Overview:

Once you learn a few tricks, SpringBoot-JPA and Hibernate object/entity relational mapping (ORM) can really speed up development and extensibility of your next REST project. The native SpringBoot JPA provides most of the CRUD functionality implemented in this example, but the Hibernate project provides the extensibility piece, and a sound framework to jump into advanced ORM capabilities, such as multi-tenancy, full-text search and table auditing. This article will take you through the process of building a simple REST project that manipulates a MySQL database (schema) with three layers of related entities. We shall also implement JSON Views to assist in our controller method output, and primitive DTO PJO objects to simplify the parameters with insert and update methods.

The schema and entity dependencies used in this article and provided in the BitBucket repo have been greatly simplified for clarity, but this should represent the typical ER challenges you face in the enterprise.

NOTE: This is not a NoSQL article, but addresses traditional RDMS challenges with SpringBoot and Hibernate. Future articles will discuss NoSQL and Big Data platform implementations with one of my favorites … MongoDB.

NOTE: Many “defensive programming techniques”, such as using String.ValueOf, has been minimized for clarity and example code snippet readability.

Paul’s Rules of Engagement for SpringBoot-JPA-Hibernate

  1. Schema names, tables and column names and other table objects  should be lower-case and in snake_case.
    > SpringBoot and Hibernate are admittedly opinionated frameworks that work best if you follow their guidelines. Out of the box, lower-case and snake_case works.
    > Other naming conventions can work too, but additional settings are required and out of scope of this article.
    > In either case, a universal consistent naming convention must be used across the application’s entity domain(s). I use lower-case snake_case on all SpringBoot-JPA-Hibernate projects.
  2. Ensure you are starting with sound normalized DB design, at least to Codd’s Third Normal Form (3NF) level of design. This facilitates the relationship definitions within JPA and Hibernate; both SpringBoot and Hibernate assume you are following best practices of Evan’s Domain Driven Design and reduced entities to at least 3NF.
  3. Most of your time will be spent framing the domain entities correctly and mapping your relationships; this is key to any SpringBoot-JPA-Hibernate project.
    > Entity classes are not suppose to be heavy; they should only contain the properties that represent table columns, applicable constructors, getters/setters, hash/equals and a a single debugging toString().
  4. Know and identify (via the @Id annotation) your entity primary keys!
    > SpringBoot-JPA-Hibernate uses the presence of a primary key(s) values in your entity during the saveAndFlush methods to determine if the CRUD operation is an update or insert.
  5. The repository-layer interface classes represent simple RDBMS connectors between your entity class and underlying database.
    > The repository-layer interface classes should contain necessary CRUD DML, and should not include other external business concerns.
    > With Spring-JPA-Hibernate, these repository interfaces extend built in functionality and expose functionality such as paging, saving (saveAndFlush), method-name querying, and other common CRUD operations.
  6. The service-layer classes will provide a conduit between the repository classes and your controller.
    > The service-layer classes are where you include external business rules, logic or tie together various repository CRUD methods into unified transaction methods.

A Word about DTO POJOs

There are several articles addressing the ideology of using separate data transformation object (DTO) POJOs with your controller methods versus just using the defined entity POJOs … and if DTOs are indeed code duplication. In the section below addressing INSERT and UPDATE statements, I am using defined DTO POJOs in the controller methods for the following reasons:

  • Readability of this article, and clarity of coding examples.
  • It has been my experience that methods rarely need a singular entity, but may include several properties across multiple entities that can be used to insert/update a complex entity relationship and fulfill business rules.
  • It is very easy to change DTO design or add a property to a DTO, as compared to the regression testing required to manipulate a data-tied SpringBoot-JPA-Hibernate entity.
  • Many controller methods do not need the entire compliment of core JPA-Hibernate entity properties, and a simple DTO containing only the required parameters provides clarity.
  • When moving entity parameters around, I find it is easier when they are decoupled from underlying RDBMS functions (with DTOs), as opposed to instantiating or moving around entities tied to SpringBoot-JPA-Hibernate, and the possibility of introducing unintended CRUD operations.

Prerequisites:

MySQL Schema and User Setup:

  • The MySQL schema DDL SQL and starter records are provided within the BitBucket repository for this project.
  • It is assumed that you are executing this SQL dump restore script as the MySQL root user on your development instance, as the triggers maintaining the UUIDs and “last_modified” columns are owned by the root@localhost default user.

The easiest method to restore the SQL dump file is to:

  1. Connect to your MySQL instance with a user with DBA equivalent role or privileges to create objects.
  2. Open the associated SQL script in a query window and execute it.

ER Diagram

As shown below, this article will use the a typical company entity relationship of a “customer” and multiple “customer_locations” of different “location_type” and “location_region” categories. This simple example will illustrate most of the relationships you will encounter.

  • As the root of the hierarchy, there is a one-to-many relationship of customer to customer_locations
  • In turn, customer_location has many-to-one relationships with:
    • customer
    • location_region
    • location_type

MySQL User

For this and other MySQL project examples, create a MySQL user that will have the following paulsdevblog% schema privileges, as follows:

  • User Name: paulsdevblog
  • Schema Privileges: paulsdevblog%
    With following Object Rights:

    • SELECT, INSERT, UPDATE, DELETE, EXECUTE, SHOW VIEW

