Skip to content

6. Spring Data JPA Hibernate

6.1. Introduction

We will use the [dbproduitscategories] database managed by the [spring-jdbc-04] project and implement the two interfaces [IDao<Category>, IDao<Product>] defined in that project. This will allow us to do several things:

  • compare the implementation code;
  • use the same test layer;
  • compare the performance of the two implementations;
  • the [JDBC] layer is implemented by the [mysql-config-jdbc] project discussed in Section 3.3;

We will now move on to the other layers.

6.2. Setting up the working environment

Using STS, import the [mysql-config-jpa-hibernate] project [1] located in the [<examples>/spring-database-config/mysql/eclipse] folder [2]:

This project configures the [Spring JPA Hibernate] layer of the project. Each JPA implementation has its own configuration project.

Next, import the [spring-jpa-generic] [1] project located in the [<examples>/spring-database-generic/spring-jpa] [2] folder:

Once this is done, refresh the Maven environment (Alt-F5) for all projects in [Package Explorer]:

 

Then, to verify the working environment, run the build configuration named [spring-jpa-generic-JUnitTestDao-hibernate]:

This configuration runs the [JUnitTestDao] test. This test should pass:

  

6.3. The JPA layer configuration project

  

The purpose of this project is to configure the JPA layer of the architecture shown below:

6.3.1. Maven Configuration

The project is a Maven project configured by the following [pom.xml] file:


<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
    xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>generic-config-jpa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mysql openjpa configuration</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- variable dependencies ********************************************** -->
        <!-- JPA provider -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- constant dependencies ********************************************** -->
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Spring Context -->
        <!-- Legacy JDBC configuration -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-jdbc</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • lines 5-7: the Maven artifact generated by this project. The configuration projects for the other JPA implementations (Eclipselink and OpenJpa) will use this same artifact. This means that only one of these projects can be active at any given time. You should therefore avoid having them all present in [Package Explorer]. Only one is needed;
  • lines 10–14: the parent Maven project that specifies the versions of most of the dependencies required by the project;
  • lines 19–22: the Hibernate library;
  • lines 25–28: the Spring Data library;
  • lines 32–34: the JPA layer configuration project relies on the JDBC layer configuration project, which defines, among other things, the JDBC driver for the DBMS being used and the database connection details;
  • lines 35–39: the JDBC layer configuration project includes the [Spring JDBC] library, which is replaced here by the [Spring Data JPA] library. Therefore, we specify not to include it in the project dependencies. However, if it remains, this does not cause any errors;

In the end, the project dependencies are as follows:

  

6.3.2. Spring Configuration

 

The [ConfigJpa] class configures the Spring project:


package generic.jpa.config;

import javax.persistence.EntityManagerFactory;

import generic.jdbc.config.ConfigJdbc;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@Import({ ConfigJdbc.class })
public class ConfigJpa {

    // the JPA provider
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        return hibernateJpaVendorAdapter;
    }

    // JPA entity packages
    public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };

    // data source
    @Bean
    public DataSource dataSource() {
        // TomcatJdbc data source
        DataSource dataSource = new DataSource();
        // JDBC access configuration
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
        // Initial open connections
        dataSource.setInitialSize(5);
        // result
        return dataSource;
    }

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(ENTITIES_PACKAGES);
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
  • line 18: the class is a Spring configuration class;
  • line 19: it imports the beans defined by the [ConfigJdbc] configuration class used to configure the Spring project [mysql-config-jdbc]. These are the JSON filters;
  • lines 23–30: define the JPA implementation used, in this case the Hibernate implementation (line 25);
  • line 26: you can choose whether or not to display the SQL operations executed by the Hibernate implementation;
  • line 27: specifies the connected DBMS to Hibernate. This configuration is important. It allows Hibernate to use the SQL dialect of the MySQL DBMS, including its proprietary features. Furthermore, this informs it about the SQL types and DBMS objects it will be able to use. It is this ability of the JPA implementation to adapt to a specific DBMS that gives it high portability across DBMSs;
  • line 28: Hibernate may or may not generate the tables for the target database from the JPA entities it finds. This generation only occurs if the tables are absent. If they already exist, nothing is done. We will use this ability to generate tables when we demonstrate how the SQL generation scripts for the various databases used in this document were created;
  • line 33: the package containing the JPA entities for the [dbproduitscategories] database;
  • Lines 36–49: The data source [tomcat-jdbc] linked to the database [dbproduitscategories];
  • lines 52–60: the bean named [entityManagerFactory] (it must be named this way) is the bean that will create the [EntityManager] object, which manages the JPA persistence context. All JPA operations go through it. Because we are using [Spring Data JPA], we will never use this object ourselves. However, we do need to configure it. It needs to know the following:
    • the JPA implementation used (line 55);
    • the data source used (line 57);
    • the JPA entities for this source (line 56);
  • line 58: initializes the EntityManager with this information;
  • line 59: returns the singleton [entityManagerFactory];
  • lines 63–68: define the transaction manager. It must be named [transactionManager];
  • line 65: a JPA transaction manager is created;
  • line 66: it is connected to the data source from line 37 via the [entityManagerFactory] bean (lines 53 and 57);

Only the bean in lines 23–30 depends on the JPA implementation used. The other beans then rely on it.

6.3.3. Entities in the [JPA] layer

  

The target database is the [dbproduitscategories] database with its two tables [CATEGORIES] and [PRODUITS]. We have seen that it also has three other tables [USERS, ROLES, USERS_ROLES] that will be used to secure the web service to be deployed on the web. We will ignore these tables for now. As a reminder, here is the structure of the [CATEGORIES] and [PRODUCTS] tables:

The [PRODUCTS] table is as follows:

  • [ID]: the auto-incrementing primary key of the table [2];
  • [NAME]: the unique name of the product [4];
  • [PRICE]: the product’s price;
  • [DESCRIPTION]: the product description;
  • [VERSIONING] is the product’s version number. Its initial version is 1 [3]. Each time the product is modified, its version number is incremented by the code that operates the table;
  • [CATEGORY_ID]: the foreign key in the [CATEGORIES] table to identify the category to which the product belongs;
  • in [1-3], the foreign key [CATEGORIE_ID] of the [PRODUITS] table. It references the [ID] column of the [CATEGORIES] table [4-5];
  • when a category is deleted, all products linked to it are also deleted [6]. This point is important to note because it is used in the construction of the [DAO] layer that uses the [dbproduitscategories] database;

