Thursday, July 11, 2013

Using GWT and Hibernate (JPA)

This article discusses an option to use Hibernate and JPA entities directly in a GWT application. It uses a small project JpaCloner which can be found on the github https://github.com/nociar/jpa-cloner. The Google Web Toolkit  framework itself declares support for JPA entities. It means that the GWT is able to:
  • Translate JPA entities to Javascript.
  • Serialize JPA entities in GWT-RPC calls.
So there should be no problem to use your JPA domain model direcly in the client code (as JPA entities are translatable). But just try to return a JPA entity from your GWT-RPC calls. The call will end with a SerializationException. There are two problems related to the serialization of Hibernate entities by GWT. Both problems are discussed in following paragraphs.

Javassist proxies

The first probem of the serialization is caused by the type of Hibernate entities. Hibernate uses javassist proxy classes which are not known to the GWT during the compilation time. GWT is also not able to serialize lazy collections implemented by Hibernate. So the declared support of JPA by GWT seems to be useless when using Hibernate. But the situation is not as bad as it seems. :-) The thing to be done is very simple - just replace the hibernate proxies by raw JPA classes and lazy collection by standard java.util collections. The good news is that you are not forced to do it manually. You can use a small project JpaCloner for all of this, the usage is extremely simple, for example:

/** Return an instance of Company by ID. */
public void Company getCompany(long id) {
    Company entity = entityDAO.getEntity(id);
    return JpaCloner.clone(entity);
}

Please note that the JpaCloner creates a cloned entity of the raw JPA class - this means that you can easily get rid of Hibernate proxy classes.

Serialization depth

The second problem of the serialization is related to ORM technologies in general. There must be somehow defined an object graph which should be serialized. GWT-RPC serializes all properties of a returned object . In case of bidirectional JPA mappings it would easily serialize the whole DB which is of course not acceptable. Only a subset of neighboring entities should be serialized (entity subgraph). This can be again accomplished by the JpaCloner's clone() methods. Additional parameters can specify property paths and the depth of cloning. The following example shows how to serialize a company with a hierarchical department structure:

/** 
 * Return an instance of Company by ID. The instance 
 * will contain also all departments and employees.
 */
public void Company getCompanyWithDepartments(long id) {
    Company entity = entityDAO.getEntity(id);
    return JpaCloner.clone(entity, "department+.employees");
}

Please note that the cloning should be done in a transaction scope to prevent lazy initialization exception thrown by Hibernate. 

Conclusion

So what to say at the end? Just that the usage of JPA entities in your GWT projects can be easy and straightforward. There is no need to use sophisticated frameworks, nor to use redundant DTOs and Dozer which mostly mirror related JPA entities. The JpaCloner does not have any dependencies and is completely non-invasive. This is not true for projects like hibernate4gwt (now Gilead) which forces you to extend your domain model entities from the LightEntity class.

Happy coding :-)