Overview and SpringBoot Application Commentary:

As noted, the code discussed in this article is provided in the PaulsDevBlog BitBucket.

This Project’s POM

let’s take a look at the dependencies that make this whole project work.

  • spring-boot-starter-data-jpa, spring-data-jpa and spring-orm are required for general JPA functionality. I skip the version attribute here, and let SpringBoot decide what curated version is applicable for the version of the SpringBoot framework.
  • All hibernate core dependencies must be at the same release version.
    > As shown below, I am using 5.2.12.Final versions for core, entity-manager and hikaricp.
    > Future articles will address how to configure and setup hikaricp connection pooling for Hibernate.
<?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>com.example</groupId>
    <artifactId>springboothibernaterelationships</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringBootHibernateRelationships</name>
    <description>SpringBoot and Hibernate Relationships</description>

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

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

    <dependencies>
        <!-- Base SpringBoot -->
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web-services</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- Spring Boot will use mysql connector -->
        
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- Spring JPA Prerequisite for Hibernate -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>  
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
        </dependency> 
        
        <!-- Hibernate ORM dependencies -->
        <!-- NOTE that 5.2 separates core and entitymanager, which are both required -->
        <!-- NOTE Make sure version distribution is the same for all core org.hibernate dependencies -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.2.12.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.2.12.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-hikaricp</artifactId>
            <version>5.2.12.Final</version>
        </dependency>
        
        <!-- fasterxml.jackson Core -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <!--  <version>2.9.1</version> -->
        </dependency>
        <!-- fasterxml.jackson.datatype needed for one-to-many with ignore or lazy load  -->
        <!-- NOTE Make sure artifactId matches Hibernate version -->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-hibernate5</artifactId>
            <!--  <version>2.9.1</version> -->
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

This Project’s application.properties

  • Set your micro-service server port as applicable. I used 9003 for this example service.
  • The spring REST controllers and general JSON parsing for this application will assume SNAKE_CASE.
  • Spring will automatically make a “datasource” bean with the spring.datasource properties you set.
    • After the MySQL host and port is defined in the spring.datasource.url, the next string is the database (schema) name your user will initially connect to. I have added a few other MySQL connection parameters that shall be discussed in an associated article.
    • As noted above, I have a paulsdevblog MySQL user for this application’s connection
    • As noted above, we will use the com.mysql.jdbc.Driver class for the connection.
  • As a Spring-JPA-Hibernate application, you can view live generated SQL queries in the logging output console through the spring.jpa.show-sql settings. By default, I have them set to false, but you may wish to enable them to see how Spring-JPA-Hibernate constructs the queries and DML. You can also use MySQL query logging to snoop on SQL being passed back and forth from the application.
#------------REST IP and Port-------
#server.address=192.168.1.101
server.port=9003


#------------REST Field Name JSON spring.jackson-------
spring.jackson.property-naming-strategy=SNAKE_CASE

#-----------Spring JPA-Hikari Datasource----------
spring.datasource.url=jdbc:mysql://localhost:3310/paulsdevblog_hibernate_basic_example?sessionVariables=group_concat_max_len=2048&useSSL=false&connectionCollation=utf8_unicode_ci&characterEncoding=utf8
spring.datasource.username=paulsdevblog
spring.datasource.password=Ilovethissite!
spring.datasource.driver-class-name=com.mysql.jdbc.Driver


#-----------Spring JPA Base Settings----------

spring.jpa.database=default
spring.jpa.generate-ddl=false
#You may wish to set show-sql=true for development debugging
spring.jpa.show-sql=false


#-----------Spring JPA-Hibernate Base Settings----------
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=none

JSON Views

We are going to skip ahead to a discussion about JSON Views. Full support for JSON View annotations was added with the Spring Framework Version 4.2. JSON Views are a simple method to filter the output of serialized parsed JSON from your entities via the jackson library.

A typical use case for JSON Views is implemented in our example. There are “list” methods exposed within the REST controller, and we wish to limit the JSON property output to the entity UUID and a name; however, on another controller method we wish to output JSON that includes the entire entity, with several other “detail” attributes.

As shown below, the EntityViews class is our JSON Views interface class that implements our use case.  The EntityViews interface class includes a simple list of static interfaces, starting with List and gradually extending to Detail and others. There are even interface views to programmatically hide/show related child entities; this, along with Lazy fetch type, combats the Spring-JPA-Hibernate N+1 challenge with complex joins.

package com.paulsdevblog.springboothibernaterelationships.domain.views;

/**
 * Simple interface illustrating use of JSONViews
 *
 * @author paulsdevblog.com
 */
public interface EntityViews {

    /**
     * JSONViews identifier label for entity attributes that are to be
     * included in simple lists of entities
     */
    public static interface List {}
    
    /**
     * JSONViews identifier label for entity details, and inherits any
     * entities annotated with EntityViews.List
     */
    public static interface Details extends List {}
    
    /**
     * JSONViews identifier label for entity and parent relationships, and inherits any
     * entities annotated with EntityViews.List and EntityViews.Details
     */
    public static interface EntityParents extends Details {}
    
    /**
     * JSONViews identifier label for entity and child relationships, and inherits any
     * entities annotated with EntityViews.List and EntityViews.Details
     */
    public static interface EntityChildren extends Details {}


}