The [CATEGORIES] table is as follows:

  • [ID]: auto-incrementing primary key;
  • [VERSIONING]: category version number;
  • [NAME]: unique name of the category;

We will now describe the JPA entities [Product] and [Category], which correspond to the [PRODUCTS] and [CATEGORIES] tables.

  

6.3.3.1. The [AbstractCoreEntity] interface

The [AbstractCoreEntity] interface is implemented by the JPA entities [Category] and [Product]:


package generic.jpa.entities.dbproduitscategories;

public interface AbstractCoreEntity {

    // getters and setters for the [id], [version], and [entityType] fields
    public Long getId();

    public void setId(Long id);

    public Long getVersion();

    public void setVersion(Long version);

    public enum EntityType {
        PROXY, POJO
    }

    public EntityType getEntityType();

    public void setEntityType(EntityType entityType);

}

This interface, implemented by the two JPA entities, simply lists the methods for reading and writing the [id], [version], and [entityType] fields of these entities. The role of the [entityType] field will be explained later;

6.3.3.2. The JPA entity [Product]

The [Product] class is the JPA entity associated with a row in the [PRODUCTS] table:

 

package generic.jpa.entities.dbproduitscategories;

import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;

import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
@Table(name = ConfigJdbc.TAB_PRODUCTS)
@JsonFilter("jsonFilterProduct")
public class Product implements AbstractCoreEntity {
    // properties
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;

    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;

    @Transient
    protected EntityType entityType = EntityType.POJO;

    @Transient
    @JsonIgnore
    protected String simpleClassName = getClass().getSimpleName();

    // properties
    @Column(name = ConfigJdbc.TAB_PRODUITS_NOM, unique = true, length = 30, nullable = false)
    private String name;

    @Column(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, insertable = false, updatable = false, nullable = false)
    private Long categoryId;

    @Column(name = ConfigJdbc.TAB_PRODUITS_PRIX, nullable = false)
    private double price;

    @Column(name = ConfigJdbc.TAB_PRODUITS_DESCRIPTION, length = 100)
    private String description;

    // the category
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
    private Category category;

    // constructors
    public Product() {

    }

    public Product(Long id, Long version, String name, Long categoryId, double price, String description,
            Category category) {
        this.id = id;
        this.version = version;
        this.name = name;
        this.categoryId = categoryId;
        this.price = price;
        this.description = description;
        this.category = category;
    }

    // signature
    public String toString() {
        return String.format("[id=%s, version=%s, name=%s, price=10.2f, desc=%s, categoryId=%s]", id, version, name, price,
                description, categoryId);
    }

    // ------------------------------------------------------------
    // Override [equals] and [hashCode]
    @Override
    public int hashCode() {
        Long id = getId();
        return (id != null ? id.hashCode() : 0);
    }

    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractCoreEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractCoreEntity other = (AbstractCoreEntity) entity;
        Long id = getId();
        Long otherId = other.getId();
        return id != null && otherId != null && id.equals(otherId);
    }

    // getters and setters
...
    public void setCategory(Category category) {
        // entity type
        if (entityType == EntityType.PROXY) {
            throw new ProxyException(1005, new RuntimeException(
                    "The category of a [PROXY] product cannot be changed"), simpleClassName);
        }
        this.category = category;
    }

}
  • line 21: the [@Entity] annotation makes the [Product] class an entity managed by the [JPA] layer. You can also write [@Entity(name="MyProduct")], which gives the entity the name [MyProduct]. In the absence of this information, the entity’s name is the class name, in this case [Product]. This naming convention becomes necessary when there are two classes from different packages among the entities that share the same name;
  • line 22: the annotation [@Table(name = "PRODUCTS")] indicates that the [Product] class is the object representation of a row in the [PRODUCTS] table in the database;
  • line 23: the name of the JSON filter to apply to the entity. We will see that the [categorie] property on line 58 is not always available. It must therefore be excluded from the object’s JSON representation. To do this, we need a filter. We will therefore use a filter named [jsonFilterCategorie] to specify whether or not we want the [categorie] property;
  • line 26: the [@Id] annotation makes the annotated field the field associated with the primary key of the table in line 19;
  • line 27: the [@GeneratedValue(strategy = GenerationType.IDENTITY)] annotation sets the automatic generation mode for the primary key in the [PRODUITS] table. The [strategy] attribute determines this. There are different modes:

Image

The [IDENTITY] strategy is not available for all DBMSs. Among the six DBMSs tested, it was available for [MySQL 5, PostgreSQL 9.4, SQL Server 2014, DB2 Express-C10.5]. For the other two [Oracle Express 11g Release 2, Firebird 2.5.4], the [SEQUENCE] strategy had to be used. For portability between JPA implementations, the [AUTO] strategy should not be used, as it leaves the choice of primary key generation strategy up to the JPA implementation. Thus, with MySQL 5 and the [AUTO] strategy:

  • Hibernate chooses the [IDENTITY] strategy with the [AUTO_INCREMENT] mode for the primary key;
  • EclipseLink chooses the [TABLE] strategy, which creates a table named [SEQUENCE] by default that must be queried to retrieve the primary keys.

