Showing posts with label jpa. Show all posts
Showing posts with label jpa. Show all posts

Saturday, March 01, 2008

My first Custom Annotation

In a previous post, I noted that although I did not know much about how annotations worked, I seem to be using them more and more nowadays. I guess it was only a matter of time before I found a use-case where I would need to write my own annotation. Anyway, the opportunity presented itself when I was trying to write out a JPA @Entity bean described in my previous post out to a Lucene index. This post describes the annotation itself, and how I used it in my index building code.

My objective was to annotate some of the fields in my @Entity beans to tell the indexing process that these fields should be picked up. Additionally, I wanted to specify the Store and Index attributes that will govern how they will be stored and indexed.

To do this, I created the following Annotation class. The Storable and Indexable enums have elements that shadow the Field.Store and Field.Index enums in Lucene.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// IndexMeta.java
package com.mycompany.myapp.indexing;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Property level annotation to indicate indexing meta-information.
 * Bean properties that should be indexed should be annotated with this.
 * Additional field level indexing metadata is supplied through the 
 * attributes Indexable and Storable. 
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface IndexMeta {

  public enum Indexable {NO, NO_NORMS, TOKENIZED, UN_TOKENIZED};
  public enum Storable {COMPRESS, NO, YES};
  
  Indexable indexable() default Indexable.NO;
  Storable storable() default Storable.NO;
}

To test this, we created two BookArticle objects in the database. Since BookArticle objects inherit from Article objects, we need to annotate properties in both classes. This is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Article.java
package com.mycompany.myapp.persistence;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.Entity;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;

import com.mycompany.myapp.indexing.IndexMeta;
import com.mycompany.myapp.indexing.IndexMeta.Indexable;
import com.mycompany.myapp.indexing.IndexMeta.Storable;

/**
 * The article superclass table contains all the common fields.
 */
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(discriminatorType=DiscriminatorType.INTEGER, name="articleTypeId")
public abstract class Article extends ModelBase {

  @IndexMeta(indexable=Indexable.UN_TOKENIZED, storable=Storable.YES)
  private String articleId;
  @IndexMeta(indexable=Indexable.TOKENIZED, storable=Storable.YES)
  private String title;
  @IndexMeta(indexable=Indexable.TOKENIZED, storable=Storable.YES)
  private String summary;
  @IndexMeta(indexable=Indexable.NO, storable=Storable.YES)
  private String url;
  private String fileLocation;

  ... getters and setters removed ...  
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// BookArticle.java
package com.mycompany.myapp.persistence;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

import com.mycompany.myapp.indexing.IndexMeta;
import com.mycompany.myapp.indexing.IndexMeta.Indexable;
import com.mycompany.myapp.indexing.IndexMeta.Storable;

/**
 * Models a book article.
 */
@Entity
@DiscriminatorValue("2")
public class BookArticle extends Article {

  private static final long serialVersionUID = -2274023497279749079L;
  
  @IndexMeta(indexable=Indexable.TOKENIZED, storable=Storable.YES)
  private String authorName;
  private String publisherName;
  private String isbnNumber;
  
  ... getters and setters removed ...
}

Finally, we use Java 1.5's new reflection methods to find and apply the annotations during our indexing process. The JUnit test below shows how to get all the BookArticle beans from the database using JPA, and then passing the beans to a Lucene IndexWriter.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// IndexingTest.java
package com.mycompany.myapp.indexing;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.Query;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field.Index;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.index.IndexWriter;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.mycompany.myapp.indexing.IndexMeta.Indexable;
import com.mycompany.myapp.indexing.IndexMeta.Storable;
import com.mycompany.myapp.persistence.BookArticle;

/**
 * Test case to demonstrate indexing using the IndexMeta annotation.
 */
public class IndexingTest {

  private final Log log = LogFactory.getLog(getClass());
  
  private final String INDEX_LOCATION = "/tmp/testindex";
  
  private EntityManagerFactory emf;
  private EntityManager em;
  private IndexWriter indexWriter;
  