You choose how you want to group either your getters and/or properties for display by adding the following @JsonView annotation. As an array, you can apply multiple JSON View interface tags.

  • In the wild, I have used JSON Views to make a supplemental “LegacyIds” interface tag that I can selectively apply to generate POJO(s) targeted consumption by a  legacy systems and external APIs.
  • This is also a great way to expose new properties for new feature end-points, without upsetting existing endpoint POJOs.
    @JsonView( { EntityViews.Details.class } )
    public String getLocationRegionUuid() {
        return locationRegionUuid;
    }

Simple Parent Entity

We shall start with the LocationType (location_type table) entity; as shown in the ER above, it has a one-to-many relationship with the CustomerLocation (customer_location table).

  • The @Entity annotation defines that this class is an ORM entity representation of a table.
  • The @Table annotation defines the table name this ORM entity shall represent, and also provides additional settings for constraints. In this case, we re-state the table index constraint that the location_type_uuid is a unique column.
  • The @XmlRootElement annotation is for legacy support of XML REST output, if we choose to provide this.
  • Yes, the JSON specification states that property order is not required and may be random; however, I like to group my properties in a manner that makes sense other than alphabetical. The @JsonPropertyOrder annotation provides an array of property names that specifies the order the Jackson library uses to generate JSON output.
  • The first property is required for serialization; however we would rarely use this for output. I like to mark this with @JsonIgnore for good measure.
  • The @Basic(optional = false) and @NotNull are related to the input validation of the property. Yes, there is redundancy here that we hope shall be resolved in future stack revisions.
    • The @Basic annotation is sometimes viewed as a legacy annotation, and used if the table is to be created by Spring-JPA at application start. This is important for in-memory databases that would be regenerated during each application start up.
    • The @NotNull annotation is the runtime POJO value validation, as detailed in the lastest Java JSR303 specifications. This can be easily tied to controller method pre-validation, that is out of scope of this article.
    • The @Column( … nullable = false ) annotation is also used for Hibernate table and column mapping, and for DDL, but tends to be platform specific.
    • For now, until later Spring-JPA-Hibernate versions provide more clarity, I include all three methods to define if a NULL value is acceptable.
  • @Size(min = 1, max = 50) is also used for property value validation, as detailed in the lastest Java JSR303 specifications.
  • The @OneToMany annotation shall be addressed in the next section, as it works in concert with the child entity @ManyToOne annotation.
  • Note how the JSON views interface annotations are applied to the getters, and provide a JSON View group that can be associated with methods and controller endpoints.
package com.paulsdevblog.springboothibernaterelationships.domain.model;

import com.fasterxml.jackson.annotation.*;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.ZoneId;
import java.time.ZonedDateTime;

import java.util.Date;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Collections;
import java.util.Set;
import java.util.HashSet;
import java.util.HashMap;

import javax.persistence.*;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;

import org.hibernate.annotations.GenericGenerator;

import org.springframework.core.annotation.*;
import org.springframework.beans.factory.annotation.*;

import com.paulsdevblog.springboothibernaterelationships.domain.views.EntityViews;
import com.paulsdevblog.utility.DateTimeConversion;

/**
 * Simple Spring-JPA-Hibernate entity example that represents table 
 * customer_location
 * 
 * @author PaulsDevBlog.com
 */
@Entity
@Table(name = "customer_location", uniqueConstraints = {
    @UniqueConstraint( columnNames = {"customer_location_code","customer_location_ref" } )
})
@XmlRootElement
@JsonPropertyOrder(
    { 
        "customerLocationRef",
        "customerLocationUuid", 

        "customerLocationCode",
        
        "customerUuid",
        "locationTypeCode",
        "locationRegionCode",
        
        "lastModifiedAction", 
        "lastModifiedUser", 
           
        "LastModifiedDt", 
        "LastModifiedDtAsIso8601", 
        "LastModifiedDtAsEpochSecond"
    }
)
public class CustomerLocation {
    
    @JsonIgnore
    private static final long serialVersionUID = 1L;
    
    //Yes there is both a customer_location_ref and customer_location_uuid, as 
    //MySQL aggregate functions (MIN,MAX etc.) only work with numeric key column
    //This key is not used for any other purposes other than MySQL aggregation
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @Column(name = "customer_location_ref", nullable = false)
    private Long customerLocationRef;
    
