JPA with UUID
I wanted to have a data package containing only JPA annotations.
I created a base class specifying the keytype (I use UUID and Long based Entities):
package com.famvdploeg.data; import java.io.Serializable; /** * Absolute base class for persistable classes. * Contract forces get and set methods for id. * @author wytze * @param <KeyType> */ public abstract class Persistable<KeyType extends Serializable> { public abstract KeyType getId(); public abstract void setId(KeyType id); } |
package com.famvdploeg.data; import java.util.UUID; import javax.persistence.Id; import javax.persistence.MappedSuperclass; @MappedSuperclass public abstract class UuidBasedEntity extends Persistable<UUID> { @Id private UUID id; @Override public UUID getId() { return id; } @Override public void setId(UUID id) { this.id = id; } } |
Sample entity:
package com.famvdploeg.data; import java.io.Serializable; import java.util.UUID; import javax.persistence.Entity; @Entity public class Sample extends UuidBasedEntity implements Serializable { private String someExampleProperty; /* ... getters and setters */ } |
All these entities go in one separate jar.
Next I create another package using the entity jar.
package com.famvdploeg.dao; import com.famvdploeg.data.Persistable; import java.io.Serializable; public interface GenericRepository<Entity extends Persistable> { public void persist(Entity e); public Entity findById(Serializable id); public void merge(Entity t); public void remove(Serializable id); public Entity saveOrUpdate(Entity entity); } |
And a generic implementation of this dao: (Spring is used to inject the persistencecontext)
package com.famvdploeg.dao.jpa; import java.io.Serializable; import com.famvdploeg.dao.GenericRepository; import com.famvdploeg.data.Persistable; import java.lang.reflect.ParameterizedType; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.PersistenceContext; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Root; public class GenericJpaRepository<Entity extends Persistable> implements GenericRepository<Entity> { @PersistenceContext protected EntityManager em; @Override public void persist(Entity e) { em.persist(e); } @Override public Entity findById(Serializable id) { return em.find(returnEntityClass(), id); } @Override public void merge(Entity e) { em.merge(e); } @Override public void remove(Serializable id) { em.remove(findById(id)); } @Override public void flush() { em.flush(); } @Override public Entity saveOrUpdate(Entity entity) { if (entity.getId() == null) { em.persist(entity); return entity; } else { return em.merge(entity); } } protected List<Entity> findAll() { CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Entity> cq = cb.createQuery(returnEntityClass()); return em.createQuery(cq).getResultList(); } protected Entity findByProperty(String property, Object value) { try { return em.createQuery(createQueryByProperty(property, value)).getSingleResult(); } catch (NoResultException ex) { return null; } } protected List<Entity> findAllByProperty(String property, Object value) { return em.createQuery(createQueryByProperty(property, value)).getResultList(); } protected Entity findByProperties(Map<String, Object> properties) { try { return em.createQuery(createQueryByProperties(properties)).getSingleResult(); } catch (NoResultException ex) { return null; } } protected List<Entity> findAllByProperties(Map<String, Object> properties) { return em.createQuery(createQueryByProperties(properties)).getResultList(); } public Class<Entity> returnEntityClass() { ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass(); return (Class<Entity>) genericSuperclass.getActualTypeArguments()[0]; } private CriteriaQuery<Entity> createQueryByProperty(String property, Object value) { CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Entity> cq = cb.createQuery(returnEntityClass()); Root<Entity> root = cq.from(returnEntityClass()); cq = cq.where(cb.equal(root.get(property), value)); return cq; } private CriteriaQuery<Entity> createQueryByProperties(Map<String, Object> properties) { CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Entity> cq = cb.createQuery(returnEntityClass()); Root<Entity> root = cq.from(returnEntityClass()); for (Entry<String, Object> entry : properties.entrySet()) { cq = cq.where(cb.equal(root.get(entry.getKey()), entry.getValue())); } return cq; } } |
Now I want to use hibernate to map my UUID’s as PostgreSQL uuid type. We will add a mapping to do so.
(location: com/famvdploeg/data/CustomTypes.hbm.xml, place in DAO/Repository jar)
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <!-- http://docs.jboss.org/hibernate/orm/3.6/reference/en-US/html/types.html Section 6.5 covers type registry and registerTypeOverride method on the Configuration object, typedefs are the preferred way of registering custom types --> <typedef name="java.util.UUID" class="org.hibernate.type.UUIDCharType" /> <!-- I want to store clob values as text and not as materialized clobs so we override that here. Otherwise the database will show long values when using an SQL tool to query the database. --> <typedef name="materialized_clob" class="org.hibernate.type.TextType" /> <!-- Possible values: <typedef name="java.util.UUID" class="org.hibernate.type.UUIDBinaryTypee" /> <typedef name="java.util.UUID" class="org.hibernate.type.UUIDCharType" /> <typedef name="java.util.UUID" class="org.hibernate.type.PostgresUUIDType" /> --> </hibernate-mapping> |
Next we fill the persistence.xml
(location: META-INF/persistence.xml)
<?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_2_0.xsd' version="2.0"> <persistence-unit name="jpa-example-postgres" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <class>com.famvdploeg.data.Sample</class> <properties> <!--<property name="hibernate.archive.autodetection" value="class, hbm"/>--> <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQL82Dialect" /> <!-- Use this only for testing! <property name="hibernate.hbm2ddl.auto" value="create-drop" /> --> <!-- Nicer naming of tables with underscores fooName -> foo_name --> <property name="hibernate.ejb.naming_strategy" value="org.hibernate.cfg.ImprovedNamingStrategy" /> <!-- Connection properties, moved to datasource (@see applicationContext.xml) <property name="hibernate.connection.url" value="jdbc:postgresql://localhost:5432/jpa_example" /> <property name="hibernate.connection.driver_class" value="org.postgresql.Driver" /> <property name="hibernate.connection.username" value="example" /> <property name="hibernate.connection.password" value="example" /> --> <!-- c3p0 connection pooling, setup when creating datasource (@see com.mchange.v2.c3p0.ComboPooledDataSource) <property name="hibernate.c3p0.min_size">5</property> <property name="hibernate.c3p0.max_size">20</property> <property name="hibernate.c3p0.timeout">300</property> <property name="hibernate.c3p0.max_statements">50</property> <property name="hibernate.c3p0.idle_test_period">3000</property> --> </properties> <!-- The specific mapping for UUID's is added here --> <mapping-file>com/famvdploeg/data/CustomTypes.hbm.xml</mapping-file> </persistence-unit> </persistence> |
And for completeness the applicationContext.xml for Spring
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation=" http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd"> <!-- In production: <bean class="org.springframework.web.context.support.ServletContextPropertyPlaceholderConfigurer"> --> <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <!-- Test properties. Should be filled from Context.xml in production --> <property name="properties"> <props> <!-- <prop key="jpa-example-jdbc-url">jdbc:h2:mem:jpa_example</prop> <prop key="jpa-example-jdbc-username">sa</prop> <prop key="jpa-example-jdbc-password">sa</prop> <prop key="jpa-example-jdbc-driver">org.h2.Driver</prop> --> <prop key="jpa-example-jdbc-url">jdbc:postgresql://localhost:5432/jpa_example</prop> <prop key="jpa-example-jdbc-username">example</prop> <prop key="jpa-example-jdbc-password">example</prop> <prop key="jpa-example-jdbc-driver">org.postgresql.Driver</prop> </props> </property> </bean> <!-- Use pooled in production, @see com.mchange.v2.c3p0.ComboPooledDataSource --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="url" value="${jpa-example-jdbc-url}" /> <property name="username" value="${jpa-example-jdbc-username}" /> <property name="password" value="${jpa-example-jdbc-password}" /> <property name="driverClassName" value="${jpa-example-jdbc-driver}" /> </bean> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="persistenceUnitName" value="jpa-example-postgres" /> <property name="dataSource" ref="dataSource" /> </bean> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory" /> </bean> <tx:annotation-driven transaction-manager="transactionManager" /> <context:annotation-config /> </beans> |
It is also possible to store the UUID as a char(36) to do this we need to change the type in the CustomTypes.hbm.xml to org.hibernate.type.UUIDCharType.
We can then modify the mapping from the data jar through adding an orm.xml to the META-INF folder. It will normally create a varchar(255) column but that is useless. We want to specify the use of an char(36) column as UUID’s are stored as Strings of a fixed length of 36 characters.
<?xml version="1.0" encoding="UTF-8"?> <entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm orm_2_0.xsd" version="2.0"> <package>com.famvdploeg.data</package> <mapped-superclass class="UuidBasedEntity" metadata-complete="false"> <attributes> <id name="id"> <column column-definition="char(36)" /> </id> </attributes> </mapped-superclass> </entity-mappings> |
That’s it! Awesome! Clean separation of JPA and Hibernate.
Hi Wytze, first of all, I want to thank for your great contribution.
Second, I have a little problem. The method createQueryByProperties, just take only one property.
I try to solve, using a list of predicates, like that:
List predicateList = new ArrayList();
for ( Entry entry : entrySet )
{
String key = entry.getKey();
Object value = entry.getValue();
Predicate equal = cb.equal( root.get( key ), value );
predicateList.add( equal );
}
cq = cq.where( cb.and( predicateList.toArray( new Predicate[ predicateList.size() ] ) ) );
But I get this exception:
Caused by: javax.ejb.EJBException: See nested exception; nested exception is: org.apache.openjpa.persistence.ArgumentException: The specified parameter of type “class [Ljava.lang.Object;” is not a valid query parameter.
Caused by: org.apache.openjpa.persistence.ArgumentException: The specified parameter of type “class [Ljava.lang.Object;” is not a valid query parameter.
at org.apache.openjpa.jdbc.sql.DBDictionary.setUnknown(DBDictionary.java:1418)
at org.apache.openjpa.jdbc.sql.SQLBuffer.setParameters(SQLBuffer.java:644)
at org.apache.openjpa.jdbc.sql.SQLBuffer.prepareStatement(SQLBuffer.java:553)
at org.apache.openjpa.jdbc.sql.SQLBuffer.prepareStatement(SQLBuffer.java:529)
at org.apache.openjpa.jdbc.sql.SelectImpl.prepareStatement(SelectImpl.java:451)
at org.apache.openjpa.jdbc.sql.SelectImpl.execute(SelectImpl.java:392)
at com.ibm.ws.persistence.jdbc.sql.SelectImpl.execute(SelectImpl.java:80)
at org.apache.openjpa.jdbc.sql.SelectImpl.execute(SelectImpl.java:363)
at org.apache.openjpa.jdbc.sql.LogicalUnion$UnionSelect.execute(LogicalUnion.java:427)
at org.apache.openjpa.jdbc.sql.LogicalUnion.execute(LogicalUnion.java:230)
at org.apache.openjpa.jdbc.sql.LogicalUnion.execute(LogicalUnion.java:220)
at org.apache.openjpa.jdbc.kernel.SelectResultObjectProvider.open(SelectResultObjectProvider.java:94)
at org.apache.openjpa.kernel.QueryImpl$PackingResultObjectProvider.open(QueryImpl.java:2068)
at org.apache.openjpa.lib.rop.EagerResultList.(EagerResultList.java:34)
at org.apache.openjpa.kernel.QueryImpl.toResult(QueryImpl.java:1246)
at org.apache.openjpa.kernel.QueryImpl.execute(QueryImpl.java:1005)
at org.apache.openjpa.kernel.QueryImpl.execute(QueryImpl.java:861)
at org.apache.openjpa.kernel.QueryImpl.execute(QueryImpl.java:792)
at org.apache.openjpa.kernel.DelegatingQuery.execute(DelegatingQuery.java:542)
at org.apache.openjpa.persistence.QueryImpl.execute(QueryImpl.java:288)
at org.apache.openjpa.persistence.QueryImpl.getResultList(QueryImpl.java:302)
at cl.pjud.sitatp.ejb.util.dao.GenericDAO.findAllByProperties(GenericDAO.java:145)
Can you help me, please.
Greetings from Chile
Rodrigo
I found a workaround.
See comments.
public List findAllByProperties( Map properties )
{
CriteriaQuery queryByProperties = createQueryByProperties( properties );
TypedQuery query = em.createQuery( queryByProperties );
// ADDING THIS
Set keySet = properties.keySet();
for ( String key : keySet )
{
Object value = properties.get( key );
query.setParameter( key, value );
}
//
return query.getResultList();
}
private CriteriaQuery createQueryByProperties( Map properties )
{
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery cq = cb.createQuery( entityClass );
Root root = cq.from( entityClass );
Set<Entry> entrySet = properties.entrySet();
List predicateList = new ArrayList();
for ( Entry entry : entrySet )
{
String key = entry.getKey();
Object value = entry.getValue();
ParameterExpression val = cb.parameter( value.getClass(), key ); // <– Adding this line
Predicate equal = cb.equal( root.get( key ), val ); // <– And change value by val
predicateList.add( equal );
}
cq = cq.where( cb.and( predicateList.toArray( new Predicate[ predicateList.size() ] ) ) );
return cq;
}