  @Before
  public void setUp() throws Exception {
    emf = Persistence.createEntityManagerFactory("myapp");
    em = emf.createEntityManager();
    indexWriter = new IndexWriter(INDEX_LOCATION, new StandardAnalyzer(), true);
  }

  @After
  public void tearDown() throws Exception {
    em.close();
    emf.close();
    indexWriter.optimize();
    indexWriter.close();
  }

  /**
   * Builds a Lucene index using the List of beans returned by a Query.
   * @throws Exception if one is thrown.
   */
  @Test
  public void testBuildIndex() throws Exception {
    // get the indexing metadata
    Map<String,IndexMeta> indexingMetadata = 
      buildIndexingMetadata(BookArticle.class);
    // get the data
    Query q = em.createQuery("select ba from BookArticle ba");
    List<BookArticle> articles = 
      (List<BookArticle>) q.getResultList();
    for (BookArticle article : articles) {
      Document doc = new Document();
      for (String fieldName : indexingMetadata.keySet()) {
        String value = getFieldValue(article, fieldName);
        if (value == null) {
          continue;
        }
        IndexMeta indexMeta = indexingMetadata.get(fieldName);
        Store storeProperty = getStoreProperty(indexMeta.storable());
        Index indexProperty = getIndexProperty(indexMeta.indexable());
        org.apache.lucene.document.Field f = 
          new org.apache.lucene.document.Field(
          fieldName, value, storeProperty, indexProperty);
        doc.add(f);
      }
      indexWriter.addDocument(doc);
    }
  }

  /**
   * Reflectively gets the String value of the named bean property. If an 
   * exception occurs, the value returned is null.
   * @param obj the Article object.
   * @param fieldName the name of the bean property.
   * @return the String value of the bean property.
   */
  private String getFieldValue(Object obj, String fieldName) {
    try {
      return String.valueOf(BeanUtils.getProperty(obj, fieldName));
    } catch (Exception e) {
      return null;
    }
  }
  
  /**
   * Reflectively build a Map of bean property name to the annotated IndexMeta
   * properties. Climbs the object tree until the Object class is reached.
   * @param type the Article type to build the Index metadata for.
   * @return a Map of bean property name to the associated IndexMeta annotation.
   */
  private Map<String,IndexMeta> buildIndexingMetadata(Class<?> type) {
    Map<String,IndexMeta> indexingMetadata = 
      new HashMap<String,IndexMeta>();
    do {
      Field[] fields = type.getDeclaredFields();
      for (Field field : fields) {
        IndexMeta indexMeta = field.getAnnotation(IndexMeta.class);
        if (indexMeta == null) {
          // no annotation, no lucene field
          continue;
        }
        indexingMetadata.put(field.getName(), indexMeta);
      }
      type = type.getSuperclass();
    } while (type != Object.class);
    return indexingMetadata;
  }

  /**
   * Helper method to map the Field.Index value from IndexMeta.Indexable
   * @param indexable an instance of Indexable.
   * @return an instance of Field.Index.
   */
  private Index getIndexProperty(Indexable indexable) {
    switch(indexable) {
      case NO_NORMS:
        return Index.NO_NORMS;
      case TOKENIZED:
        return Index.TOKENIZED;
      case UN_TOKENIZED:
        return Index.UN_TOKENIZED;
      case NO:
      default:
        return Index.NO;
    }
  }