Ultimately, the database structure managed by these two JPA implementations is not the same. If it was generated by Hibernate, it will not be usable by EclipseLink, and vice versa.

  • Line 28: The [@Column(name="ID"] annotation sets the name of the column in the [PRODUCTS] table to be associated with the [id] field;
  • line 29: the type [Long] is used instead of [long] for the primary key. This is because [null] primary keys have a specific meaning for JPA. Therefore, it is preferable to use an object type here rather than a simple type;
  • line 31: the [@Version] annotation indicates that the [version] field is associated with a versioning column. The JPA implementation will increment this version number each time the entity is modified. This number is used to prevent simultaneous updates of the entity by two different users: two users, U1 and U2, read entity E with a version number equal to V1. U1 modifies E and persists this change to the database: the version number then changes to V1+1. U2 modifies E in turn and attempts to persist this change to the database: they will receive an exception because their version (V1) differs from the one in the database (V1+1);
  • Line 36: the entity type. There will be two: POJO and PROXY. By default, the Product instance will be a POJO (Plain Old Java Object). In some cases, the [Product] instances retrieved from the database will be of type [PROXY]. This will be the case when the [Category] property on line 58 has not been initialized with a category due to the [fetch = FetchType.LAZY] attribute on line 56. In this case, the JPA implementations to be tested differ:
    • [Hibernate, OpenJPA]: accessing the category of a [PROXY]-type product throws an exception. Hibernate uses the term "proxy" to refer to a JPA instance obtained in [LAZY] mode. This is why I used this term to refer to this type of entity;
    • [EclipseLink]: accessing the category of a product of type [PROXY] triggers a search for that category in the database, and no exception is thrown;

Because I wanted a test layer independent of the JPA implementation used, I needed to know the type of each entity: POJO or PROXY. That is why I added the [entityType] field to the JPA entities;

  • Line 35: The [@Transient] annotation indicates that the JPA implementation must ignore this field. Indeed, it does not exist in the DBMS tables;
  • line 40: the [Product] class throws a [ProxyException] that requires the class name;
  • line 38: as before, we indicate that the JPA implementation must ignore this field;
  • line 39: the [@JsonIgnore] annotation indicates that the JSON serializer/deserializer for a [Product] instance must ignore this field;
  • line 43: the [@Column] annotation associates the [name] field with the [NAME] column in the [PRODUCTS] table. When the field has the same name as the associated column (case-insensitive), the [@Column] annotation can be omitted. This would be the case here. The attributes [unique = true, length = 30, nullable = false] are only used when the JPA implementation generates the [CATEGORIES] table from the [Product] entity. They will be translated into the SQL attributes [UNIQUE, VARCHAR(30), NOT NULL], which ensure that the [NAME] column will have at most 30 characters, will be unique in the table, and cannot have the value NULL;
  • lines 46–47: the [idCategorie] field is linked to the [CATEGORIE_ID] column. We will return to these attributes a little later;
  • lines 49–50: the [price] field is associated with the [PRICE] column;
  • lines 52-53: the [description] field is associated with the [DESCRIPTION] column;
  • lines 56–58: the product category;
  • line 56: the [@ManyToOne] annotation indicates that the column referenced by the annotation on line 57 [@JoinColumn(name = "CATEGORIE_ID")] is a foreign key from the [PRODUITS] table of the [Product] entity to the [CATEGORIES] table associated with the entity on line 58. This annotation must be applied to a JPA entity. Therefore, the class in line 58 must be a JPA entity;
  • Line 56: The annotation [fetch = FetchType.LAZY] specifies that when a product is retrieved from the [PRODUCTS] table, its category (line 58) is not retrieved immediately (lazy loading). It is then retrieved during the first call to the [getCategory] method. To achieve this, at runtime, the JPA layer enhances the initial [getCategorie] method (which simply returns the category field) by making a call to the DBMS to fetch the category—a technique known as "proxying." JPA implementations differ in how they handle this feature, as mentioned earlier. This attribute is not mandatory. The JPA implementation used is allowed to ignore it. It is because the [categorie] property may or may not be present that we introduced the JSON filter on line 23. The join column [CATEGORIE_ID] in the [PRODUITS] table is automatically updated when a product is inserted or updated. It receives the value of [categorie.getId()], where [categorie] is the field on line 58. The JPA specification requires that this join column cannot be updated by any other means. It therefore enforces the attributes [insertable = false, updatable = false] on line 46, which ensure that the [CATEGORIE_ID] column (the join column) associated with the [idCategorie] field cannot be modified by the [idCategorie] field. Only the transfer of the [CATEGORIE_ID] column into the [idCategorie] field will be possible;
  • lines 91–104: equality between [Product] entities is defined as equality between their primary keys [id];
  • lines 108-115: to make our test layer portable, we will uniformly manage the [PROXY] entities across the three JPA implementations [Hibernate, EclipseLink, OpenJpa]. For a [Product] entity of type [PROXY], we will prevent the value of the [category] field from being changed. The [ProxyException] class is as follows:
  

package generic.jpa.infrastructure;

import generic.jdbc.infrastructure.UncheckedException;

public class ProxyException extends UncheckedException {

    private static final long serialVersionUID = 7278276670314994574L;

    public ProxyException() {
    }

    public ProxyException(int code, Throwable e, String simpleClassName) {
        super(code, e, simpleClassName);
    }

}

To conclude the discussion of this entity, it should be noted that annotations and their attributes are used in two distinct cases:

  • to create database tables;
  • to query them. In this case, the JPA implementation expects to find the tables exactly as it would have generated them itself. Therefore, we cannot associate just any [PRODUCTS] table with the previous [Product] entity. It must have at least (it may have others) the characteristics of the [PRODUCTS] table that JPA would have generated. When working with JPA, the ideal approach is to start with an empty database in which JPA generates the tables. We will discuss this generation a little later. The SQL script provided for the MySQL DBMS was generated from the tables generated by JPA.

All attributes of the [Product] entity are used to generate the [PRODUCTS] table. Once this is done, generation attributes such as [unique = true, length = 30, nullable = false] are no longer used when querying the tables.

6.3.3.3. The JPA [Category] entity

The [Category] class is a JPA entity associated with a row in the [CATEGORIES] table:

Its code is as follows:


package generic.jpa.entities.dbproduitscategories;

import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;

import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
@Table(name = ConfigJdbc.TAB_CATEGORIES)
@JsonFilter("jsonFilterCategory")
public class Category implements AbstractCoreEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;

    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;

    @Transient
    protected EntityType entityType = EntityType.POJO;

    @Transient
    @JsonIgnore
    protected String simpleClassName = getClass().getSimpleName();

    // properties
    @Column(name = ConfigJdbc.TAB_CATEGORIES_NAME, unique = true, length = 30, nullable = false)
    private String name;

    // associated products
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "category", cascade = { CascadeType.ALL })
    private List<Product> products;

    // constructors
    public Category() {

    }

    public Category(Long id, Long version, String name, List<Product> products) {
        this.id = id;
        this.version = version;
        this.name = name;
        this.products = products;
    }

    // signature
    public String toString() {
        return String.format("[id=%s, version=%s, name=%s]", id, version, name);
    }

    // methods
    public void addProduct(Product product) {
        // entity type
        if (entityType == EntityType.PROXY) {
            throw new ProxyException(1004, new RuntimeException(
                    "Cannot add products to a category of type [PROXY]"), simpleClassName);
        }
        // add a product
        if (products == null) {
            products = new ArrayList<Product>();
        }
        if (product != null) {
            // add the product
            products.add(product);
            // set its category
            product.setCategory(this);
            product.setCategoryId(this.id);
        }
    }

    // ------------------------------------------------------------
    // Override [equals] and [hashCode]
    @Override
    public int hashCode() {
        Long id = getId();
        return (id != null ? id.hashCode() : 0);
    }

    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractCoreEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractCoreEntity other = (AbstractCoreEntity) entity;
        Long id = getId();
        Long otherId = other.getId();
        return id != null && otherId != null && id.equals(otherId);
    }

    // getters and setters
