The idea is to create a hierarchy of classes that all domain objects can derive from. These classes are the single point where we implement the following:
- Methods for Collections: we need all domain objects to be comparable, hashable and to equal on ID (primary key)
- Primary key: either auto-numbering or GUID
- Optimistic concurrency
What could be added if needed:
- Timestamp
- Auditing: the user id of last update
All domain objects can extend these; we could also benefit from all domain objects following the same pattern (e.g. having a "getId()" method), when implementing the generic DAOs.
If we add entity classes, and configure the project as suggested below, then, at this stage, we would achieve the following:
- The database schema will be generated by Hibernate using the entity classes that extend the generic domain object classes suggested here; the configuration suggested below actually provides for dropping and the tables and re-generating them each time the build runs.
- Entity manager and transaction manager will be configured by Spring and ready for the next stage -- implementing the Data Access Objects
- The unit tests can be implemented with the assumption that the database schema is up to date -- i.e. following the entity classes -- for every build (see "execution" clause in Hibernate plug-in configuration suggested below)
Libraries and Versions
... as expressed by Maven dependencies:
.... <dependency> <groupid>org.springframework</groupid> <artifactid>spring</artifactid> <version>2.5.6</version> </dependency> <dependency> <groupid>org.springframework</groupid> <artifactid>spring-core</artifactid> <version>2.5.6</version> </dependency> <dependency> <groupid>org.springframework</groupid> <artifactid>spring-context</artifactid> <version>2.5.6</version> </dependency> <dependency> <groupid>org.springframework</groupid> <artifactid>spring-test</artifactid> <version>2.5.6</version> </dependency> <dependency> <groupid>org.springframework</groupid> <artifactid>spring-web</artifactid> <version>2.5.6</version> </dependency> ... <dependency> <groupid>javax.persistence</groupid> <artifactid>persistence-api</artifactid> <version>1.0</version> </dependency> <dependency> <groupid>org.hibernate</groupid> <artifactid>hibernate-entitymanager</artifactid> <version>3.3.2.GA</version> </dependency> <dependency> <groupid>javax.transaction</groupid> <artifactid>jta</artifactid> <version>1.1</version> </dependency> <dependency> <groupid>${jdbc.groupId}</groupid> <artifactid>${jdbc.artifactId}</artifactid> <version>${jdbc.version}</version> </dependency> ...
Using Hibernate Maven Plugin to generate the schema
Let's start with the "build" section of the POM, where we define how the resources get filtered (in Maven parlance, this means resolving variables in them using property values from the POM):
... <resources> <resource> <directory>${project.basedir}/src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <testresources> <testresource> <directory>${project.basedir}/src/test/resources</directory> <filtering>true</filtering> </testresource> </testresources>
Where the resources we are filtering as shown above are used? It is mostly in the jdbc.properties file quoted below. This is the file that is referenced from the Hibernate section of the POM, and also in the Spring configuration quoted below. This way we are making sure that all the database properties are specified in the POM.
The reason we are choosing POM as the single place of specifying the parameters for the project is because Maven has the ability to implement logic switching sets of values depending on the environment where the build is performed (test, development, QA, production). The other possible places (.properties files, Spring configuration) lack this flexibility.
The idea was borrowed from this Maven archetype
# # These properties are used at build time and run-time; # they define the DB type and location; POM defines several # sets of values for different environments (test, QA, ...) # jdbc.driverClassName=${jdbc.driverClassName} jdbc.url=${jdbc.url} jdbc.username=${jdbc.username} jdbc.password=${jdbc.password} jpa.database=${jpa.database} jpa.showSql=${jpa.showSql} # Needed by Hibernate3 Maven Plugin defined in pom.xml hibernate.dialect=${hibernate.dialect} hibernate.connection.username=${jdbc.username} hibernate.connection.password=${jdbc.password} hibernate.connection.url=${jdbc.url} hibernate.connection.driver_class=${jdbc.driverClassName}
The property values that will be used at build time to resolve variables in the configuration file quoted above are specified in the POM -- in the "profiles" section -- as follows:
<properties> <dbunit.dialect>org.dbunit.ext.mysql.MySqlDataTypeFactory</dbunit.dialect> <hibernate.dialect>org.hibernate.dialect.MySQLDialect</hibernate.dialect> <jdbc.groupid>mysql</jdbc.groupid> <jdbc.artifactid>mysql-connector-java</jdbc.artifactid> <jdbc.version>5.1.6</jdbc.version> <jdbc.driverclassname>com.mysql.jdbc.Driver</jdbc.driverclassname> <jdbc.username>example</jdbc.username> <jdbc.password> </jdbc.password> <jpa.database>MYSQL</jpa.database> <jpa.showsql>true</jpa.showsql> </properties>
Hibernate plug-in configuration references the jdbc.properties file that is getting receiving the values of properties specified in the POM as described above:
... <plugin> <groupid>org.codehaus.mojo</groupid> <artifactid>hibernate3-maven-plugin</artifactid> <version>2.2</version> <executions> <execution> <phase>process-classes</phase> <goals> <goal>hbm2ddl</goal> </goals> </execution> </executions> <configuration> <components> <component> <name>hbm2ddl</name> <implementation>jpaconfiguration</implementation> </component> </components> <componentproperties> <persistenceunit>example</persistenceunit> <outputfilename>schema.ddl</outputfilename> <propertyfile>target/classes/jdbc.properties</propertyfile> <drop>true</drop> <create>true</create> <export>true</export> <update>false</update> <format>true</format> </componentproperties> </configuration> <dependencies> <dependency> <groupid>${jdbc.groupId}</groupid> <artifactid>${jdbc.artifactId}</artifactid> <version>${jdbc.version}</version> </dependency> </dependencies> </plugin> ...
And persistence.xml is nearly empty (all mappings are handled by Hibernate, and entity manager is supplied by Spring):
<persistence version="1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/persistence" xsi:schemalocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"> <persistence-unit name="example" transaction-type="RESOURCE_LOCAL"> </persistence-unit> </persistence>
Finally, what binds together JPA and Hibernate is the Spring container; note the database parameters that reference the properties file:
<beans xmlns:context="http://www.springframework.org/schema/context" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemalocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd "> <context:property-placeholder location="classpath:/jdbc.properties"> </context:property-placeholder> <context:annotation-config> </context:annotation-config> <tx:annotation-driven> </tx:annotation-driven> <jee:jndi-lookup id="dataSource" jndi-name="jdbc/Database"> </jee:jndi-lookup> <bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" id="entityManagerFactory" p:datasource-ref="dataSource" p:jpavendoradapter-ref="jpaAdapter"> <property name="loadTimeWeaver"> <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"> </bean> </property> <bean class="org.springframework.orm.jpa.JpaTransactionManager" id="transactionManager" p:entitymanagerfactory-ref="entityManagerFactory"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" id="jpaAdapter" p:database="${jpa.database}" p:showsql="${jpa.showSql}"> </bean> </bean> </bean> </beans>
Root class of reusable domain object hierarchy:
Implements comparison, equality and hash code, the primary key is left for the subclasses:
package com.example.domain; import java.io.Serializable; import javax.persistence.MappedSuperclass; import javax.persistence.PostLoad; import javax.persistence.PostPersist; import javax.persistence.PrePersist; import javax.persistence.Transient; import javax.persistence.Version; import org.apache.commons.lang.ObjectUtils; /** * Root class for all entities; implements comparison and what goes with it. * Also has a "version" field for optimistic concurrency checking. * * @author maxim * * @param <PK> primary key type * * @see AutoNumberedDomainObject * @see SurrogateIdDomainObject */ @MappedSuperclass abstract public class DomainObject<PK> implements Serializable, Comparable<DomainObject<PK>>, Cloneable { @Transient private boolean justCreated=true; @PostPersist @PostLoad public void saved(){ justCreated=false; } static private long serialVersionUID = 1L; @Version protected int version; @Override public String toString() { return "DomainObject ("+this.getClass().getSimpleName()+")"; } @SuppressWarnings("unchecked") @Override public boolean equals(Object o) { if (o == this) return true; if (o == null) return false; return (o.getClass().isAssignableFrom(this.getClass())) && (this.getClass().isAssignableFrom(o.getClass())) && ObjectUtils.equals(((DomainObject<PK>) o).getId(), getId()); } @Override public int hashCode() { if (getId() == null) return 0; return getId().hashCode(); } /** * A simple stable ordering based on Id */ @SuppressWarnings("unchecked") public int compareTo(DomainObject<PK> o) { if (getId() == null) return -1; if (o.getId() == null) return 1; return ((Comparable<PK>) getId()).compareTo(o.getId()); } @Override public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } /** * * @return lable to display in selection lists; override in subclasses to produce something better */ public String getSelectionLabel(){ return getId().toString(); } /** * @param id * the id to set */ public abstract void setId(PK id); /** * @return the id */ public abstract PK getId(); public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } public void setJustCreated(boolean notSaved) { this.justCreated = notSaved; } public boolean isJustCreated() { return justCreated; } }
Domain object with auto-numbered ID
This class extends DomainObject to add JPA auto-numbered ID fields.
package com.example.domain; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.MappedSuperclass; /** * Extends {@link DomainObject} to support auto-numbered IDs via JPA * {@link GeneratedValue} with {@link GenerationType} = * {@link GenerationType#TABLE} * * Does <b>not</b> force a unique constraint on the ID: add this in subclasses * if performance hit is accessible. * * @param PK * the primary key class for auto-numbering; the choice is usually * between Long and Integer * * @author maxim * */ @MappedSuperclass public abstract class AutoNumberedDomainObject<PK extends Number> extends DomainObject<PK> { private static long serialVersionUID = 1L; @Override public String toString() { return "AutoNumberedDomainObject ("+this.getClass().getSimpleName()+") [id=" + id + "]"; } @Id @GeneratedValue(strategy = GenerationType.TABLE) protected PK id; @Override public PK getId() { return this.id; } @Override public void setId(PK id) { this.id = id; } }
Domain object with Surrogate Id
Another (and, arguably, more usable) flavour of DomainObject generates surrogate primary keys:
package com.example.domain; import java.util.UUID; import javax.persistence.Column; import javax.persistence.Id; import javax.persistence.MappedSuperclass; /** * DomainObject that has a surrogate ID calculated as a time-based GUID. * * Does <b>NOT</b> force a unique constraint on the ID: add this in subclasses * if performance hit is acceptable. * * @author maxim * * @see UUID * */ @MappedSuperclass public class SurrogateIdDomainObject extends DomainObject<String> { @Override public String toString() { return "SurrogateIdDomainObject ("+this.getClass().getSimpleName()+") [id=" + id + "]"; } /** * Default constructor; generates new ID */ public SurrogateIdDomainObject() { super(); this.id = newId(); } /** * * @param generateId specify false to <b>not</b> generate the ID */ public SurrogateIdDomainObject(boolean generateId) { super(); if (generateId) { this.id = newId(); } } private static final long serialVersionUID = 1L; /** * "e5d302ed-0501-4a3e-abfc-a336124fe2b5" */ @Id @Column(name = "ID",length=36) private String id; /** * Creates new and assigns a surrogate ID */ protected void initializeAsNew() { setId(newId()); } public void setId(String id) { this.id = id; } public String getId() { return id; } /** * * @return new UUID-based ID, e.g. "e5d302ed-0501-4a3e-abfc-a336124fe2b5" * @see UUID */ public static String newId() { return UUID.randomUUID().toString(); } }
Using the generic domain object classes
Having defined all of the above, we can benefit from it defining new domain objects as follows:
... @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) @Entity public class User extends SurrogateIdDomainObject { private String name; ...
The XML (actually, JAXB) annotations will come into play when we will be passing this object to and from Web Services.
No comments:
Post a Comment