15 comments:

  1. Hello,
    I have 2 hibernate entities (User and Domain) mapped in a M:N relationship, with bi directionality. I also need a extra attribute (status) in the relationship class "UserProfile" witch is mapped with @Embeddable and @EmbeddedId. Can JpaCloner help me? I am using "userDomains.domain" but GWT is complaining about a lazy exception with "Domain.userDomains". Thank you.

    ReplyDelete
  2. I mean, I ma using JpaCloner.clone(user, "userDomains.domain") and getting the exception I wrote above. tks

    ReplyDelete
  3. Hello,
    the JpaCloner supports @Entity and @Embeddable objects, just specify them in the patterns like:
    return JpaCloner.clone(user, "userDomains.domain", "userDomains.userProfile");

    After the cloinng the lazy exceptions cannot be thrown by GWT-RPC serialization (as the cloned entities are not proxy classes). But please note that the cloning should be done in a *transaction scope*! Can you check this?

    ReplyDelete
    Replies
    1. Actually I believe that the cloning process is going ok (I can see the hibernate selects in the log). My method ends like:

      User clonedUser = JpaCloner.clone(user, "userDomains.domain");

      em.getTransaction().commit();
      em.close();

      return clonedUser ;

      and then I get the exception below:
      15:52:10,466 ERROR [org.apache.catalina.core.ContainerBase.[jboss.web].[default-host].[/nethandle]] (http--127.0.0.1-8080-2) Exception while dispatching incoming RPC call: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.webradar.nethandle.model.profile.Domain.userDomains, no session or session was closed
      at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:393) [hibernate-core-4.0.1.Final.jar:4.0.1.Final]
      at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(AbstractPersistentCollection.java:385) [hibernate-core-4.0.1.Final.jar:4.0.1.Final]
      at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:378) [hibernate-core-4.0.1.Final.jar:4.0.1.Final]
      at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:112) [hibernate-core-4.0.1.Final.jar:4.0.1.Final]
      at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:500) [hibernate-core-4.0.1.Final.jar:4.0.1.Final]
      at java.lang.String.valueOf(String.java:2854) [rt.jar:1.7.0_21]
      at java.lang.StringBuilder.append(StringBuilder.java:128) [rt.jar:1.7.0_21]
      at com.google.gwt.user.server.rpc.impl.ServerSerializationStreamWriter.serialize(ServerSerializationStreamWriter.java:667) [gwt-servlet-2.5.1.jar:]
      at com.google.gwt.user.client.rpc.impl.AbstractSerializationStreamWriter.writeObject(AbstractSerializationStreamWriter.java:126) [gwt-servlet-2.5.1.jar:]
      at


      and so on...

      Delete
  4. Hmm, this sounds strange. It seems that the Domain entity is not cloned, but just copied. But this cannot happen if the class is annotated by @Entity or @Embedded. Could you try to check? If not, please paste the mapped classes of User, UserDomain and Domain (all classes involved in cloning). thx

    ReplyDelete
    Replies
    1. Here we go. Thanks again for your attention. I´m trying to get rid of Gilead.
      I followed the instructions in http://www.mkyong.com/hibernate/hibernate-many-to-many-example-join-table-extra-column-annotation/ to make may M:N relationship with an extre column

      //Domain
      @SuppressWarnings("serial")
      @Entity
      @Table(name="Domain")
      @Access(AccessType.FIELD)
      public class Domain implements Serializable {

      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;

      @Column(name="name")
      private String name;


      @OneToMany(fetch = FetchType.LAZY, mappedBy = "pk.domain", cascade=CascadeType.ALL)
      private List userDomains = new ArrayList(0);


      public Long getId() {
      return id;
      }


      public void setId(Long id) {
      this.id = id;
      }


      public String getName() {
      return name;
      }


      public void setName(String name) {
      this.name = name;
      }


      public List getUserDomains() {
      return userDomains;
      }


      public void setUserDomains(List userDomains) {
      this.userDomains = userDomains;
      }

      }


      //User
      @SuppressWarnings("serial")
      @Entity
      @Table(name="Users", uniqueConstraints=@UniqueConstraint(columnNames={"login"}))
      @Access(AccessType.FIELD)
      public class User implements Serializable {

      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;

      private String login;

      private String name;

      private String email;

      private String encripPassword;

      @Temporal(TemporalType.TIMESTAMP)
      private Date lastLogin;

      private int loginsQty;


      @OneToMany(fetch = FetchType.LAZY, mappedBy = "pk.user")
      private List userDomains = new ArrayList(0);


      public User() {}

      public User(Long id, String name, String email) {
      this.id = id;
      this.name = name;
      this.email = email;
      }

      public Long getId() {
      return id;
      }

      public void setId(Long id) {
      this.id = id;
      }

      public String getName() {
      return name;
      }

      public void setName(String name) {
      this.name = name;
      }

      public String getEmail() {
      return email;
      }

      public void setEmail(String email) {
      this.email = email;
      }



      public String getLogin() {
      return login;
      }

      public void setLogin(String login) {
      this.login = login;
      }

      public Date getLastLogin() {
      return lastLogin;
      }

      public void setLastLogin(Date lastLogin) {
      this.lastLogin = lastLogin;
      }

      public int getLoginsQty() {
      return loginsQty;
      }

      public void setLoginsQty(int loginsQty) {
      this.loginsQty = loginsQty;
      }

      public String getEncripPassword() {
      return encripPassword;
      }

      public void setEncripPassword(String encripPassword) {
      this.encripPassword = encripPassword;
      }

      public List getUserDomains() {
      return userDomains;
      }

      public void setUserDomains(List userDomains) {
      this.userDomains = userDomains;
      }

      }

      Delete
    2. //UserDomain
      @SuppressWarnings("serial")
      @Entity
      @Table(name = "Users_X_Domain")
      @AssociationOverrides({
      @AssociationOverride(name = "pk.user", joinColumns = @JoinColumn(name = "user_id")),
      @AssociationOverride(name = "pk.domain", joinColumns = @JoinColumn(name = "domain_id")) })
      @Access(AccessType.FIELD)
      public class UserDomain implements Serializable {

      @EmbeddedId
      private UserDomainPk pk = new UserDomainPk();

      //additional attribute!!
      @Type(type="com.webradar.nethandle.model.profile.enums.EnumUserDomainUserType")
      @Column(name="type_id")
      private EnumUserDomain enumUserDomain;


      //helper method
      public User getUser() {
      return getPk().getUser();
      }

      public void setUser(User user) {
      getPk().setUser(user);
      }

      //helper method
      public Domain getDomain() {
      return getPk().getDomain();
      }

      public void setDomain(Domain domain) {
      getPk().setDomain(domain);
      }


      public boolean equals(Object o) {
      if (this == o)
      return true;
      if (o == null || getClass() != o.getClass())
      return false;

      UserDomain that = (UserDomain) o;

      if (getPk() != null ? !getPk().equals(that.getPk())
      : that.getPk() != null)
      return false;

      return true;
      }

      public int hashCode() {
      return (getPk() != null ? getPk().hashCode() : 0);
      }


      public UserDomainPk getPk() {
      return pk;
      }


      public void setPk(UserDomainPk pk) {
      this.pk = pk;
      }


      public EnumUserDomain getEnumUserDomain() {
      return enumUserDomain;
      }


      public void setEnumUserDomain(EnumUserDomain enumUserDomain) {
      this.enumUserDomain = enumUserDomain;
      }

      }

      //UserDomainPk
      @SuppressWarnings("serial")
      @Embeddable
      @Access(AccessType.FIELD)
      public class UserDomainPk implements Serializable {

      @ManyToOne
      private User user;

      @ManyToOne
      private Domain domain;


      public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      UserDomainPk that = (UserDomainPk) o;

      if (user != null ? !user.equals(that.user) : that.user != null) return false;
      if (domain != null ? !domain.equals(that.domain) : that.domain != null) return false;

      return true;
      }

      public int hashCode() {
      int result;
      result = (user != null ? user.hashCode() : 0);
      result = 31 * result + (domain != null ? domain.hashCode() : 0);
      return result;
      }

      public User getUser() {
      return user;
      }

      public void setUser(User user) {
      this.user = user;
      }

      public Domain getDomain() {
      return domain;
      }

      public void setDomain(Domain domain) {
      this.domain = domain;
      }

      }

      //persistence.xml : relevant part
      com.mypackage.User
      com.mypackage.Domain
      com.mypackage.UserDomain
      com.mypackage.UserDomainPk

      Delete
    3. // CLIENT METHOD
      public User tryLogin(String userName, String userPasswd) {

      try {

      EntityManager em = factory.createEntityManager();
      em.getTransaction().begin();
      User user = em.createQuery( "from User u where u.login = :asd", User.class ).setParameter("asd", userName).getSingleResult();

      if (user==null) {

      System.out.println("login:" + userName + " nao encontrado");
      return null;

      } else {

      System.out.println("id:"+user.getId()+" email:"+user.getEmail());

      MessageDigest md = MessageDigest.getInstance("MD5");
      md.update(userPasswd.getBytes());
      BigInteger hash = new BigInteger(1, md.digest());
      String userEncripPasswd = hash.toString(16);


      if(!user.getEncripPassword().equals(userEncripPasswd)){
      System.out.println("password does not match");
      return null;
      }

      user.setLastLogin(new Date());
      user.setLoginsQty(user.getLoginsQty()+1);

      //options i´ve tryed
      //JpaCloner.clone(user, "userDomains.pk.domain.attrs");
      //JpaCloner.clone(user, "userDomains.(domain).attrs");
      //JpaCloner.clone(user, "userDomain*.domain.attrs");
      //JpaCloner.clone(user, "userDomains.(domain$).attrs");
      //JpaCloner.clone(user, "userDomains.domain.attr*");
      //JpaCloner.clone(user, "userDomains.domain.attrs.columnName");
      //JpaCloner.clone(user, "userDomains");
      //JpaCloner.clone(user, "userDomains.domain.attrs");
      //JpaCloner.clone(user, "userDomains.domain.attrs.columnName");
      //JpaCloner.clone(user, "userDomains.attrs");
      //JpaCloner.clone(user, "userDomains.domain");


      User clonedUser = JpaCloner.clone(user, "userDomains");

      em.getTransaction().commit();
      em.close()

      return clonedUser;

      }


      } catch (Exception e) {

      System.out.println(ExceptionUtils.getStackTrace(e));
      return null;
      }



      }

      Delete
  5. Hi,
    I think that you have discovered a JPA cloner bug :-) The cloning do not correctly handle @EmbeddedId annotation, only the @Embedded annotation. Please create an issue on https://github.com/nociar/jpa-cloner. I will try to fix it ASAP.

    btw: the correct cloning pattern for your case is "userDomains.domain" (will be correctly handled when after the fix)

    ReplyDelete
    Replies
    1. That´s good. Just did. Let´s make it better!! Thanks again.

      Delete
  6. Hallo,
    how works JPA Cloner in the direction Client->Server. for example to save a entity

    ReplyDelete
    Replies
    1. Hello, JPA Cloner can be used only in a server side code. Please note that the project heavily uses Java Reflection Framework. JPA Cloner allows to easily transport JPA entities from server side to GWT client side. Once you have a JPA entity on the client, you can modify it and send to an RPC procedure like:

      class MyServiceImpl extends RemoteService implements MyService {
      ....public void saveEntity(MyEntity entity) {
      ........// TODO save the entity in DB
      ....}
      ....public MyEntity getEntity(Long id) {
      ........MyEntity entity = dao.get(id);
      ........return JpaCloner.clone(entity, "some.properties");
      ....}

      }

      That's all :-)

      Delete
  7. This comment has been removed by the author.

    ReplyDelete
  8. please recommend a way to restore from cloned object original JPA enity,
    so we can save it, to update DB record.
    Problem sems to be in nulls on not cloned properties which will saved if to apply persist for cloned object.

    As well, is there way to restory hierancy of original oject with updated changes from cloned objects ?

    thanks


    ReplyDelete
    Replies
    1. Hello Vladimir,
      the question asked is for sure relevant, but I am afraid that there is no easy answer. The process of modification of a JPA record in a GWT application is:
      1) get a cloned entity (with some cloned relation properties)
      2) modify the cloned entity on the client side
      3) merge the modified entity on the server side

      The merging of the modified entity can be sometimes easy like calling the EntityManager#merge(T entity). But the problem is if the entity contains some relations with the cascade property. For example a company with a collection of employees and partners (or many more):

      @Entity
      public class Company
      ..@OneToMany(cascade=ALL)
      ..private List employees;
      ..@OneToMany(cascade=ALL)
      ..private List partners;
      }

      If you want to update just a company record, you can implement your service like this:

      public MyServiceImpl implements MyService {
      ..EntityManager em;

      ..Company getById(long id) {
      ....Company company = em.find(Company.class, id);
      ....return JpaCloner.clone(company); // employees and partners are left as null
      ..}

      ..long save(Company company) {
      ....Company merged;
      ....if (company.getId() == null) {
      ......company.setEmployees(new ArrayList()); // set empty list
      ......company.setPartners(new ArrayList()); // set empty list
      ......em.persist(company);
      ......merged = company;
      ....} else {
      ......// restore the employees from the DB
      ......Company original = em.find(Company.class, company.getId());
      ......company.setEmployees(original.getEmployees());
      ......company.setPartners(original.getPartners());
      ......merged = em.merge(company);
      ....}
      ....em.flush();
      ....return merged.getId();
      ..}
      }

      But now I see that this approach is not very friendly for progragrammers. It could be better to have a new method in the JpaCloner class like:
      public static void copy(T entity1, T entity2) {...}

      This method will copy all basic properties (i.e. columns) from entity1 to entity2. This method could be used for merging of basic properties from a modified detached JPA entity to an attached JPA entity. The save method can be rewritten like:

      ..long save(Company company) {
      ....Company merged;
      ....if (company.getId() == null) {
      ......company.setEmployees(new ArrayList()); // set empty list
      ......em.persist(company);
      ......merged = company;
      ....} else {
      ......Company original = em.find(Company.class, company.getId());
      ......JpaCloner.copy(company, original); // copy just basic properties, not relations
      ......merged = original;
      ....}
      ....em.flush();
      ....return merged.getId();
      ..}


      What is your opinion Vladimir?

      Delete