...
}
  • line 24: the class is a JPA entity;
  • line 25: associated with the [CATEGORIES] table;
  • line 26: the JSON representation of the [Category] entity is controlled by the filter named [jsonFilterCategory]. This must be configured before any request for a JSON representation of the entity. The [jsonFilterCategorie] filter will be used to determine whether or not to include the [products] field from line 40 in the JSON representation of the [Categorie] entity;
  • lines 29–32: the [id] field is associated with the primary key [ID] of the [CATEGORIES] table. The selected generation mode is [IDENTITY], which corresponds to [AUTO_INCREMENT] for MySQL;
  • lines 34–36: the [version] field is linked to the [VERSIONING] column in the [CATEGORIES] table;
  • lines 38-39: the type of the [Categorie] entity;
  • lines 41–43: the short name of the [Categorie] class;
  • Lines 46–47: The [name] field is linked to the [NAME] column in the [CATEGORIES] table. We assign it the JPA attributes [unique = true, length = 30, nullable = false] so that when the [CATEGORIES] table is generated, the [NAME] column has the SQL attributes [UNIQUE, VARCHAR(30), NOT NULL];
  • lines 50-51: the products that belong to the category;
  • line 50: the [@OneToMany] annotation is the inverse relationship of the [@ManyToOne] relationship we encountered in the [Product] entity. The [mappedBy = "category"] attribute specifies the field in the [Product] entity annotated by the inverse [@ManyToOne] relationship. The attribute [cascade = { CascadeType.ALL }] specifies that operations (persist, merge, remove) performed on an @Entity [Category] should cascade to the [products] in line 51. Partial cascades can be specified using the constants [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE];
  • line 50: the [fetch = FetchType.LAZY] attribute specifies that when a category is retrieved from the [CATEGORIES] table, its products are not immediately retrieved. They are retrieved during the first call to the [getProduits] method. To achieve this, at runtime, the JPA layer enhances the initial [getProduits] method (which simply returns the products field) by making a call to the DBMS to fetch the products for the category. This attribute is mandatory. The JPA implementation cannot ignore it. Because the [products] property may or may not be initialized, we have introduced the JSON filter on line 26, which allows us to specify whether or not we want this property and the entity type on line 39;
  • lines 71–88: the [addProduct] method allows adding a product to the category;
  • lines 73–76: To standardize proxy handling across different JPA implementations, we decided that products cannot be added to a [Category] entity of type PROXY;
  • lines 92–112: two [Category] entities are considered equal if they have the same primary key [id];

6.3.4. The [persistence.xml] file

  

JPA applications must define certain properties of the JPA provider being used, as well as the JPA entities to be used, in a [META-INF/persistence.xml] file located in the application’s classpath. Above, it has been placed in the [src/main/resources] folder, which is effectively part of an Eclipse project’s classpath. When using JPA in conjunction with Spring, certain information that should be in the [persistence.xml] file is placed elsewhere in Spring configuration classes. In a Spring JPA application, Spring drives JPA. With Spring JPA Hibernate, the [persistence.xml] file can be reduced to its simplest form:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" 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_1_0.xsd">
    <persistence-unit name="dummy-persistence-unit" transaction-type="RESOURCE_LOCAL" />
</persistence>
  • lines 1-5: a [persistence.xml] file must have a root <persistence> tag. The attributes of the tag on line 2 will not be used in this application;
  • a persistence file can define one or more persistence units using the <persistence-unit> tag (line 4). A persistence unit manages access to a specific database. If the application manages two databases simultaneously, it will have two persistence units;
  • Line 4: A persistence unit has a name [name attribute], supports a transaction type [transaction-type attribute], has properties, and defines the entities associated with the database tables managed by the persistence unit. Here, since database access will be managed by [Spring JPA Hibernate], these last two pieces of information can be placed elsewhere. There are two types of transactions:
    • [RESOURCE_LOCAL]: transactions are managed by the application itself. This is the case here, where Spring will manage the transactions;
    • [JTA] (Java Transaction API): the EJB (Enterprise Java Bean) container running the application automatically manages transactions based on Java annotations found in the code. We are not using this configuration here;

We will see later that the contents of this [persistence.xml] file depend on the JPA implementation used.

6.4. The [spring-jpa-generic] project

Let’s recap what we want to do. We want to implement the following architecture:

in which the [DAO] layer would implement the [IDao<Product>, IDao<Category>] interface studied in Chapter 4. The goal is to compare two implementations of this interface:

  • one built with Spring JDBC;
  • the other built with Spring JPA;

In the architecture above:

  • the [JDBC] layer is implemented by the [mysql-config-jdbc] project discussed in Section 3.3;
  • the [JPA] layer is implemented by the [mysql-config-jpa-hibernate] project discussed in Section 6.3;

The [spring-jpa-generic] project handles the implementation of the [DAO] and [Spring Data] layers.

  

6.4.1. Maven Configuration

The [spring-jpa-generic] project is a Maven project configured by the following [pom.xml] file:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-jpa-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jpa-generic</name>
    <description>Spring Data demo with category and product tables</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- JPA configuration for the DBMS -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jpa</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • Lines 22–26: The project has only one dependency, which is on the project that configures the application’s [JPA] layer, which we just examined. This is a generic application:
    • we change the DBMS by changing the [JDBC] layer configuration project;
    • we change the JPA implementation by changing the [JPA] layer configuration project;

In the end, the dependencies are as follows:

  

6.4.2. Spring Configuration

  

The [AppConfig] class configures the Spring project:


package spring.data.config;

