楼主: Sky-Tiger

JPA implementation patterns

[复制链接]
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
11#
 楼主| 发表于 2009-7-16 22:34 | 只看该作者
saveOrUpdate vs. merge

Those of you that have worked with plain Hibernate will probably have grown quite accustomed to using the Session.saveOrUpdate method to save entities. The saveOrUpdate method figures out whether the object is new or has already been saved before. In the first case the entity is saved, in the latter case it is updated.

When switching from Hibernate to JPA a lot of people are dismayed to find that method missing. The closest alternative seems to be the EntityManager.merge method, but there is a big difference that has important implications. The Session.saveOrUpdate method, and its cousin Session.update, attach the passed entity to the persistence context while EntityManager.merge method copies the state of the passed object to the persistent entity with the same identifier and then return a reference to that persistent entity. The object passed is not attached to the persistence context.

That means that after invoking EntityManager.merge, we have to use the entity reference returned from that method in place of the original object passed in. This is unlike the the way one can simply invoke EntityManager.persist on an object (even multiple times as mentioned above!) to save it and continue to use the original object. Hibernate's Session.saveOrUpdate does share that nice behaviour with EntityManager.persist (or rather Session.save) even when updating, but it has one big drawback; if an entity with the same ID as the one we are trying to update, i.e. reattach, is already part of the persistence context, a NonUniqueObjectException is thrown. And figuring out what piece of code persisted (or merged or retrieved) that other entity is harder than figuring out why we get a "detached entity passed to persist" message.

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
12#
 楼主| 发表于 2009-7-16 22:34 | 只看该作者
Putting it all together

So let's examine the three possible cases and what the different methods do:
Scenario         EntityManager.persist         EntityManager.merge         SessionManager.saveOrUpdate
Object passed was never persisted         1. Object added to persistence context as new entity
2. New entity inserted into database at flush/commit         1. State copied to new entity.
2. New entity added to persistence context
3. New entity inserted into database at flush/commit
4. New entity returned         1. Object added to persistence context as new entity
2. New entity inserted into database at flush/commit
Object was previously persisted, but not loaded in this persistence context         1. EntityExistsException thrown (or a PersistenceException at flush/commit)         2. Existing entity loaded.
2. State copied from object to loaded entity
3. Loaded entity updated in database at flush/commit
4. Loaded entity returned         1. Object added to persistence context
2. Loaded entity updated in database at flush/commit
Object was previously persisted and already loaded in this persistence context         1. EntityExistsException thrown (or a PersistenceException at flush or commit time)         1. State from object copied to loaded entity
2. Loaded entity updated in database at flush/commit
3. Loaded entity returned         1. NonUniqueObjectException thrown

Looking at that table one may begin to understand why the saveOrUpdate method never became a part of the JPA specification and why the JSR members instead choose to go with the merge method. BTW, you can find a different angle on the saveOrUpdate vs. merge problem in Stevi Deter's blog about the subject.

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
13#
 楼主| 发表于 2009-7-16 22:35 | 只看该作者
The problem with merge

Before we continue, we need to discuss one disadvantage of the way EntityManager.merge works; it can easily break bidirectional associations. Consider the example with the Order and OrderLine classes from the previous blog in this series. If an updated OrderLine object is received from a web front end (or from a Hessian client, or a Flex application, etc.) the order field might be set to null. If that object is then merged with an already loaded entity, the order field of that entity is set to null. But it won't be removed from the orderLines set of the Order it used to refer to, thereby breaking the invariant that every element in an Order's orderLines set has its order field set to point back at that Order.

In this case, or other cases where the simplistic way EntityManager.merge copies the object state into the loaded entity causes problems, we can fall back to the DIY merge pattern. Instead of invoking EntityManager.merge we invoke EntityManager.find to find the existing entity and copy over the state ourselves. If EntityManager.find returns null we can decide whether to persist the received object or throw an exception. Applied to the Order class this pattern could be implemented like this:


        Order existingOrder = dao.findById(receivedOrder.getId());
        if(existingOrder == null) {
                dao.persist(receivedOrder);
        } else {
                existingOrder.setCustomerName(receivedOrder.getCustomerName());
                existingOrder.setDate(receivedOrder.getDate());
        }

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
14#
 楼主| 发表于 2009-7-16 22:35 | 只看该作者