    //This is the priamry key we use for all joins and UUID for REST endpoints
    @Id
    @GeneratedValue(generator = "uuid2")
    @GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator" )
    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 50)
    @Column(name = "customer_location_uuid", nullable = false, length = 50)
    private String customerLocationUuid;

    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 25)
    @Column(name = "customer_location_code", nullable = false, length = 50)
    private String customerLocationCode;
    
    //customer relationship
    
    @Basic(optional = false)
    @NotNull
    @Size(min = 5, max = 50)
    @Column(name = "customer_uuid", nullable = false, length = 50)
    private String customerUuid;
    
    @JoinColumn(name = "customer_uuid", referencedColumnName = "customer_uuid", updatable = false, insertable = false)
    @ManyToOne( fetch = FetchType.LAZY )
    private Customer customer;

    //location_type relationship
    
    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 25)
    @Column(name = "location_type_code", nullable = false, length = 25)
    private String locationTypeCode;
    
    @JoinColumn(name = "location_type_code", referencedColumnName = "location_type_code", updatable = false, insertable = false)
    @ManyToOne( fetch = FetchType.LAZY )
    private LocationType locationType;
    
    //location_region relationship
    
    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 25)
    @Column(name = "location_region_code", nullable = false, length = 25)
    private String locationRegionCode;
    
    @JoinColumn(name = "location_region_code", referencedColumnName = "location_region_code", updatable = false, insertable = false)
    @ManyToOne( fetch = FetchType.LAZY )
    private LocationRegion locationRegion;
    
    //Last modified by
    
    @Size(max = 255)
    @Column(name = "last_modified_user", length = 255)
    private String lastModifiedUser;
    
    @Column(name = "last_modified_dt")
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDt;
    
    @Size(max = 25)
    @Column(name = "last_modified_action", length = 25)
    private String lastModifiedAction;

    public CustomerLocation() {}

    public CustomerLocation(
        Long customerLocationRef, 
        String customerLocationUuid, 
        String customerLocationCode, 
        String customerUuid,
        String locationTypeCode, 
        String locationRegionCode, 
        String lastModifiedUser, 
        Date lastModifiedDt, 
        String lastModifiedAction
    ) {
        this.customerLocationRef = customerLocationRef;
        this.customerLocationUuid = customerLocationUuid;
        this.customerLocationCode = customerLocationCode;
        this.customerUuid = customerUuid;
        this.locationTypeCode = locationTypeCode;
        this.locationRegionCode = locationRegionCode;
        this.lastModifiedUser = lastModifiedUser;
        this.lastModifiedDt = lastModifiedDt;
        this.lastModifiedAction = lastModifiedAction;
    }

    @JsonView( { EntityViews.Details.class } )
    public Long getCustomerLocationRef() {
        return customerLocationRef;
    }

    public void setCustomerLocationRef(Long customerLocationRef) {
        this.customerLocationRef = customerLocationRef;
    }

    @JsonView( { EntityViews.List.class } )
    public String getCustomerLocationUuid() {
        return customerLocationUuid;
    }

    public void setCustomerLocationUuid(String customerLocationUuid) {
        this.customerLocationUuid = customerLocationUuid;
    }

    @JsonView( { EntityViews.List.class } )
    public String getCustomerLocationCode() {
        return customerLocationCode;
    }

    public void setCustomerLocationCode(String customerLocationCode) {
        this.customerLocationCode = customerLocationCode;
    }

    //customer relationships
    
    @JsonView( { EntityViews.List.class } )
    public String getCustomerUuid() {
        return customerUuid;
    }

    public void setCustomerUuid(String customerUuid) {
        this.customerUuid = customerUuid;
    }

    @JsonView( { EntityViews.EntityParents.class } )
    public Customer getCustomer() {
        return customer;
    }

    public void setCustomer(Customer customer) {
        this.customer = customer;
    }

    //location_type relationships

    @JsonView( { EntityViews.List.class } )
    public String getLocationTypeCode() {
        return locationTypeCode;
    }

    public void setLocationTypeCode(String locationTypeCode) {
        this.locationTypeCode = locationTypeCode;
    }

    @JsonView( { EntityViews.EntityParents.class } )
    public LocationType getLocationType() {
        return locationType;
    }

    public void setLocationType(LocationType locationType) {
        this.locationType = locationType;
    }
    
    //location_region relationships
    
    @JsonView( { EntityViews.List.class } )
    public String getLocationRegionCode() {
        return locationRegionCode;
    }

    public void setLocationRegionCode(String locationRegionCode) {
        this.locationRegionCode = locationRegionCode;
    }

    @JsonView( { EntityViews.EntityParents.class } )
    public LocationRegion getLocationRegion() {
        return locationRegion;
    }

    public void setLocationRegion(LocationRegion locationRegion) {
        this.locationRegion = locationRegion;
    }

    @JsonView( { EntityViews.Details.class } )
    public String getLastModifiedUser() {
        return lastModifiedUser;
    }

    public void setLastModifiedUser(String lastModifiedUser) {
        this.lastModifiedUser = lastModifiedUser;
    }

    @JsonView( { EntityViews.Details.class } )
    public Date getLastModifiedDt() {
        return lastModifiedDt;
    }
    
    @JsonView( { EntityViews.Details.class } )
    public String getLastModifiedDtAsIso8601() {
        return DateTimeConversion.dateToISO8601( lastModifiedDt );
    }
    
    @JsonView( { EntityViews.Details.class } )
    public long getLastModifiedDtAsEpochSecond() {
        return DateTimeConversion.dateToEpochSecond( lastModifiedDt );
    }

    public void setLastModifiedDt(Date lastModifiedDt) {
        this.lastModifiedDt = lastModifiedDt;
    }

    @JsonView( { EntityViews.Details.class } )
    public String getLastModifiedAction() {
        return lastModifiedAction;
    }

    public void setLastModifiedAction(String lastModifiedAction) {
        this.lastModifiedAction = lastModifiedAction;
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 97 * hash + Objects.hashCode(this.customerLocationRef);
        hash = 97 * hash + Objects.hashCode(this.customerLocationUuid);
        hash = 97 * hash + Objects.hashCode(this.customerLocationCode);
        hash = 97 * hash + Objects.hashCode(this.customerUuid);
        hash = 97 * hash + Objects.hashCode(this.locationTypeCode);
        hash = 97 * hash + Objects.hashCode(this.locationRegionCode);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final CustomerLocation other = (CustomerLocation) obj;
        if (!Objects.equals(this.customerLocationUuid, other.customerLocationUuid)) {
            return false;
        }
        if (!Objects.equals(this.customerLocationCode, other.customerLocationCode)) {
            return false;
        }
        if (!Objects.equals(this.customerUuid, other.customerUuid)) {
            return false;
        }
        if (!Objects.equals(this.locationTypeCode, other.locationTypeCode)) {
            return false;
        }
        if (!Objects.equals(this.locationRegionCode, other.locationRegionCode)) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "CustomerLocation{" 
                
                    + "customerLocationRef=" + customerLocationRef 
                    + ", customerLocationUuid=" + customerLocationUuid 
                
                    + ", customerLocationCode=" + customerLocationCode 
                
                    + ", customerUuid=" + customerUuid 
                    + ", locationTypeCode=" + locationTypeCode 
                    + ", locationRegionCode=" + locationRegionCode 
                
                
                    + ", lastModifiedUser=" + String.valueOf( lastModifiedUser )
                    + ", lastModifiedDtAsIso8601=" + this.getLastModifiedDtAsIso8601() 
                    + ", lastModifiedDtAsEpochSecond=" + String.valueOf( this.getLastModifiedDtAsEpochSecond() )
                    + ", lastModifiedAction=" + String.valueOf( this.lastModifiedAction ) 
                
                + '}';
    }

}