import generic.jpa.config.ConfigJpa;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
@Import({ ConfigJpa.class })
public class AppConfig {

}
  • Line 11: The class is a Spring configuration class;
  • line 10: the [@EnableJpaRepositories] annotation is used to designate the packages containing Spring Data’s [CrudRepository] interfaces. This makes them Spring components that can be injected into other Spring components;
  • line 12: the [@ComponentScan] annotation indicates that the [spring.data.dao] package must be scanned for Spring components. The [DaoCategory] and [DaoProduct] components will be found;
  • line 13: the beans from the [ConfigJpa] configuration class are imported. These include the bean for the JPA implementation being used (Hibernate, Eclipselink, OpenJpa), the data source to be used, the EntityManager that will handle JPA operations, and the transaction manager;

6.4.3. The [Spring Data] layer

  

6.4.3.1. The [CategoriesRepository] interface

The [CategoriesRepository] interface manages access to the [CATEGORIES] table:


package spring.data.repositories;

import generic.jpa.entities.dbproduitscategories.Categorie;

import java.util.List;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

public interface CategoriesRepository extends CrudRepository<Category, Long> {

    // Category with its products
    @Query("select c from Category c left join fetch c.products where c.id=?1")
    public Categorie getLongCategorieById(Long id);

    @Query("select c from Category c left join fetch c.products where c.name=?1")
    public Category getLongCategoryByName(String name);

    @Query("select c from Category c where c.name in ?1")
    public List<Category> getShortCategoriesByName(Iterable<String> names);

    @Query("select c from Category c where c.id in ?1")
    public List<Category> getShortCategoriesById(Iterable<Long> ids);

    @Query("select distinct c from Category c left join fetch c.products where c.id in ?1")
    public List<Category> getLongCategoriesById(List<Long> names);

    @Query("select distinct c from Category c left join fetch c.products where c.name in ?1")
    public List<Category> getLongCategoriesByName(List<String> names);

    @Query("select c from Category c")
    public List<Category> getAllShortCategories();

    @Query("select distinct c from Category c left join fetch c.products")
    public List<Category> getAllLongCategories();

}
  • Line 10: The [CrudRepository] interface was used and explained in Section 5.1.3. As a reminder:
    • the first parameter type of the interface is the JPA entity managed for CRUD operations (findOne, findAll, save, delete, deleteAll),
    • the second parameter type of the interface is the primary key of the JPA entity, here an integer [Long];

The methods of the interface are implemented using JPQL (Java Persistence Query Language) queries. These queries target JPA entities. In such a query:

  • tables are replaced by their associated JPA entities;
  • columns are replaced by fields of the JPA entities used in the query;

Let’s take the example of lines 31–32: the method on line 32 retrieves all categories from the database in their short form. It is implemented by the JPQL (Java Persistence Query Language) query on line 31, which closely resembles its SQL counterpart. For a deeper understanding of JPQL, see [ref2] (see section 1.2).

The methods of the [CategoriesRepository] interface are as follows:

  • Lines 13-14: The [getLongCategoryById] method returns the long version of a category referenced by its primary key [id], i.e., the category along with its products. Recall that in the [Category] entity, the [products] field had the attribute [fetch = FetchType.LAZY] (lazy loading). In the JPQL query, we force the loading of products using the [fetch] keyword. The query’s ?1 parameter will be replaced at runtime by the value of the first parameter of the method on line 12, i.e., the [Long id] parameter;
  • lines 16–17: the [getLongCategoryByName] method returns the long version of a category referenced by its name [name];
  • lines 19–20: the [getShortCategoriesByName] method returns the short versions of categories referenced by their names. The [products] field of these categories is not null. It contains a reference to a proxy (a class created by the JPA implementation) whose role is to retrieve the products in the category when called. Calling it outside the JPA persistence context throws an exception (Hibernate and OpenJPA, but not EclipseLink). For this reason, we will not use the [products] field of a category’s short version;
  • lines 22–23: the [getShortCategoriesById] method returns the short versions of categories referenced by their primary keys [id];
  • lines 25–26: the [getLongCategoriesById] method returns the long versions of categories referenced by their primary keys [id];
  • lines [28-29]: the [getLongCategoriesByName] method returns the long versions of categories referenced by their names;
  • lines 31-32: the [getAllShortCategories] method returns the short versions of all categories;
  • lines 34-35: the [getAllLongCategories] method returns the long versions of all categories;

Note: Not all JPA implementations support the same JPQL syntax. Thus, the following syntax is accepted by Hibernate and EclipseLink but not by OpenJpa:


@Query("select c from Category c left join fetch c.products p where c.name=?1")

OpenJpa does not accept the alias [p] above.

6.4.3.2. The [ProductsRepository] interface

The [ProductsRepository] interface manages access to the [PRODUCTS] table:


package spring.data.repositories;

import generic.jpa.entities.dbproduitscategories.Product;

import java.util.List;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Transactional;

@Transactional()
public interface ProductRepository extends CrudRepository<Product, Long> {

    // a product with its category
    @Query("select p from Product p left join fetch p.category where p.id=?1")
    public Product getLongProductById(Long id);

    @Query("select p from Product p left join fetch p.category where p.name=?1")
    public Product getLongProductByName(String name);

    @Query("select p from Product p where p.id in ?1")
    public List<Product> getShortProductsById(List<Long> ids);

    @Query("select p from Product p where p.name in ?1")
    public List<Product> getShortProductsByName(List<String> names);

    @Query("select distinct p from Product p left join fetch p.category where p.id in ?1")
    public List<Product> getLongProductsById(List<Long> ids);

    @Query("select distinct p from Product p left join fetch p.category where p.name in ?1")
    public List<Product> getLongProductsByName(List<String> names);

    @Query("select distinct p from Product p left join fetch p.category")
    public List<Product> getAllProducts();