he pattern

So where does all this leave us? The rule of thumb I stick to is this:

    * When and only when (and preferably where) we create a new entity, invoke EntityManager.persist to save it. This makes perfect sense when we view our domain access objects as collections. I call this the persist-on-new pattern.
    * When updating an existing entity, we do not invoke any EntityManager method; the JPA provider will automatically update the database at flush or commit time.
    * When we receive an updated version of an existing simple entity (an entity with no references to other entities) from outside of our application and want to save the new state, we invoke EntityManager.merge to copy that state into the persistence context. Because of the way merging works, we can also do this if we are unsure whether the object has been already persisted.
    * When we need more control over the merging process, we use the DIY merge pattern.

I hope this blog gives you some pointers on how to save entities and how to work with detached entities. We'll get back to detached entities when we discuss Data Transfer Objects in a later blog. But next week we'll handle a number of common entity retrieval pattern first. In the meantime your feedback is welcome. What are your JPA patterns?

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
15#
 楼主| 发表于 2009-7-16 22:35 | 只看该作者
Last week I talked about how to save an entity. And once we've saved an entity we'd also like to retrieve it. Compared to managing bidirectional associations or saving entities, retrieving entities is actually rather simple. So simple I doubted whether there would be much point in writing this blog ;-) . However we did use a few nice patterns when writing code for this. And I'm interested to hear what patterns you use to retrieve entities. So here is the next instalment in the series on JPA implementation patterns.

Basically, there are two ways to retrieve an entity with JPA:

    * EntityManager.find will find an entity by its id or return null when that entity does not exists.
    * If you pass a query string specified in Java Persistence Query Language to EntityManager.createQuery it will return a Query object that can then be executed to return a list of entities or a single entity.

A Query object can also be created by referring to a named query (using EntityManager.createNamedQuery), or by passing in an SQL query (using one of the three three flavours of EntityManager.createNativeQuery). And while the name implies otherwise, a Query can also be used to execute an update or delete statement.

A named query may seem like a nice way to keep the query with the entities it queries, I've found that not to work out very well. Most queries need parameters to be set with one of the variants of Query.setParameter. Keeping the query and the code that sets these parameters together makes them both easier to understand. That is why I keep them together in the DAO and shy away from using named queries.

A convention I've found to be useful is to differentiate between finding an entity and getting an entity. In the first case null is returned when an entity cannot be found, while in the latter case an exception is thrown. Using the latter method when your code expects an entity to be present prevents NullPointerExceptions from popping up later.

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
16#
 楼主| 发表于 2009-7-16 22:35 | 只看该作者
Finding and getting a single entity by id

An implementation of this pattern for the JpaDao base class we discussed a few blogs ago can look like this (I've included the find method for contrast):

public E findById(K id) {
        return entityManager.find(entityClass, id);
}

public E getById(K id) throws EntityNotFoundException {
        E entity = entityManager.find(entityClass, id);
        if (entity == null) {
                throw new EntityNotFoundException(
                        "Entity " + entityClass.getName() + " with id " + id + " not found");
        }
        return entity;
}

Of course you'd also need to add this new method to the Dao interface:

E getById(K id);

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
17#
 楼主| 发表于 2009-7-16 22:35 | 只看该作者
Finding and getting a single entity with a query

A similar distinction can be made when we use a query to look for a single entity. The findOrderSubmittedAt method below will return null when the entity cannot be found by the query. The getOrderSubmittedAt method throws a NoResultException. Both methods will throw a NonUniqueResultException if more than one result is returned. To keep the getOrderSubmittedAt method consistent with the findById method we could map the NoResultException to an EntityNotFoundException. But since there are both unchecked exceptions, there is no real need.

Since these methods apply only to the Order object, there are a part of the JpaOrderDao:

public Order findOrderSubmittedAt(Date date) throws NonUniqueResultException {
        Query q = entityManager.createQuery(
                "SELECT e FROM " + entityClass.getName() + " e WHERE date = :date_at");
        q.setParameter("date_at", date);
        try {
                return (Order) q.getSingleResult();
        } catch (NoResultException exc) {
                return null;
        }
}

public Order getOrderSubmittedAt(Date date) throws NoResultException, NonUniqueResultException {
        Query q = entityManager.createQuery(
                "SELECT e FROM " + entityClass.getName() + " e WHERE date = :date_at");
        q.setParameter("date_at", date);
        return (Order) q.getSingleResult();
}

Adding the correct methods to the OrderDao interface is left as an exercise for the reader. ;-)

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
18#
 楼主| 发表于 2009-7-16 22:35 | 只看该作者
