gembin

OSGi, Eclipse Equinox, ECF, Virgo, Gemini, Apache Felix, Karaf, Aires, Camel, Eclipse RCP

HBase, Hadoop, ZooKeeper, Cassandra

Flex4, AS3, Swiz framework, GraniteDS, BlazeDS etc.

There is nothing that software can't fix. Unfortunately, there is also nothing that software can't completely fuck up. That gap is called talent.

About Me

 

JPA 学习

Defining Your Object Model with JPA


In the perfect world, your object model would map seamlessly to your database schema. Most organizations however, have database naming standards, requirements for how relationships are modeled and columns that all tables must have.

The following example will show how to use JPA in a real-world scenario. You’ve been asked to quickly mock up a simple blog application; here are your constraints:

  • All database table names are uppercase, words separated by an underscore (“_”)
  • All table columns must start with the table name, or its initials
  • All mutable tables need to track who created and updated a row and when the insert and update occurred
  • Object relationships need to be managed intelligently
  • You should use as few trips as possible to the database to make sure we can handle current and future load

The application should include the following objects:

  • User
  • Blog entry
  • Comments
  • The comments need to support nesting, so a user can reply to another’s comment, there is no limit to the nesting
  • You need to list all users who start comment threads, comments without responses, etc.

Here is one possible object model:

Figure 1: Blog Object Model

ModelBase is an abstract superclass of three entities: BlogPost, Comment, and User. It also defines fields for who created and modified each entity and when the changes occurred. The nested comment requirement is taken care of by Comment’s parent and child relationships.

The database schema for the object model is:

Figure 2: Blog Entity Relationship Diagram

The database tables can be seen here:

The root object of the class hierarchy is ModeBase. The @Column annotation maps each field in ModelBase to a database table column. The problem is, ModelBase does not follow the database naming conventions listed above. This will be fixed with the concrete class definitions a little later. Here is ModelBase:

 1 import javax.persistence.*;