    @Query("select p from Product p")
    public List<Product> getAllShortProducts();
}
  • lines [15-16]: the [getLongProductById] method returns the long version of a product identified by its primary key [id], including its category. Recall that in the [Product] entity, the [category] field had the attribute [fetch = FetchType.LAZY] (lazy loading). In the JPQL query, we force the loading of the category using the [fetch] keyword;
  • lines 18-19: the [getLongProductByName] method returns the long version of a product identified by its name;
  • lines 21-22: the [getShortProduitsById] method returns the short version of products identified by their primary key [id]. In this short version, the [category] field is not null. It contains a reference to a proxy generated by the JPA implementation, which, if called, will fetch the product’s category. This call can only be made within the JPA persistence context. Making it elsewhere causes an exception (Hibernate and OpenJPA, but not EclipseLink). Therefore, in the [DAO] layer or elsewhere, we will not use the [category] field of a product in its short version. In the product’s short version, the [idCategorie] field is initialized. Its value is the primary key of the category to which the product belongs. This allows us to later retrieve this category from the [DAO] layer via the method [DaoCategorie.getShortCategoriesById(idCategorie)];
  • lines 24–25: the [getShortProduitsByName] method returns the short version of the products identified by their names;
  • lines 27–28: the [getLongProduitsById] method returns the long version of the products identified by their primary keys;
  • lines 30-31: the [getLongProductsByName] method returns the long version of the products identified by their names;
  • lines 33-34: the [getAllLongProducts] method returns the long version of all products;
  • lines 36-37: the [getAllShortProducts] method returns the short version of all products;

These interfaces will be implemented by classes generated by the JPA implementation at runtime. Such classes are called [proxy] classes. By default, the methods of the [CrudRepository] interface execute within a transaction. The fact that the [ProductsRepository] and [CategoriesRepository] interfaces extend the [CrudRepository] class makes them Spring components. As such, they can be injected into other Spring components.

6.4.4. The [DAO] layer

  

6.4.4.1. The [IDao<T>] interface

The [IDao<T>] interface is the one already discussed in the implementation of the [DAO] layer using Spring JDBC (see section 4.7);


package spring.data.dao;

import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity;

import java.util.List;

public interface IDao<T extends AbstractCoreEntity> {

    // list of all T entities
    public List<T> getAllShortEntities();

    public List<T> getAllLongEntities();

    // specific entities - short version
    public List<T> getShortEntitiesById(Iterable<Long> ids);

    public List<T> getShortEntitiesById(Long... ids);

    public List<T> getShortEntitiesByName(Iterable<String> names);

    public List<T> getShortEntitiesByName(String... names);

    // specific entities - long version
    public List<T> getLongEntitiesById(Iterable<Long> ids);

    public List<T> getLongEntitiesById(Long... ids);

    public List<T> getLongEntitiesByName(Iterable<String> names);

    public List<T> getLongEntitiesByName(String... names);

    // Update multiple entities
    public List<T> saveEntities(Iterable<T> entities);

    public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);

    // Delete all entities
    public void deleteAllEntities();

    // Delete multiple entities
    public void deleteEntitiesById(Iterable<Long> ids);

    public void deleteEntitiesById(Long... ids);

    public void deleteEntitiesByName(Iterable<String> names);

    public void deleteEntitiesByName(String... names);

    public void deleteEntitiesByEntity(Iterable<T> entities);

    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}

6.4.4.2. The abstract class [AbstractDao]

  

The abstract class [AbstractDao] is the parent class of the classes implementing the [DAO] layer:

  • the [DaoProduit] class, which implements the [IDao<Produit>] interface and manages access to the [PRODUITS] table;
  • the [DaoCategorie] class, which implements the [IDao<Categorie>] interface and manages access to the [CATEGORIES] table;

Its code is as described in Section 4.8, with the following minor difference: no method has the [@Transactional] attribute, which causes the method to execute within a transaction. Here, we take advantage of the fact that Spring Data’s [CrudRepository] interfaces execute within a transaction by default.

6.4.4.3. The [DaoCategorie] class

  

The [DaoCategorie] class implements the [IDao<Categorie>] interface as follows:


package spring.data.dao;

import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Category;
import generic.jpa.entities.dbproduitscategories.Product;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProductsRepository;

@Component
public class CategoryDao extends AbstractDao<Category> {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private CategoriesRepository categoriesRepository;

    @Override
    public List<Category> getAllShortEntities() {
        try {
            return setShortCategoriesType(categoriesRepository.getAllShortCategories());
        } catch (Exception e) {
            throw new DaoException(211, e, simpleClassName);
        }
    }

    private List<Category> setShortCategoriesType(List<Category> categories) {
        for (Category category : categories) {
            category.setEntityType(EntityType.PROXY);
        }
        return categories;
    }

    @Override
    public List<Category> getAllLongEntities() {
        try {
            return categoriesRepository.getAllLongCategories();
        } catch (Exception e) {
            throw new DaoException(202, e, simpleClassName);
        }
    }

    @Override
    public void deleteAllEntities() {
        try {
            categoriesRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(208, e, simpleClassName);
        }
    }

    @Override
    protected List<Category> getShortEntitiesById(List<Long> ids) {
        try {
            return setShortCategoriesType(categoriesRepository.getShortCategoriesById(ids));
        } catch (Exception e) {
            throw new DaoException(203, e, simpleClassName);
        }
    }

    @Override
    protected List<Category> getShortEntitiesByName(List<String> names) {
        try {
            return setShortCategoriesType(categoriesRepository.getShortCategoriesByName(names));
        } catch (Exception e) {
            throw new DaoException(204, e, simpleClassName);
        }
    }

    @Override
    protected List<Category> getLongEntitiesById(List<Long> ids) {
        try {
            return categoriesRepository.getLongCategoriesById(ids);
        } catch (Exception e) {
            throw new DaoException(205, e, simpleClassName);
        }
    }

    @Override
    protected List<Category> getLongEntitiesByName(List<String> names) {
        try {
            return categoriesRepository.getLongCategoriesByName(names);
        } catch (Exception e) {
            throw new DaoException(206, e, simpleClassName);
        }
    }

    @Override
    protected List<Category> saveEntities(List<Category> categories) {
    ...
    }

    @Override
    protected void deleteEntitiesById(List<Long> ids) {
        try {
            categoriesRepository.delete(getShortEntitiesById(ids));
        } catch (Exception e) {
            throw new DaoException(209, e, simpleClassName);
        }
    }

    @Override
    protected void deleteEntitiesByName(List<String> names) {
        try {
            categoriesRepository.delete(getShortEntitiesByName(names));
        } catch (Exception e) {
            throw new DaoException(212, e, simpleClassName);
        }
    }

}
  • line 17: the [@Component] annotation makes the [DaoCategorie] class a Spring component;
  • line 18: the [DaoCategorie] class extends the [AbstractDao<Categorie>] class, which means it implements the [IDao<Categorie>] interface;
  • lines 20–24: injection of references to the two [CrudRepository] interfaces from [Spring Data]. This injection occurs when Spring objects are instantiated, typically at the start of the Spring project’s execution;
  • All methods of the class delegate the work to the methods of the same names in the [CrudRepository] interfaces;
  • All methods that return entities in their short form indicate this by setting the entity type to [EntityType.PROXY] (lines 29, 63, 72);