Finding multiple entities with a query

Of course we also want to be able to find more than one entity. In that case I've found there to be no useful distinction between getting and finding. The findOrdersSubmittedSince method just return a list of entities found. That list can contain zero, one or more entities. See the following code:

public List<Order> findOrdersSubmittedSince(Date date) {
        Query q = entityManager.createQuery(
                        "SELECT e FROM " + entityClass.getName() + " e WHERE date >= :date_since");
        q.setParameter("date_since", date);
        return (List<Order>) q.getResultList();
}

Observant readers will note that this method was already present in the first version of the JpaOrderDao.

So while retrieving entities is pretty simple, there are a few patterns you can stick to when implementing finders and getters. Of course I'd be interested to know how you handle this in your code.

P.S. JPA 1.0 does not support it yet, but JPA 2.0 will include a Criteria API. The Criteria API will allow you to dynamically build JPA queries. Criteria queries are more flexible than string queries so you can build them depending on input in a search form. And because you define them using domain objects, they are easier to maintain as references to domain objects get refactored automatically. Unfortunately the Criteria API requires you to refer to your entity's properties by name, so your IDE will not help you when you rename those.

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
19#
 楼主| 发表于 2009-7-16 22:35 | 只看该作者
Removing entities that are part of an association

Consider the example with the Order and OrderLine classes we've discussed previously. Let's say we want to remove and OrderLine from an Order and we go about it in this simple manner:

orderLineDao.remove(lineToDelete);

There is a problem with this code. When you tell the entity manager to remove the entity, it will not automatically be removed from any associations that point to it. Just like JPA does not automatically manage bidirectional associations. In this case that would be the orderLines set in the Order object pointed to by the OrderLine.order property. If I were to word this statement as a failing JUnit test case, it would be this one:

OrderLine orderLineToRemove = orderLineDao.findById(someOrderLineId);
Order parentOrder = orderLineToRemove.getOrder();
int sizeBeforeRemoval = parentOrder.getOrderLines().size();
orderLineDao.remove(orderLineToRemove);
assertEquals(sizeBeforeRemoval - 1, parentOrder.getOrderLines().size());

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
20#
 楼主| 发表于 2009-7-16 22:36 | 只看该作者
Implications

The failure of this test case has two subtle and therefore nasty implications:

    * Any code that uses the Order object after we have removed the OrderLine but will still see that removed OrderLine. Only after committing the transaction, starting a new transaction, and reloading the Order in a new transaction, it will not show up in the Order.orderLines set anymore. In simple scenarios we won't run into this problem, but when things get more complex we can be surprised by these "zombie" OrderLines appearing.
    * When the PERSIST operation is cascaded from the Order class to the Order.orderLines association and the containing Order object is not removed in the same transaction, we will receive an error such as "deleted entity passed to persist". Different from the "detached entity passed to persist" error we talked about in a previous blog, this error is caused by the fact that the Order object has a reference to an already deleted OrderLine object. That reference is then discovered when the JPA provider flushes the entities in the persistence context to the database, causing it try and persist the already deleted entity. And hence the error appears.

使用道具 举报

回复

您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

TOP技术积分榜 社区积分榜 徽章 团队 统计 知识索引树 积分竞拍 文本模式 帮助
  ITPUB首页 | ITPUB论坛 | 数据库技术 | 企业信息化 | 开发技术 | 微软技术 | 软件工程与项目管理 | IBM技术园地 | 行业纵向讨论 | IT招聘 | IT文档
  ChinaUnix | ChinaUnix博客 | ChinaUnix论坛
CopyRight 1999-2011 itpub.net All Right Reserved. 北京盛拓优讯信息技术有限公司版权所有 联系我们 未成年人举报专区 
京ICP备16024965号-8  北京市公安局海淀分局网监中心备案编号:11010802021510 广播电视节目制作经营许可证:编号(京)字第1149号
  
快速回复 返回顶部 返回列表