I am trying to refactor an old application to use EJB3 with JPA.
We have two client layers (one servlet-based, one not) which both call into a delegate layer, which calls an EJB layer, which in turn calls a DAO. The EJB is EJB2 (bean-managed persistence), and the DAO uses hand-rolled SQL queries, committing transactions and closing connections manually.
I want to replace the EJB2 with EJB3, and change all the DAO to use JPA.
I started off by replacing the EJB2 code with an EJB3 using container-managed transactions. Since hibernate Criteria are so simple, and the EntityManager can be injected, I can do something like this:
@Stateless
public class NewSelfcareBean implements SelfcareTcApi {
@PersistenceContext(unitName="core")
EntityManager em;
public BasicAccount getAccount(String id) {
Criteria crit = getCriteria(BasicAccount.class);
crit.add(Restrictions.eq("id", id));
BasicAccount acc = (BasicAccount) crit.uniqueResult();
}
}
No need for a separate DAO layer. The account object looks a bit like this:
@Entity
@Table(name="er_accounts")
public class BasicAccount {
@OneToMany( mappedBy="account", fetch=FetchType.LAZY)
protected List<Subscription> subscriptions;
}
But in the servlet layer, where I call the EJB to get the account object, I want to build a response which might (or might not) include child subscriptions from the BasicAccount:
The servlet layer looks like this:
ResponseBuilder rb;
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
...
Account acc = getDelegateLayer().getAccount();
rb.buildSubscriptionResponse(acc.getSubscriptions());
...
}
Obviously this doesn't work since the transaction and entitymanager have been closed by the time we get back to the servlet layer - I get a LazyInitializationException.
So I can see a few options:
Hibernate.init(acc.getSubscriptions())
- this will work but needs to be done in the EJB. Supposing I re-use the bean for another client method which doesn't need the subscriptions? An unnecessary DB call.None of these options seems any good.
Am I missing something? How should this be done? I can't be the first person to have this problem...
Two fetching policies means two use cases, so you are better of writing two methods in this case:
public BasicAccount getAccount(String id) {
Criteria crit = getCriteria(BasicAccount.class);
crit.add(Restrictions.eq("id", id));
BasicAccount acc = (BasicAccount) crit.uniqueResult();
}
}
public BasicAccount getAccountWithSubscriptions(String id) {
Criteria crit = getCriteria(BasicAccount.class);
crit.add(Restrictions.eq("id", id));
crit.setFetchMode("subscriptions", FetchMode.JOIN);
BasicAccount acc = (BasicAccount) crit.uniqueResult();
}
}
Eager fetching is most often a code smell and it's the service layer (EJB) responsibility to fetch the data. Finding yourself hacking the web layer to add transactional responsibility is a sign of breaking application layer boundaries.
A better approach is to use DTOs. JPA entities are persistence related and they leak the database and the ORM specific fetching retrieval mechanisms. A DTO is a much better fit since it can minimize the amount of data being fetched and sent to the web layer, therefore being ideal for rendering a view. Although you can practically use entities both in the service layer and in the web layer, there are use cases when a data projection is a better alternative.
This is a VERY typical problem with EAGER/LAZY fetching.
Solution 1 (not the best): Create two methods, one getBasicAccountWithSubscription()
and one getBasicAccount()
, like Vlad said. THIS is code smell IMHO. Imagine you have 10 other such relationships. Creating such a method for each possible combination will generate 2^10 (exponential) new methods (like getBasicAccountWithSubsciptionWithoutLastLoginsWithTelephones...()
), which is not good. Of course you could generate a method with 10 boolean parameters that tell which relationship to fetch, but the method will mix in this case DB stuff (what relationship to load) with business logic parameters (ID of the BasicAccount in your case).
Solution 2 (the best IMHO): create another method in your EJB: List<Subscription> getSubscriptionsForAccount(Long accountId);
(with the property being fetched lazy). Call the new method where you need the subscriptions of an account. In order to make sure that no one calls your BasicAccount.getSubscriptions()
method, you could make it private. If you would have 10 relationships you would create a new method for every relationship (thus liniar number of new methods). E.g if you would have another relationship private List<Login> logins;
in your BasicAccount
entity, you would add another method to the EJB service public List<Login> getLoginsForAccount(Long accountId)
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With