Complex Multiple-Join Entity

The following is the CustomerLocation (customer_location table) entity represents a complex many-to-one join table, as defined in the above ER.

  • Note there is both a customerLocationRef ( customer_location_ref table column) and customerLocationUuid ( customer_location_uuid table column) that use different identity generators.
    > The “ref” column is a SQL performance trick for aggregate functions (MAX, MIN, COUNT, etc.) that require or perform substantially better with numeric data types.
    > The “UUID” column is for external joins, REST publishing, and uniqueness across multi-tenant systems and instances.
  • Looking at previous LocationType class code, you see the @OneToMany annotation that points to a FetchType.LAZY relationship established in the CustomerLocation entity (and table via foreign key relationships).
  • This many-to-one join properties establish the relationships to the other parent entity/tables. These relationship properties are included as pairs:
    • As I wish to output and pass around the codes and UUIDs, the first property is the distinctive foreign key property (e.g. String locationTypeCode)
    • The associated serialized parent entity property follows the key, with @JoinColumn and @ManyToOne annotations (e.g. LocationType locationType).
      > This is the property name reference used by the @OneToMany annotation in the LocationType entity.
    • The  foreign key property has the applicable setter/getter with
      @JsonView( { EntityViews.List.class } )
    • The associated parent entity setter/getter has a separate @JsonView or a @JSONIgnore to help alleviate N+1 issues and double SQL fetching.
package com.paulsdevblog.springboothibernaterelationships.domain.model;

import com.fasterxml.jackson.annotation.*;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.ZoneId;
import java.time.ZonedDateTime;

import java.util.Date;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Collections;
import java.util.Set;
import java.util.HashSet;
import java.util.HashMap;

import javax.persistence.*;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;

import org.hibernate.annotations.GenericGenerator;

import org.springframework.core.annotation.*;
import org.springframework.beans.factory.annotation.*;

import com.paulsdevblog.springboothibernaterelationships.domain.views.EntityViews;
import com.paulsdevblog.utility.DateTimeConversion;

/**
 * Simple Spring-JPA-Hibernate entity example that represents table 
 * customer_location
 * 
 * @author PaulsDevBlog.com
 */
@Entity
@Table(name = "customer_location", uniqueConstraints = {
    @UniqueConstraint( columnNames = {"customer_location_code","customer_location_ref" } )
})
@XmlRootElement
@JsonPropertyOrder(
    { 
        "customerLocationRef",
        "customerLocationUuid", 

        "customerLocationCode",
        
        "customerUuid",
        "locationTypeCode",
        "locationRegionCode",
        
        "lastModifiedAction", 
        "lastModifiedUser", 
           
        "LastModifiedDt", 
        "LastModifiedDtAsIso8601", 
        "LastModifiedDtAsEpochSecond"
    }
)
public class CustomerLocation {
    
    @JsonIgnore
    private static final long serialVersionUID = 1L;
    
    //Yes there is both a customer_location_ref and customer_location_uuid, as 
    //MySQL aggregate functions (MIN,MAX etc.) only work with numeric key column
    //This key is not used for any other purposes other than MySQL aggregation
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @Column(name = "customer_location_ref", nullable = false)
    private Long customerLocationRef;
    