The [saveEntities] method warrants an explanation:


@Override
    protected List<Category> saveEntities(List<Category> categories) {
        // we note the products that will be inserted
        List<Product> insertedProducts = new ArrayList<Product>();
        for (Category category : categories) {
            EntityType categoryType = category.getEntityType();
            List<Product> products = null;
            if ((categoryType == EntityType.POJO) && (products = category.getProducts()) != null) {
                for (Product product : products) {
                    if (product.getId() == null) {
                        insertedProducts.add(product);
                    }
                    // We take this opportunity to re-establish (if necessary) the product --> category relationship
                    product.setCategory(category);
                }
            }
        }
        // Save the categories and products
        try {
            categoriesRepository.save(categories);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // Update the [idCategorie] field for the inserted products
        for (Product product : insertedProducts) {
            product.setCategoryId(product.getCategory().getId());
        }
        // result
        return categories;
    }
  • line 2: the categories passed as parameters are both categories to be inserted [id==null] and categories to be updated [id!=null];
  • line 20: we persist the categories using the method [categoriesRepository.save(entities)]. During testing, we observe that the [idCategorie] field of the persisted products (id==null) is not populated. To resolve this issue, we note in lines 4–17 the products to be inserted, and once persisted, we populate their [idCategorie] field (lines 25–27);
  • lines 5–17: we iterate through the list of categories;
  • lines 8–16: for each category, we iterate through its list of products. Here lies a difficulty. The [saveEntities] method is used both to persist and to modify a category. In the latter case, the category may have been retrieved in its short version, thus containing a reference to a proxy method in the [products] field. Using it with Hibernate then causes an exception, because the category being used is no longer in the JPA persistence context, which was closed at the end of the transaction for the method that retrieved the short versions of the categories. We then use the [EntityType] field of the [Category] entity on line 8 to determine whether or not we can access the category’s list of products;
  • line 14: we link the product to its category. Normally, this should already be the case. But we do not know how this product was created or whether it has been linked to its category. So, to avoid any issues (to manage the [Product] entity, JPA requires that it reference the [Category] entity to which it is linked), we establish this link ourselves.

By comparing this code to that of the [DaoProduit] class in the Spring JDBC implementation (see section 4.9), we can see that the Spring Data JPA library greatly simplifies the writing of the [DAO] layer.

6.4.4.4. The [ProductDao] class

  

The [DaoProduct] class implements the [IDao<Product>] interface as follows:


package spring.data.dao;

import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Category;
import generic.jpa.entities.dbproduitscategories.Product;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProductsRepository;

import com.google.common.collect.Lists;

@Component
public class ProductDao extends AbstractDao<Product> {
    @Autowired
    private ProductRepository productsRepository;

    @Autowired
    private CategoriesRepository categoriesRepository;

    @Override
    public List<Product> getAllShortEntities() {
        try {
            return setShortProductsType(productsRepository.getAllShortProducts());
        } catch (Exception e) {
            throw new DaoException(102, e, simpleClassName);
        }
    }

    private List<Product> setShortProductsType(List<Product> products) {
        for (Product product : products) {
            product.setEntityType(EntityType.PROXY);
        }
        return products;
    }

    @Override
    public List<Product> getAllLongEntities() {
        try {
            return productsRepository.getAllLongProducts();
        } catch (Exception e) {
            throw new DaoException(117, e, simpleClassName);
        }
    }

    @Override
    public void deleteAllEntities() {
        try {
            productsRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(112, e, simpleClassName);
        }
    }

    @Override
    protected List<Product> getShortEntitiesById(List<Long> ids) {
        try {
            return setShortProductsType(productRepository.getShortProductsById(ids));
        } catch (Exception e) {
            throw new DaoException(103, e, simpleClassName);
        }
    }

    @Override
    protected List<Product> getShortEntitiesByName(List<String> names) {
        try {
            return setShortProductsType(productRepository.getShortProductsByName(names));
        } catch (Exception e) {
            throw new DaoException(104, e, simpleClassName);
        }
    }

    @Override
    protected List<Product> getLongEntitiesById(List<Long> ids) {
        try {
            return linkLongProductsToCategories(productRepository.getLongProductsById(ids));
        } catch (Exception e) {
            throw new DaoException(105, e, simpleClassName);
        }
    }

    @Override
    protected List<Product> getLongEntitiesByName(List<String> names) {
        try {
            return linkLongProductsToCategories(productRepository.getLongProductsByName(names));
        } catch (Exception e) {
            throw new DaoException(106, e, simpleClassName);
        }
    }

    private List<Product> linkLongProductsToCategories(List<Product> products) {
        for (Product product : products) {
            Category category = product.getCategory();
            if (category != null) {
                product.setCategory(category);
                product.setCategoryId(category.getId());
            }
        }
        return products;
    }

    @Override
    protected List<Product> saveEntities(List<Product> entities) {
        // restore (if necessary) the link between a product and its category
        for (Product product : entities) {
            if (product.getEntityType() == EntityType.POJO) {
                product.setCategory(new Category(product.getCategoryId(), 0L, null, null));
            }
        }
        // persist the products
        try {
            return Lists.newArrayList(productRepository.save(entities));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
    }

    @Override
    protected void deleteEntitiesById(List<Long> ids) {
        try {
            productsRepository.delete(getShortEntitiesById(ids));
        } catch (Exception e) {
            throw new DaoException(113, e, simpleClassName);
        }
    }

    @Override
    protected void deleteEntitiesByName(List<String> names) {
        try {
            productsRepository.delete(getShortEntitiesByName(names));
        } catch (Exception e) {
            throw new DaoException(118, e, simpleClassName);
        }
    }

}

The code is similar to that of the [DaoCategorie] class:

  • for the long versions of categories, tests show that the [idCategorie] field of products is not populated. The [linkLongProduitsToCategories] method in lines 96–105 resolves this issue;
  • the [saveEntities] method in lines 108–121 inserts new products or modifies existing ones. The JPA layer requires that each [Product] entity be linked to a [Category] entity. Since we don’t know if the user has done this, we do it ourselves in lines 110–113. All we need to do is link the [Product] to a [Category] entity whose primary key matches the [idCategory] field of the [Product]. During testing, we find that an error occurs if we set the category version to null. So here we set it to 0, but we can set it to whatever we want. Apart from the primary key, no fields of the [Category] entity are required by the JPA layer to insert or update a [Product] entity;

6.4.5. The test layer

  

The tests above are identical to those in the Spring JDBC implementation. Refer to the following pages if necessary:

  • [JUnitTestCheckArguments]: section 4.11.1;
  • [JUnitTestDao]: section 4.11.2;
  • [JUnitTestPushTheLimits]: section 4.11.3;

We use the following test configurations:

The results obtained for the various tests are as follows:

In [1], the [JUnitTestPushTheLimits] test with the Spring Data JPA Hibernate implementation, and in [2], with the Spring JDBC implementation. We can see that the latter performs better. We therefore reach an initial conclusion: it is significantly easier to develop a [DAO] layer with Spring Data JPA, but it is less performant than a Spring JDBC implementation.

The [JUnitTestProxies] test is a dummy JUnit test. It is there to demonstrate how each JPA implementation behaves when dealing with proxies, i.e., the short versions of entities:


package spring.data.tests;

import generic.jpa.entities.dbproduitscategories.Category;
import generic.jpa.entities.dbproduitscategories.Product;

import java.util.ArrayList;
import java.util.List;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import spring.data.config.AppConfig;
import spring.data.dao.IDao;

import com.google.common.collect.Lists;

@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestProxies {

    // [DAO] layer
    @Autowired
    private IDao<Product> productDao;
    @Autowired
    private IDao<Category> categoryDao;

    @Before
    public void clean() {
        // We clear the database before each test
        log("Clearing the database", 1);
        // Empty the [CATEGORIES] table and, by cascade, the [PRODUCTS] table
        daoCategory.deleteAllEntities();
    }

    @Test
    public void doNothing() {
        System.out.println("doNothing");
    }

    private List<Category> fill(int numCategories, int numProducts) {
        // populate the tables
        List<Category> categories = new ArrayList<Category>();
        for (int i = 0; i < nbCategories; i++) {
            Category category = new Category(null, null, String.format("category[%d]", i), null);
            category.setProducts(new ArrayList<Product>());
            for (int j = 0; j < nbProducts; j++) {
                Product product = new Product(null, null, String.format("product[%d,%d]", i, j), null,
                        100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
                category.addProduct(product);
            }
            categories.add(category);
        }
        // Adding the category - the products will also be
        // inserted
        daoCategory.saveEntities(categories);
        // result
        return categories;
    }

    @Test
    public void getShortCategoriesByName1() {
        // initialization
        fill(1, 1);
        // test
        log("getShortCategoriesByName1", 1);
        Category category = daoCategory.getShortEntitiesByName(Lists.newArrayList("category[0]")).get(0);
        System.out.println(String.format("Category type: %s", category.getEntityType()));
        System.out.println("Category:");
        try {
            System.out.println(category.getProducts().size());
        } catch (Exception e) {
            System.err.println(String.format("Exception: %s, Message: %s", e.getClass().getName(), e.getMessage()));
        }
    }

    @Test
    public void getShortProductsByName1() {
        // initialization
        fill(1, 1);
        // test
        log("getShortProductsByName1", 1);
        Product product = ProductDAO.getShortEntitiesByName(Lists.newArrayList("product[0,0]")).get(0);
        System.out.println(String.format("Product type: %s", product.getEntityType()));
        System.out.println("Product category name:");
        try {
            System.out.println(product.getCategory().getName());
        } catch (Exception e) {
            System.err.println(String.format("Exception: %s, Message: %s", e.getClass().getName(), e.getMessage()));
        }
    }

    @Test
    public void getLongCategoriesByName1() {
        // initialization
        fill(1, 1);
        // test
        log("getLongCategoriesByName1", 1);
        Category category = daoCategory.getLongEntitiesByName(Lists.newArrayList("category[0]")).get(0);
        System.out.println(String.format("Category type: %s", category.getEntityType()));
        System.out.println("Category:");
        try {
            System.out.println(category.getProducts().size());
        } catch (Exception e) {
            System.err.println(String.format("Exception: %s, Message: %s", e.getClass().getName(), e.getMessage()));
        }
    }

    @Test
    public void getLongProductsByName1() {
        // initialization
        fill(1, 1);
        // test
        log("getLongProductsByName1", 1);
        Product product = ProductDAO.getLongEntitiesByName(Lists.newArrayList("product[0,0]")).get(0);
        System.out.println(String.format("Product type: %s", product.getEntityType()));
        System.out.println("Product category name:");
        try {
            System.out.println(product.getCategory().getName());
        } catch (Exception e) {
            System.err.println(String.format("Exception: %s, Message: %s", e.getClass().getName(), e.getMessage()));
        }
    }

    private void log(String message, int mode) {
        // display message
        String toPrint = null;
        switch (mode) {
        case 1:
            toPrint = String.format("%s --------------------------------", message);
            break;
        case 2:
            toPrint = String.format("-- %s", message);
            break;
        }
        System.out.println(toPrint);
    }

}

The results are as follows:


Dumping the database --------------------------------
doNothing
Emptying the database --------------------------------
getShortCategoriesByName1 --------------------------------
Category type: PROXY
Category:
Exception: org.hibernate.LazyInitializationException, Message: failed to lazily initialize a collection of role: generic.jpa.entities.dbproduitscategories.Categorie.produits, could not initialize proxy - no Session
Dumping the database --------------------------------
getLongCategoriesByName1 --------------------------------
Type category: POJO
Category:
1
Dumping the database --------------------------------
getShortProductsByName1 --------------------------------
Product type: PROXY
Product category name:
Exception: org.hibernate.LazyInitializationException, Message: could not initialize proxy - no Session
Dumping the database --------------------------------
getLongProductsByName1 --------------------------------
Product type: POJO
Product category name:
category[0]

Here we can see that when accessing the [Categorie.produits] field of a PROXY-type category and the [Produit.categorie] field of a PROXY-type product, an [org.hibernate.LazyInitializationException] is thrown in both cases (lines 7 and 17).