  /**
   * Helper method to return Field.Store value for IndexMeta.Storable.
   * @param storable an instance of Storable.
   * @return an instance of Field.Store.
   */
  private Store getStoreProperty(Storable storable) {
    switch(storable) {
      case COMPRESS:
        return Store.COMPRESS;
      case YES:
        return Store.YES;
      case NO:
      default:
        return Store.NO;
    }
  }
}

And of course, the obligatory screenshot of what this index looks like in Luke.

I hope this has been useful. I had read about annotations in the official annotation documentation and in the O'Reilly Java 1.5 Tiger - A Developer's Handbook by Brett McLaughlin and David Flanagan, but I could never come up with a use-case where I would need to build my own. This example may resonate a little better with Java developers who use ORMs and Lucene.

On a somewhat related note, check out Steve Yegge's blog post "Execution in the Kingdom of Nouns". Not only is it quite hilarious, it is also the clearest analogy I have seen that explains the differences between Object Oriented programming and functional programming. Why related? Well, we started with classes and interfaces. Java 1.5 gave us Enums and Annotations. Maybe it is time Java gave us Function objects as well, so we can use them directly when requirements dictate, rather than setting up anonymous holder classes to do the same thing.

Saturday, February 23, 2008

Implementing Inheritance in database with JPA

This post is the result of a casual conversation with one of our engineers. About couple of months ago, he happened to mention that if we ever got around to refactoring some code in one of our systems, using JPA for database persistence would be preferable instead of JDBC, as we do currently. I did not know much about JPA at the time, except that it was something to do with EJB3 and Hibernate, so I decided to read up about it.

Turns out I was only partially correct. JPA is an API which is implemented by various popular ORM implementations such as Toplink from Oracle, Kodo from BEA, Hibernate from Redhat's JBoss group and OpenJPA from Apache. In a sense, JPA is to ORMs what JDBC was to databases. It provides a common API to work against multiple ORMs, so developers need to learn one API to work against any JPA compliant ORM, and a company could (at least in theory) switch between ORM providers without changing any source code. Based on previous experience with coding against Hibernate 2.x, I think JPA code (using Hibernate) also looks much simpler.

One book I found very helpful was Chris Maki's "JPA 101: Java Persistence Explained" from Sourcebeat. Its available as a PDF eBook and is fairly reasonably priced, and contains almost no fluff, unless you count the first chapter, which deals with setting up the example application using Eclipse and Maven, which probably most (but not all) developers would be familiar with. However, the book has plenty of examples and is very readable. I would recommend it strongly if you are trying to get started with JPA.

After reading the book, I realized that the engineer's suggestion was pretty much spot-on. In particular, I liked the concept of implementing the application's object inheritance hierarchy directly in the database, which I describe below.

So basically, we license content from various different providers. All content has certain metadata that we will always extract, such as title and summary, and we would always assign that article a unique URL on our website. However, each provider is different, and some may provide additional metadata that is unique to the provider. So basically, consider two data sources, one called Magazine and one called Book. The object UML would look something like this:

The ModelBase class is something that is needed by JPA, and its convenient to set up a single one that enforces on the correct id class (for the given database) for any persistable bean in the application. The Article class specifies the properties we would always extract, regardless of provider, and the MagazineArticle and BookArticle specify the metadata unique to each provider.

JPA allows three different inheritance strategies, which most providers implement. I chose the JOINED strategy, where the properties common to all subclasses are stored in a master table, and the unique properties for each Article subclass are stored in their own tables, linked back using the autogenerated surrogate id as the foreign key. This has the advantage of being quite normalized, and if the inheritance structure is relatively flat (mine would only be one level deep), then the performance overhead of doing joins is minimal. The corresponding database structure for the JOINED subclass strategy would look like this:

Notice the absence of the discriminator column in the Article table above. I spent nearly a day trying to figure out what I was doing wrong before I found that Hibernate, unlike other ORM implementations JPA covers, does not need to use the discriminator column for JOINED inheritance, and hence apparently has no plans to conform to a standard it considers broken in this regard. It does not affect me that much since I plan on using Hibernate anyway, but while I am no expert on these things, I think that this strategy may harm Hibernate's adoption by big companies for whom JPA compliance is a higher priority. The least that should be done IMO is to adequately document this abberation, so other developers are not tripped up like I was.

But anyway, on to the code. Since I was using MySQL, I decided to build a ModelBase object which is annotated as @MappedSuperclass, and which specifies the id type and generation strategy. JPA can work with legacy ids, but it is considerably more work to implement than autogenerated surrogate keys, so I decided to keep things simple. In any case, if we decide to switch to some other database, all we would need to do is change the id generation strategy in this one class (and the provider in the persistence.xml class).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ModelBase.java
package com.mycompany.myapp.persistence;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.OneToMany;

@MappedSuperclass
public abstract class ModelBase implements Serializable {