    //This is the priamry key we use for all joins and UUID for REST endpoints
    @Id
    @GeneratedValue(generator = "uuid2")
    @GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator" )
    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 50)
    @Column(name = "customer_location_uuid", nullable = false, length = 50)
    private String customerLocationUuid;

    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 25)
    @Column(name = "customer_location_code", nullable = false, length = 50)
    private String customerLocationCode;
    
    //customer relationship
    
    @Basic(optional = false)
    @NotNull
    @Size(min = 5, max = 50)
    @Column(name = "customer_uuid", nullable = false, length = 50)
    private String customerUuid;
    
    @JoinColumn(name = "customer_uuid", referencedColumnName = "customer_uuid", updatable = false, insertable = false)
    @ManyToOne( fetch = FetchType.LAZY )
    private Customer customer;

    //location_type relationship
    
    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 25)
    @Column(name = "location_type_code", nullable = false, length = 25)
    private String locationTypeCode;
    
    @JoinColumn(name = "location_type_code", referencedColumnName = "location_type_code", updatable = false, insertable = false)
    @ManyToOne( fetch = FetchType.LAZY )
    private LocationType locationType;
    
    //location_region relationship
    
    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 25)
    @Column(name = "location_region_code", nullable = false, length = 25)
    private String locationRegionCode;
    
    @JoinColumn(name = "location_region_code", referencedColumnName = "location_region_code", updatable = false, insertable = false)
    @ManyToOne( fetch = FetchType.LAZY )
    private LocationRegion locationRegion;
    
    //Last modified by
    
    @Size(max = 255)
    @Column(name = "last_modified_user", length = 255)
    private String lastModifiedUser;
    
    @Column(name = "last_modified_dt")
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDt;
    
    @Size(max = 25)
    @Column(name = "last_modified_action", length = 25)
    private String lastModifiedAction;

    public CustomerLocation() {}

    public CustomerLocation(
        Long customerLocationRef, 
        String customerLocationUuid, 
        String customerLocationCode, 
        String customerUuid,
        String locationTypeCode, 
        String locationRegionCode, 
        String lastModifiedUser, 
        Date lastModifiedDt, 
        String lastModifiedAction
    ) {
        this.customerLocationRef = customerLocationRef;
        this.customerLocationUuid = customerLocationUuid;
        this.customerLocationCode = customerLocationCode;
        this.customerUuid = customerUuid;
        this.locationTypeCode = locationTypeCode;
        this.locationRegionCode = locationRegionCode;
        this.lastModifiedUser = lastModifiedUser;
        this.lastModifiedDt = lastModifiedDt;
        this.lastModifiedAction = lastModifiedAction;
    }

    @JsonView( { EntityViews.Details.class } )
    public Long getCustomerLocationRef() {
        return customerLocationRef;
    }

    public void setCustomerLocationRef(Long customerLocationRef) {
        this.customerLocationRef = customerLocationRef;
    }

    @JsonView( { EntityViews.List.class } )
    public String getCustomerLocationUuid() {
        return customerLocationUuid;
    }

    public void setCustomerLocationUuid(String customerLocationUuid) {
        this.customerLocationUuid = customerLocationUuid;
    }

    @JsonView( { EntityViews.List.class } )
    public String getCustomerLocationCode() {
        return customerLocationCode;
    }

    public void setCustomerLocationCode(String customerLocationCode) {
        this.customerLocationCode = customerLocationCode;
    }

    //customer relationships
    
    @JsonView( { EntityViews.List.class } )
    public String getCustomerUuid() {
        return customerUuid;
    }

    public void setCustomerUuid(String customerUuid) {
        this.customerUuid = customerUuid;
    }

    @JsonView( { EntityViews.EntityParents.class } )
    public Customer getCustomer() {
        return customer;
    }

    public void setCustomer(Customer customer) {
        this.customer = customer;
    }

    //location_type relationships

    @JsonView( { EntityViews.List.class } )
    public String getLocationTypeCode() {
        return locationTypeCode;
    }

    public void setLocationTypeCode(String locationTypeCode) {
        this.locationTypeCode = locationTypeCode;
    }

    @JsonView( { EntityViews.EntityParents.class } )
    public LocationType getLocationType() {
        return locationType;
    }

    public void setLocationType(LocationType locationType) {
        this.locationType = locationType;
    }
    
    //location_region relationships
    
    @JsonView( { EntityViews.List.class } )
    public String getLocationRegionCode() {
        return locationRegionCode;
    }

    public void setLocationRegionCode(String locationRegionCode) {
        this.locationRegionCode = locationRegionCode;
    }

    @JsonView( { EntityViews.EntityParents.class } )
    public LocationRegion getLocationRegion() {
        return locationRegion;
    }

    public void setLocationRegion(LocationRegion locationRegion) {
        this.locationRegion = locationRegion;
    }

    @JsonView( { EntityViews.Details.class } )
    public String getLastModifiedUser() {
        return lastModifiedUser;
    }

    public void setLastModifiedUser(String lastModifiedUser) {
        this.lastModifiedUser = lastModifiedUser;
    }

    @JsonView( { EntityViews.Details.class } )
    public Date getLastModifiedDt() {
        return lastModifiedDt;
    }
    
    @JsonView( { EntityViews.Details.class } )
    public String getLastModifiedDtAsIso8601() {
        return DateTimeConversion.dateToISO8601( lastModifiedDt );
    }
    
    @JsonView( { EntityViews.Details.class } )
    public long getLastModifiedDtAsEpochSecond() {
        return DateTimeConversion.dateToEpochSecond( lastModifiedDt );
    }

    public void setLastModifiedDt(Date lastModifiedDt) {
        this.lastModifiedDt = lastModifiedDt;
    }

    @JsonView( { EntityViews.Details.class } )
    public String getLastModifiedAction() {
        return lastModifiedAction;
    }

    public void setLastModifiedAction(String lastModifiedAction) {
        this.lastModifiedAction = lastModifiedAction;
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 97 * hash + Objects.hashCode(this.customerLocationRef);
        hash = 97 * hash + Objects.hashCode(this.customerLocationUuid);
        hash = 97 * hash + Objects.hashCode(this.customerLocationCode);
        hash = 97 * hash + Objects.hashCode(this.customerUuid);
        hash = 97 * hash + Objects.hashCode(this.locationTypeCode);
        hash = 97 * hash + Objects.hashCode(this.locationRegionCode);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final CustomerLocation other = (CustomerLocation) obj;
        if (!Objects.equals(this.customerLocationUuid, other.customerLocationUuid)) {
            return false;
        }
        if (!Objects.equals(this.customerLocationCode, other.customerLocationCode)) {
            return false;
        }
        if (!Objects.equals(this.customerUuid, other.customerUuid)) {
            return false;
        }
        if (!Objects.equals(this.locationTypeCode, other.locationTypeCode)) {
            return false;
        }
        if (!Objects.equals(this.locationRegionCode, other.locationRegionCode)) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "CustomerLocation{" 
                
                    + "customerLocationRef=" + customerLocationRef 
                    + ", customerLocationUuid=" + customerLocationUuid 
                
                    + ", customerLocationCode=" + customerLocationCode 
                
                    + ", customerUuid=" + customerUuid 
                    + ", locationTypeCode=" + locationTypeCode 
                    + ", locationRegionCode=" + locationRegionCode 
                
                
                    + ", lastModifiedUser=" + String.valueOf( lastModifiedUser )
                    + ", lastModifiedDtAsIso8601=" + this.getLastModifiedDtAsIso8601() 
                    + ", lastModifiedDtAsEpochSecond=" + String.valueOf( this.getLastModifiedDtAsEpochSecond() )
                    + ", lastModifiedAction=" + String.valueOf( this.lastModifiedAction ) 
                
                + '}';
    }

}