2
3 @MappedSuperclass
4 @EntityListeners({ModelListener.class})
5 public abstract class ModelBase {
6
7 @Id
8 @GeneratedValue(strategy = GenerationType.IDENTITY)
9 @Column(name="id")
10 private Long id;
11
12 @Version
13 @Column(name="version")
14 private Integer version;
15
16 @ManyToOne(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
17 @JoinColumn(name="created_by_user_id")
18 private User createdByUser;
19
20 @Temporal(value = TemporalType.TIMESTAMP)
21 @Column(name = "date_created", nullable = false)
22 private Date dateCreated;
23
24 @ManyToOne(fetch=FetchType.LAZY)
25 @JoinColumn(name="updated_by_user_id")
26 private User updatedByUser;
27
28 @Temporal(value = TemporalType.TIMESTAMP)
29 @Column(name = "date_updated", nullable = false)
30 private Date dateUpdated;
31
32 // methods removed for readability
33 }

ModelBase uses the @MappedSuperclass annotation to tell the JPA persistence provide that this object is not an entity but it’s fields will be included in each entities table (for the entities that subclass ModelBase). You can use a mapped superclass to define all common fields. In this case it defines a field for optimist locking, version, primary key, id and fields for date created and updated. The second annotation, @EntityListener defines a class to be called by the JPA persistence provider at various lifecycle events. ModelListener, sets the user who created and modified each entity and when it was created and updated. Here is ModelListener:

 1 public class ModelListener {
2
3 @PrePersist
4 public void setDatesAndUser(ModelBase modelBase) {
5
6 // set createdBy and updatedBy User information
7 User currentUser = UserUtil.getCurrentUser();
8
9 // check to see if modelBase and currentUser are
10 // the same, if so, make currentUser modelBase.
11 if (modelBase.equals(currentUser)) {
12 currentUser = (User) modelBase;
13 }
14
15 if (currentUser != null) {
16 if (modelBase.getCreatedByUser() == null) {
17 modelBase.setCreatedByUser(currentUser);
18 }
19 modelBase.setUpdatedByUser(currentUser);
20 }
21
22 // set dateCreated and dateUpdated fields
23 Date now = new Date();
24 if (modelBase.getDateCreated() == null) {
25 modelBase.setDateCreated(now);
26 }
27 modelBase.setDateUpdated(now);
28 }
29 }

ModelListener has only one method, setDatesAndUser(ModelBase modelBase). This method is tied to the pre-persist entity lifecycle event and will always be called just before an entity is persisted to the database. This gives us a convenient way to set when and by whom the object is created and updated.

JPA requires that entity listeners be stateless and therefore we need a way to establish who the current user of our system is so we can set the createdByUser and updatedByUser fields. The UserUtil class uses a ThreadLocal to store the current user and provides an easy way for clients to establish the “current” user of the system.

 1 public class UserUtil {
2
3 private static ThreadLocal<User> currentUser =
4 new ThreadLocal<User>();
5
6 public static void setCurrentUser(User user) {
7 currentUser.set(user);
8 }
9
10 public static User getCurrentUser() {
11 return currentUser.get();
12 }
13 }

If you want to turn off any inherited entity listeners for a particular entity, you can use @ExcludeSuperclassListeners. This annotation does not have any elements, you add it to your entity like this:

1 @ExcludeDefaultListeners
2 public class Comment extends ModelBase implements Serializable

Now, Comment will not be passed to ModelListener when it’s persisted.

There are a couple of drawbacks to using entity listeners, most notably the lack of any lifecycle methods for the listener itself, it must be stateless, and it must have a public no-arg constructor. If you can live with these restrictions entity listeners are a good way to hook into the JPA entity lifecycle.

JPA also supports callback methods, these are methods in your object model class itself and must have the following method signature: void methodName(). Use the entity lifecycle event annotations (e.g. @PrePersist or @PostPersist) to indicate which event the callback method participates in. Here is an example of a callback method (continuing with the example of Comment turning off it’s entity listeners):

 1 @ExcludeDefaultListeners
2 public class Comment extends ModelBase implements Serializable {
3
4 // code removed for readability
5
6 @PrePersist
7 public void setDates() {
8 Date now = new Date();
9 if (getDateCreated() == null) {
10 setDateCreated(now);
11 }
12 setDateUpdated(now);
13 }
14 }

By default, an entities table name is the same as the entity itself. In the case of BlogPost, the default table name would be BlogPost. To explicitly set the table name an entity is mapped to, use the @Table annotation. To change the column mappings defined in ModelBase, use @AttributeOverride. If you need to override more than one field, use the @AttributeOverrides (plural) annotation. Here is how you change the mappings:

1 @Entity
2 @Table(name = "BLOG_POST")
3 @AttributeOverrides( { @AttributeOverride(name = "id", column = @Column(name = "BP_ID")),
4 @AttributeOverride(name="version", column=@Column(name="BP_VERSION")),
5 @AttributeOverride(name="dateCreated", column=@Column(name="BP_DATE_CREATED")),
6 @AttributeOverride(name="dateUpdated", column=@Column(name="BP_DATE_UPDATED"))
7 })
8 public class BlogPost extends ModelBase implements Serializable {

The two remaining fields to remap from ModelBase are not attributes but associations, so you need to use a different set of annotations, @AssociationOverrides (plural) and @AssociationOverride. Here is how you rename the createdByUser and updatedByUser foreign key columns in the BlogPost entity:

1 @AssociationOverrides( {
2 @AssociationOverride(name="createdByUser",
3 joinColumns=@JoinColumn(name="BP_CREATED_BY_USER_ID")),
4
5 @AssociationOverride(name="updatedByUser",
6 joinColumns=@JoinColumn(name="BP_UPDATED_BY_USER_ID"))
7 })

As you can see, the @AssociationOverride annotation is a little be more complex than @AttributeOverride because it has a nested annotation, @JoinColumn.

As you can see in Figure 1, there are three more fields to be defined in BlogPost. Here is their definition:

1     @Lob
2 @Column(name = "BP_CONTENT")
3 private String content;
4
5 @Column(name="BP_TITLE")
6 private String title;
7
8 @Column(name = "BP_EDIT_COUNT")
9 private Integer editCount;

The @Lob annotation tells the persistence provider that content is bound to a large object type column. The @Column annotation defines the column name the content field is mapped to. Use the @Column annotation to map an entities field to a specific column name.

One last thing to look at is BlogPost’s relationship to Comment. Here is how the comments field is defined:

1     @OneToMany(cascade={CascadeType.PERSIST,CascadeType.MERGE},
2 fetch=FetchType.LAZY)
3 @Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
4 private List<Comment> comments = new ArrayList<Comment>();

The OneToMany annotation tells the persistence provider this is an association and not an attribute. The cascade element tells the persistence provider to persist or merge any comments associated with this instance when blog post is persisted or merged. Unfortunately, JPA does not provide a cascade type for managing orphaned deletes. Fortunately, the Hibernate team has created a set of Hibernate extension annotations to make managing relationships (and every other aspect of an entity) easier. The delete orphan cascade annotation lets Hibernate know it can delete a row from the comment table if a comment is removed from the comments list. Without this annotation, you would have to manually delete the removed comment.

There are two ways for the persistence provider to map a one-to-many relationship in the database. One is to use a join table (a unidirectional relationship) and the other is to have a foreign key column in the many side of the relationship (a bidirectional relationship). Given the BlogPost to Comment relationship, if you just defined the blog post to comment relationship as shown above, JPA should expect the following tables in the database:

Figure 3: Blog Post to Comment join table

You eliminate the join table, BlogPost_Comment, by defining a bidirectional relationship. To tell JPA this is a bidirectional relationship, you need to add the mappedBy element to the @OneToMany annotation like this:

1     @OneToMany(cascade={CascadeType.PERSIST,CascadeType.MERGE},
2 fetch=FetchType.LAZY, mappedBy="blogPost")
3 @Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
4 private List<Comment> comments = new ArrayList<Comment>();

The mappedBy element indicates the owning-side of the relationship, or the field in Comment that will hold a reference to BlogPost. Here is the comment side of the relationship:

1     @ManyToOne
2 @JoinColumn(name="COMMENT_BLOG_POST_ID")
3 private BlogPost blogPost;

By defining the post to comments relationship this way we eliminate the need for a join table.

The Comment entity has the same attribute and association overrides as BlogPost (mapping to different column names of course), however it has an interesting relationship to itself. Comments support nested comments, so a comment can contain a list of responses to itself. To create this relationship, Comment needs two fields, one for the collection of comments and another to represent the parent comment. Here is how the fields are defined:

1     @ManyToOne
2 @JoinColumn(name="COMMENT_PARENT")
3 private Comment parent;
4
5 @OneToMany(mappedBy="parent",
6 cascade={CascadeType.MERGE, CascadeType.PERSIST})
7 @OrderBy("dateCreated DESC")
8 private List<Comment> children = new ArrayList<Comment>();

These relationships are the same as any other one-to-many bidirectional relationship between BlogPost and Comment; the only difference is both sides of the relationship are in the same class. The parent field holds the primary key of the parent comment.

Now that you have the fields defined, you need to add methods to manage the relationship. Both sides of the relationship need to be set in order for the Comment entity to behave properly. To ensure the relationship is set up correctly the java bean property methods for children need to be slightly modified. Since the JPA annotations used in the Comment entity are field based, the JPA persistence provider does not use property accessor methods to set its state. This means we do not need the public void setChildren(List<Comment> children) method for the persistence provider. In addition, by removing this method clients of our model cannot directly set the collection. Next is the public List<Comment> getChildren() method; like setChildren() this method allows clients to directly modify a comments children. To fix this, make getChildren() return an immutable list, like this:

1     public List<Comment> getChildren() {
2 return Collections.unmodifiableList(children);
3 }

The final step is to define methods to add and remove child comments. Here is the method for adding child comments:

 1     public void addChildComment(Comment comment) {
2 if (comment == null) {
3 throw new IllegalArgumentException("child comment is null!");
4 }
5
6 // check to see if comment has a parent
7 if (comment.getParent() != null) {
8 // if there is a parent check to see if it's already
9 // associated to this comment
10 if (comment.getParent().equals(this)) {
11 // if this instance is already the parent, we can just return
12 return;
13 }
14 else {
15 // disconnect post from it's current relationship
16 comment.getParent().children.remove(this);
17 }
18 }
19
20 // make this instance the new parent
21 comment.setParent(this);
22 children.add(comment);
23 }

If you want to add a remove comment method, it might look like this:

 1     public void removeChildComment(Comment comment) {
2 if (comment == null) {
3 throw new IllegalArgumentException("child comment is null!");
4 }
5
6 // make sure we are the parent before we break the relationship
7 if (comment.parent != null && comment.getParent().equals(this)) {
8 comment.setParent(null);
9 children.remove(comment);
10 }
11 else {
12 throw new IllegalArgumentException(
13 "child comment not associated with this instance");
14 }
15 }

Not only does this method remove a comment from its parents children collection, it makes sure the parent of comment is this instance. If comment were not associated with this instance, removing it from children would have no affect. However, setting its parent to null would leave a dangling comment in the database.

The last two methods to deal with are getParent() and setParent() . Here is their definition:

1     public Comment getParent() {
2 return parent;
3 }
4
5 private void setParent(Comment parent) {
6 this.parent = parent;
7 }

The addChildComment() method uses setParent() , however; clients of the object model should not be able to change a comments parent. The easiest way to ensure this is to make setParent() private. getParent() is fine the way it is. The pattern used for Comments parent/child relationship can be applied to any bidirectional association in your object model.

Deciding where to put the relationship management methods in a bidirectional relationship is rather arbitrary since the purpose of the methods is to ensure the relationships are established correctly. With one-to-many bidirectional relationships, I tend to put the management methods on the “one” side. In the case of Comment this isn’t obvious, but in the BlogPost to Comment relationship I placed the relationship methods in BlogPost. This seems more natural; you add comments to a blog post, not the other way around. If you have a many-to-many relationship it really doesn’t matter which side has the methods so pick one and restrict the other side.

JPA provides methods for fetching entities by their primary key but it might be a good idea to provide some queries to fetch blog posts and comments by User, count the number of comments and blog posts a user has made, determine which comments do not have child comments, and find all root comments (those without a parent).

JPA provides three different types of queries, dynamic queries, static or named queries, and native queries. Dynamic and static queries use the Java Persistence Query Language and native queries use SQL. A dynamic query is one that is processed at runtime, meaning it is parsed and SQL generated every time it’s created. Static queries are processed when the persistence provider loads your object model. This means static queries are parsed once and reused every time you run the query.

To declare a named or static query, you use the @NamedQuery annotation. If you have more than one named query, you need to use the @NamedQueries annotation. Both annotations can be placed on an entity or mapped superclass and are declared at the class or type level. Query names are global, that is, they are not bound to any entity. As a result, you should use some sort of naming convention. One approach is to prefix every query name with the entity name it’s associated with.

Here are the named queries used in Comment and BlogPost:

 1 @NamedQueries({
2 // select comments that do not have parent comments
3 @NamedQuery(name = "comment.rootComments",
4 query = "SELECT c FROM Comment c WHERE c.parent IS NULL"),
5
6 // select comments made by a User
7 @NamedQuery(name = "comment.userComments",
8 query = "SELECT c FROM Comment c WHERE c.createdByUser = :user"),
9
10 // count the number of comments made by a user
11 @NamedQuery(name = "comment.userCount",
12 query = "SELECT COUNT(c) FROM Comment c WHERE c.createdByUser = :user"),
13
14 // select the comments a user made without responses
15 @NamedQuery(name = "comment.noChildren",
16 query = "SELECT c FROM Comment c WHERE c.children IS EMPTY AND c.parent IS NULL AND c.createdByUser = ?1")
17 })
18 public class Comment extends ModelBase implements Serializable{ ... }
19
20
21 @NamedQueries( {
22 @NamedQuery(name = "blogPost.createdByUser",
23 query = "SELECT p FROM BlogPost p WHERE p.createdByUser = ?1"),
24
25 // determine the number of posts a User has made
26 @NamedQuery(name = "blogPost.postCount",
27 query = "SELECT COUNT(p) FROM BlogPost p WHERE p.createdByUser = ?1"),
28
29 // fetch a blog post and eagerly fetch its comments
30 @NamedQuery(name = "blogPost.createdByUserComments",
31 query = "SELECT p FROM BlogPost p JOIN FETCH p.comments as c WHERE p.createdByUser = ?1")
32 })
33 public class BlogPost extends ModelBase implements Serializable { ... }

Each of the above queries uses either positional or named parameters; positional parameters use the “?1” syntax and named parameters use the “:name” syntax. Here is how you would create and execute a static query which uses named parameters:

1 // assume entityManager exists
2 User user = UserUtil.getCurrentUser();
3
4 // Here is the definition of comment.userComments for reference
5 // SELECT c FROM Comment c WHERE c.createdByUser = :user
6
7 Query q = entityManager.createNamedQuery("comment.userComments");
8 q.setParameter("user", user);
9 List results = q.getResultList();

Another query using positional parameters (note: the count function returns a Long):

1 // assume entityManager exists
2 User user = UserUtil.getCurrenstUser();
3
4 // Here is the definition of blogPost.postCount for reference
5 // SELECT COUNT(p) FROM BlogPost p WHERE p.createdByUser = ?1
6
7 Query q = entityManager.createNamedQuery("blogPost.postCount");
8 q.setParameter(1, user);
9 Long result = (Long) q.getSingleResult();

The JPQL has many built in functions and expressions to assist you in working with your object model. If you have not checked out JPQL before, you might be surprised by how powerful it is.

The last query to look at is blogPost.createdByUserComments. This query uses the fetch join operator to eagerly fetch a blog posts’ comments. The BlogPost to Comment association is defined as being LAZY (here is the relationship again):

1     @OneToMany(cascade={CascadeType.PERSIST,CascadeType.MERGE},
2 fetch=FetchType.LAZY, mappedBy="blogPost")
3 @Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
4 private List<Comment> comments = new ArrayList<Comment>();

Making the relationship LAZY (by setting the fetch element of the @OneToMany annotation to FetchType.LAZY) enables your application to fetch a blog post, say when a user wants to edit the post, without also fetching all the comments. When you want to fetch the blog post and its comments, you can use the named query, blogPost.createdByUserComments. Sometimes you will want to make a relationship eager, by setting the fetch element of the @OneToMany annotation to FetchType.EAGER, so when you fetch entity “A” the persistence provider will also fetch entity “B”. Having one fetch to get back several objects is more efficient than multiple trips to the database but you will need to decide the correct semantics for each collection in your object model.

If you haven’t looked at JPA before, I hope this short example will encourage you to give it a try. Hibernate’s latest release has excellent JPA support and provides an extensive set of JPA extension annotations.

Get the Source Code here.

Biography

Chris Maki is a Principal Software Engineer at Overstock.com. Before joining Overstock.com, Chris was an independent consultant specializing in server-side Java development. He has been designing and building server-side systems for 15 years in C++, Objective-C, and Java.

Chris spent many years working as an enterprise architect specializing in large-scale Java system design and development. In addition to being a Java evangelist, he is an avid proponent of Agile Software Development.

Chris is the President of the Utah Java User Group and a member of the WebBeans (JSR-299) and JPA 2.0 (JSR-317) expert groups.

When Chris isn�t playing with his computer, you can find him hanging out with his wonderful wife of 12 years, Nicole, and their three boys, Trenton, Avery, and Brayden. You can find Chris on his blog at http://www.jroller.com/page/cmaki.

posted on 2008-04-30 19:19 gembin 阅读(2469) 评论(1)  编辑  收藏 所属分类: JavaEE

评论

# re: JPA 学习 2010-06-18 11:29 lacewigs

great site. Thanks for sharing with us!!!  回复  更多评论   


只有注册用户登录后才能发表评论。


网站导航:
 

导航

统计

常用链接

留言簿(6)

随笔分类(440)

随笔档案(378)

文章档案(6)

新闻档案(1)

相册

收藏夹(9)

Adobe

Android

AS3

Blog-Links

Build

Design Pattern

Eclipse

Favorite Links

Flickr

Game Dev

HBase

Identity Management

IT resources

JEE

Language

OpenID

OSGi

SOA

Version Control

最新随笔

搜索

积分与排名

最新评论

阅读排行榜

评论排行榜

free counters