  @Id 
  @GeneratedValue(strategy=GenerationType.AUTO)
  private Long id;
  
  public Long getId() {
    return id;
  }
  
  public void setId(Long id) {
    this.id = id;
  }
}

The above class is marked as @MappedSuperclass, so there is no corresponding table in the database. The next class is the main Article class, also abstract, since we don't want to use this as is, ever. For each content provider, we want to add in extra metadata unique to that provider.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Article.java
package com.mycompany.myapp.persistence;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.Entity;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;

@Entity
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(discriminatorType=DiscriminatorType.INTEGER, name="articleTypeId")
public abstract class Article extends ModelBase {

  private String articleId;
  private String title;
  private String summary;
  private String url;
  
  public String getArticleId() {
    return articleId;
  }
  
  public void setArticleId(String articleId) {
    this.articleId = articleId;
  }
  
  public String getTitle() {
    return title;
  }
  
  public void setTitle(String title) {
    this.title = title;
  }
  
  public String getSummary() {
    return summary;
  }
  
  public void setSummary(String summary) {
    this.summary = summary;
  }
  
  public String getUrl() {
    return url;
  }
  
  public void setUrl(String url) {
    this.url = url;
  }
}

Notice that we have the @DiscriminatorColumn annotation. With Hibernate, this has absolutely no effect, and in fact, you don't even have to have this for InheritanceType.JOINED. The only time you need this for Hibernate is for single table inheritance.

The subclasses of Article are shown below. As before, we don't need to have the @DiscriminatorValue annotation in either subclass, since Hibernate will not use it or record it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// MagazineArticle.java
package com.mycompany.myapp.persistence;

import java.util.Date;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
@DiscriminatorValue("1")
public class MagazineArticle extends Article {

  private static final long serialVersionUID = 4276734517833727032L;

  private String publicationName;
  
  @Temporal(TemporalType.DATE)
  private Date publicationDate;
  
  private String authorName;
  
  public String getPublicationName() {
    return publicationName;
  }
  
  public void setPublicationName(String publicationName) {
    this.publicationName = publicationName;
  }
  
  public Date getPublicationDate() {
    return publicationDate;
  }
  
  public void setPublicationDate(Date publicationDate) {
    this.publicationDate = publicationDate;
  }
  
  public String getAuthorName() {
    return authorName;
  }