Hey where are the hard-coded SQL queries? Hello Spring-JPA-Hibernate Method-name Query Language

Looking at the Repository interface classes, you may have noticed something missing … hard-coded SQL queries, updates, and insert statements. This example uses the Spring-JPA-Hibernate method-name query structure.

Using the Spring-JPA Reference guidelines, basic SQL statements can be generated through including entity property names and key words in camel case within the method name. for example, the method:

public List<LocationRegion> findByLocationRegionUuidContainsAllIgnoreCaseOrderByLocationRegionUuidAsc( @Param("locationRegionUuid") String locationRegionUuid );
  • For simple, singular entity queries, the Spring-JPA method name query format is great.
  • As noted in the reference guidelines, the trigger tagging this method as a Spring-JPA method-name query is the method name prefix “findBy
  • The next part is the parameter properties used for the query filter, in our example, this is the LocationRegionUuid field.
    • Multiple properties can be separated with “And“.
    • These are referenced by the @Param annotation in the method’s respective parameters.
  • If you want this to be a wild-card SQL LIKE, then follow the parameter property names with “Contains
  • The next keywords are “AllIgnoreCase” which will ignore the case of the query parameter(s) when finding matches
  • The next keywords are “OrderBy“, followed by the entity property names to be used for ordering results.
  • The last keyword is “Asc” which is ascending order.

The above link provides many more examples and usages. This feature alone makes the the effort of standing up a Spring-JPA-Hibernate application worth it.

INSERT and UPDATE Methods with Spring-JPA-Hibernate

There are as many techniques for INSERT/UPDATE/DELETE with Spring-JPA-Hibernate as there are articles about Hibernate.

As shown below, for INSERT, I have had great success with the following pattern:

  1. Create a new transitory entity object, in this case CustomerLocation
  2. Set all identity key properties to NULL, as Spring-JPA and Hibernate access the values in these fields to determine if the operation is an INSERT or UPDATE.
    > In the case of CustomerLocation, set both CustomerLocationRef and CustomerLocationUuid to NULL
  3. Use the DTO getters as a source for remaining attributes you wish to change.
    > In future articles, there are validation input annotations that can also be used to selectively update fields.
  4. Perform a SaveAndFlush to “persist” data changes to the RDBMS.
    @Override
    public CustomerLocation insertCustomerLocation( CustomerLocationDTO customerLocationDTO ) throws ServiceLayerDataException{
        try {

            //Setup Entity for Hibernate JPA action
            CustomerLocation customerLocation = new CustomerLocation();
            
            //Null keys to ensure Hibernate JPA treats this and Entity INSERT
            customerLocation.setCustomerLocationRef( null );
            customerLocation.setCustomerLocationUuid( null );

            customerLocation.setCustomerLocationCode( customerLocationDTO.getCustomerLocationCode() );  
            
            customerLocation.setCustomerUuid( customerLocationDTO.getCustomerUuid() );  
            customerLocation.setLocationTypeCode( customerLocationDTO.getLocationTypeCode() );  
            customerLocation.setLocationRegionCode(customerLocationDTO.getLocationRegionCode() );  
            
            customerLocation.setLastModifiedUser( customerLocationDTO.getLastModifiedUser() );  

            //Hibernate JPA save and flush (commit) persist
            CustomerLocation returnCustomerLocation = this.customerLocationRepository.saveAndFlush( customerLocation );

            //Return modified Entity
            return returnCustomerLocation;

        } catch (Exception ex){
            
            String currentMethod =  String.valueOf( new Object(){}.getClass().getEnclosingMethod().getName() ).trim() ;
            String currentExceptionMsg = String.valueOf( ex.getMessage() );
            
            logger.error( currentMethod + " ExceptionMsg: " + currentExceptionMsg );
            
            throw new ServiceLayerDataException( currentExceptionMsg, currentMethod );
            
        }
    }