  public void setAuthorName(String authorName) {
    this.authorName = authorName;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// BookArticle.java
package com.mycompany.myapp.persistence;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("2")
public class BookArticle extends Article {

  private static final long serialVersionUID = -2274023497279749079L;
  
  private String authorName;
  private String publisherName;
  private String isbnNumber;
  
  public String getAuthorName() {
    return authorName;
  }
  
  public void setAuthorName(String authorName) {
    this.authorName = authorName;
  }
  
  public String getPublisherName() {
    return publisherName;
  }
  
  public void setPublisherName(String publisherName) {
    this.publisherName = publisherName;
  }
  
  public String getIsbnNumber() {
    return isbnNumber;
  }
  
  public void setIsbnNumber(String isbnNumber) {
    this.isbnNumber = isbnNumber;
  }
}

Finally, we need to set up the database. I created a database and populated the tables shown in the database diagram above. Then to link up the code and the database, we create a persistence.xml file in src/main/resources/META-INF directory, like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
    version="1.0">
  <persistence-unit name="myapp" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <class>com.mycompany.myapp.persistence.ModelBase</class>
    <class>com.mycompany.myapp.persistence.Article</class>
    <!-- put your article subclasses here -->
    <class>com.mycompany.myapp.persistence.MagazineArticle</class>
    <class>com.mycompany.myapp.persistence.BookArticle</class>
    <properties>
      <property name="hibernate.connection.driver_class" 
        value="com.mysql.jdbc.Driver"/>
      <property name="hibernate.connection.url" 
        value="jdbc:mysql://localhost:3306/contentdb"/>
      <property name="hibernate.connection.username" value="jpauser" />
      <property name="hibernate.connection.password" value="jpauser"/>
      <property name="hibernate.dialect" 
        value="org.hibernate.dialect.MySQLDialect"/>
      <property name="hibernate.cache.provider_class" 
        value="org.hibernate.cache.HashtableCacheProvider"/>
      <property name="hibernate.show_sql" value="true"/>
    </properties>
  </persistence-unit>
</persistence>

Here is a JUnit to insert data into this database structure. Most people would probably use DBUnit to do this, but my objective was to find how to insert data into the database using JPA, so I did a unit test. Notice how the code is pretty much unaware of the underlying database structure. It deals with Java objects, and the JPA entityManager does the work of creating and executing the SQL for it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// ArticlePersistenceTest.java
package com.mycompany.myapp.persistence;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.Query;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class ArticlePersistenceTest {

  private final Log log = LogFactory.getLog(getClass());
  
  private EntityManager entityManager;
  private EntityManagerFactory entityManagerFactory;
  
  @Before
  public void setUp() throws Exception {
    entityManagerFactory = Persistence.createEntityManagerFactory("myapp");
    entityManager = entityManagerFactory.createEntityManager();
  }

  @After
  public void tearDown() throws Exception {
    entityManager.close();
    entityManagerFactory.close();
  }

  @Test
  public void testPersistAdamArticle() throws Exception {

    // build and persist a magazine article
    MagazineArticle ma = new MagazineArticle();
    ma.setArticleId("mag-000-001");
    ma.setTitle("Magazine Article Title 1");
    ma.setSummary("This is a short summary of magazine article 0001...");
    ma.setUrl("/path/to/mag-art-0001");
    Calendar pubDateCalendar = Calendar.getInstance();
    pubDateCalendar.set(2002, 11, 15); 
    ma.setPublicationDate(pubDateCalendar.getTime());
    ma.setPublicationName("Harper Collins");
    ma.setAuthorName("Dr Doolittle");

    entityManager.getTransaction().begin();
    entityManager.persist(ma);
    entityManager.getTransaction().commit();

    // build and persist a book article
    BookArticle ba = new BookArticle();
    ba.setArticleId("bk-000-001");
    ba.setTitle("Book Article Title 1");
    ba.setSummary("This is a short summary of book article 0001...");
    ba.setUrl("/path/to/book-art-0001");
    ba.setAuthorName("Dr Busybody");
    ba.setPublisherName("Tom Collins");
    ba.setIsbnNumber("1234-5678");

    entityManager.getTransaction().begin();
    entityManager.persist(ba);
    entityManager.getTransaction().commit();
    
    // select all articles
    Query q = entityManager.createQuery("select a from Article a");
    List<Article> results = q.getResultList();
    for (Article result : results) {
      log.debug("result=" + result.toString());
    }
  }
}

In my code, I purposely kept the code as free of override annotations as possible, which may not be possible in real life. For example, your DBA may enforce a particular table naming or column naming structure. You can map beans to corresponding table names using the @Table annotation, and property names to corresponding column names using the @Column annotation.

Another thing I noticed was that the performance of the JPA code is slightly slower compared to straight JDBC calls. However, this is expected, since JPA provides a level of abstraction that allows us to write more readable code, and does some generic heavy lifting behind the scenes that would be concievably less efficient than hand crafted SQL. I think this becomes less noticeable when we run the applications over longer periods of time and we are able to take advantage of the ORM's cache.

Overall, I was quite impressed with JPA. The JOINED subclass strategy is conceptually nicer than the table per class strategy we have currently implemented using straight JDBC. With a JOINED strategy, we can enforce that certain fields will need to be populated regardless of a provider. It is also normalized, with no repetition of column names across individual tables. Often, the implementor of a new table will use different column names or column types, which makes it harder to work with the articles in a generic way on the front end.

As for the learning curve involved with JPA, obviously there is one, but I forsee that JPA will soon be as ubiquitous as JDBC is today. Already, more and more Java shops are switching over to ORMs, and there are plenty of free and open-source products available which are as good as their commercial counterparts. Learning the JPA API will enable you to work with the JPA compliant ORMs out there, and now that it supports annotations, its just a matter of learning a few simple annotations to get going with JPA.