As shown below, for UPDATE, I use a similar pattern:

  1. Create a new transitory entity object, in this case CustomerLocation, through a lookup of the UUID.
    > This provides the current entity values; the presence of the valid identifier property values tell Spring-JPA-Hibernate that this is an UPDATE operation.
    > If the identifier properties were NULL, Spring-JPA-Hibernate would assume INSERT.
  2. Use the DTO getters as a source for remaining attributes you wish to change.
    > In future articles, there are validation input annotations that can also be used to selectively update fields.
  3. Perform a SaveAndFlush to “persist” data changes to the RDBMS.
    @Override
    public CustomerLocation updateCustomerLocation( CustomerLocationDTO customerLocationDTO ) throws ServiceLayerDataException{
        try {

            //Get keys from DTO
            String customerLocationUuid = String.valueOf( customerLocationDTO.getCustomerLocationUuid());

            //Get current record with Ref and UUID keys or Hibernate JPA will try to set ref key to null, and update will fail
            CustomerLocation customerLocation = this.getOneByCustomerLocationUuid(customerLocationUuid );
            
            if ( customerLocation == null ) { 
                throw new ServiceLayerDataException( "Record not Found. Cannot Update.", String.valueOf( new Object(){}.getClass().getEnclosingMethod().getName() ).trim() );
            } 

            customerLocation.setCustomerLocationCode( customerLocationDTO.getCustomerLocationCode() );  
            
            customerLocation.setCustomerUuid( customerLocationDTO.getCustomerUuid() );  
            customerLocation.setLocationTypeCode( customerLocationDTO.getLocationTypeCode() );  
            customerLocation.setLocationRegionCode(customerLocationDTO.getLocationRegionCode() );  
            
            customerLocation.setLastModifiedUser( customerLocationDTO.getLastModifiedUser() );  

            //Hibernate JPA save and flush (commit) persist
            CustomerLocation returnCustomerLocation = this.customerLocationRepository.saveAndFlush( customerLocation );

            //Return modified Entity
            return returnCustomerLocation;

        } catch (Exception ex){
            
            String currentMethod =  String.valueOf( new Object(){}.getClass().getEnclosingMethod().getName() ).trim() ;
            String currentExceptionMsg = String.valueOf( ex.getMessage() );
            
            logger.error( currentMethod + " ExceptionMsg: " + currentExceptionMsg );
            
            throw new ServiceLayerDataException( currentExceptionMsg, currentMethod );
            
        }
    }
    
}

Running the REST Service

  • Do a Build-Clean followed by run to start up your microservice.
  • I use Postman to test and model REST APIs. It is a great tool.

Running the REST Service: Simple Entity: LocationTypes

  • For a list of LocationTypes,
    GET http://localhost:9003/locationtype/list

Running the REST Service: Parent Entity: Customer

  • Using Postman, to retrieve a list of Customer entities in the RDBMS:
    GET http://localhost:9003/customer/list
  • For details of a specific Customer entity:
    GET http://localhost:9003/customer/uuid/[customer_uuid]
  • For a complex POJO of a specific Customer entity with all relationship objects,
    GET http://localhost:9003/customer/relationships/[customer_uuid]
  • To INSERT new Customer entities, send the CustomerDTO POJO attributes:
    POST http://localhost:9003/customer/

  • To UPDATE existing Customer entities, send the CustomerDTO POJO attributes:
    PUT http://localhost:9003/customer/uuid/[your_customer_uuid]
    NOTE:
    Include the applicable Customer entity customer_uuid ID key

Running the REST Service: Many-to-One Join Entity: CustomerLocation

  • Using Postman, to retrieve a list of CustomerLocation entities in the RDBMS:
    GET http://localhost:9003/customerlocation/list
  • For a complex POJO of a specific CustomerLocation entity with all relationship objects,
    GET http://localhost:9003/customerlocation/relationships/[customer_location_uuid] 
  • To INSERT new CustomerLocation entities, send the CustomerLocationDTO POJO attributes:
    POST http://localhost:9003/customerlocation/
  • To UPDATE existing CustomerLocation entities, send the CustomerLocationDTO POJO attributes:
    PUT http://localhost:9003/customerlocation/uuid/[customer_location_uuid]
    NOTE:
    Include the applicable customer entity customer_location_uuid ID key

Extending this Example

Other articles on this blog can be referenced to further extend and expand this example:

 

BitBucket Repo and Project Code

This article and associated code was a tad long, but provided a comprehensive overview of SpringBoot-JPA-Hibernate. Future articles will discuss other great Hibernate features. Thanks, and please Link/Like!  paulsDevBlog.End();

References: