Skip to content

Latest commit

 

History

History
580 lines (479 loc) · 21.5 KB

hibernate.md

File metadata and controls

580 lines (479 loc) · 21.5 KB

Example: Hibernate 6.1

In this section, we will demonstrate the new features in a simple Java SE application with Hibernate.

Creating a Simple Java Project

There are a lot of approaches to prepare a simple Java project skeleton. The simplest is using your favorite IDE to generate one for you.

Alternatively, you can create it using Maven Quickstart archetype.

Open a terminal, execute the following command.

mvn archetype:generate
    -DarchetypeGroupId=org.apache.maven.archetypes
    -DarchetypeArtifactId=maven-archetype-quickstart
    -DarchetypeVersion=1.4

The interactive mode will guide you setup the project info, such as groupId, artifact, version etc.

In this example project, we use com.example as groupId, and demo as artifactId. Then confirm and begin to generate the project source codes.

After it is done, open the project in a Java IDE such as IntelliJ IDEA(Community Edition is free), or Eclipse Java/Java EE bundle, or NetBeans IDE, or a simple text editor, eg. VS Code.

Open the project pom.xml, add Hibernate 6.1, and JUnit etc. into project dependencies, and setup Maven compiler plugin to use the latest LTS Java 17 to compile the source codes.

The final pom.xml looks like the following.

<?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>org.example</groupId>
    <artifactId>hibernate6</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>jakartaee10-sandbox-parent</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>..</relativePath>
    </parent>

    <name>hibernate6</name>
    <description>Jakarta EE 10 Sandbox: Hibernate 6/JPA 3.1 example</description>

    <properties>
        <maven.compiler.release>17</maven.compiler.release>

        <!-- requires 6.1.2.Final or higher -->
        <hibernate.version>6.1.4.Final</hibernate.version>
        <h2.version>2.1.214</h2.version>

        <!-- test deps -->
        <junit-jupiter.version>5.9.1</junit-jupiter.version>
        <assertj-core.version>3.23.1</assertj-core.version>

        <slf4j.version>2.0.3</slf4j.version>
        <logback.version>1.4.4</logback.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.hibernate.orm</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>${hibernate.version}</version>
        </dependency>
        <dependency>
            <groupId>jakarta.persistence</groupId>
            <artifactId>jakarta.persistence-api</artifactId>
            <version>3.1.0</version>
        </dependency>

        <!-- H2 Database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>${h2.version}</version>
        </dependency>

        <!-- logging with logback -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>${logback.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>

        <!-- test dependencies -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${junit-jupiter.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>${assertj-core.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate.orm</groupId>
            <artifactId>hibernate-testing</artifactId>
            <version>${hibernate.version}</version>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M7</version>
            </plugin>
        </plugins>
    </build>
</project>

NOTE: To share common resources for all feature-based projects, create a parent POM to centralize the common configurations in one place, check the parent pom.xml file.

In this example project, we use H2 embedded database for test purpose. Hibernate 6.1 implements the features of Jakarta Persistence 3.1, but it includes a Jakarta Persistence 3.0 API in the transitive dependency tree.

To use Jakarta Persistence 3.1 API, we have to add jakarta.persistence:jakarta.persistence-api 3.1 explicitly.

In the src/main/resources/META-INF, add a new file named persistence.xml to activate JPA.

<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_1.xsd"
             version="3.1">

    <persistence-unit name="defaultPU" transaction-type="RESOURCE_LOCAL">

        <description>Hibernate test case template Persistence Unit</description>
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

        <exclude-unlisted-classes>false</exclude-unlisted-classes>

        <properties>
            <property name="hibernate.archive.autodetection" value="class, hbm"/>

            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.connection.driver_class" value="org.h2.Driver"/>
            <property name="hibernate.connection.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1"/>
            <property name="hibernate.connection.username" value="sa"/>

            <property name="hibernate.connection.pool_size" value="5"/>

            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.hbm2ddl.auto" value="create-drop"/>

            <property name="hibernate.max_fetch_depth" value="5"/>

            <property name="hibernate.cache.region_prefix" value="hibernate.test"/>
            <property name="hibernate.cache.region.factory_class"
                      value="org.hibernate.testing.cache.CachingRegionFactory"/>

            <!--NOTE: hibernate.jdbc.batch_versioned_data should be set to false when testing with Oracle-->
            <property name="hibernate.jdbc.batch_versioned_data" value="true"/>

            <property name="jakarta.persistence.validation.mode" value="NONE"/>
            <property name="hibernate.service.allow_crawling" value="false"/>
            <property name="hibernate.session.events.log" value="true"/>
        </properties>

    </persistence-unit>
</persistence>

We use logback as the logging framework in this project. In the src/main/resources, add a logback.xml to configure logback.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <property name="LOGS" value="./logs"/>

    <appender name="Console"
              class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>
                %green(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{1.}): %msg%n%throwable
            </Pattern>
        </layout>
    </appender>

    <appender name="RollingFile"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOGS}/app.log</file>
        <encoder
                class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
        </encoder>

        <rollingPolicy
                class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- rollover daily and when the file reaches 10 MegaBytes -->
            <fileNamePattern>${LOGS}/archived/app-%d{yyyy-MM-dd}.%i.log
            </fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
    </appender>

    <!-- LOG everything at INFO level -->
    <root level="info">
        <appender-ref ref="RollingFile"/>
        <appender-ref ref="Console"/>
    </root>

    <!-- Debug hibernate SQL,  see: https://thorben-janssen.com/hibernate-logging-guide/ -->
    <logger name="org.hibernate.SQL" level="DEBUG"/>
    <logger name="org.hibernate.type.descriptor.sql" level="trace"/>

    <!-- Custom debug level for the application code -->
    <logger name="com.example" level="debug" additivity="false">
        <appender-ref ref="RollingFile"/>
        <appender-ref ref="Console"/>
    </logger>
</configuration>

We set org.hibernate.SQL logging level to DEBUG and org.hibernate.type.descriptor.sql to trace, it will help you to dig into the Hibernate generated SQL at runtime.

UUID Basic Type

Create a simple Entity.

@Entity
public class Person {
    @Id
    @Column(name = "id", nullable = false)
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    private String name;
    private int age = 30;

    public Person() {
    }

    public Person(String name, int age) {
        assert age > 0;
        this.name = name;
        this.age = age;
        this.birthDate = LocalDateTime.now().minusYears(this.age);
    }

    // getters and setters
    // override equals and hashCode
}

An entity class is annotated with an @Entity, optionally you can specify the entity name and add table definition with an extra @Table annotation.

Here we defined a UUID type ID, and use the new UUID generation strategy introduced in JPA 3.1.

JPA requires an Entity should includes a no-arguments constructor, if you declare another constructor with a few arguments, you should declare this no-arguments constructor explicitly.

Create a simple JUnit test to verify if the UUID type working as expected.

class PersonUUIDTest {
    private static final Logger log = LoggerFactory.getLogger(PersonUUIDTest.class);

    private EntityManagerFactory entityManagerFactory;

    @BeforeEach
    void setUp() {
        entityManagerFactory = Persistence.createEntityManagerFactory("defaultPU");
        var entityManager = entityManagerFactory.createEntityManager();
        entityManager.getTransaction().begin();
        var deleteFromPerson = entityManager.createQuery("DELETE FROM Person").executeUpdate();
        log.debug("Deleted {} persons", deleteFromPerson);
        entityManager.getTransaction().commit();
        entityManager.close();
    }

    @Test
    @DisplayName("insert person and verify person")
    public void testInsertAndFindPerson() throws Exception {
        var person = new Person("John", 30);
        var entityManager = entityManagerFactory.createEntityManager();
        entityManager.getTransaction().begin();
        entityManager.persist(person);
        entityManager.getTransaction().commit();
        var id = person.getId();
        assertNotNull(id);

        try {
            var foundPerson = entityManager.find(Person.class, id);
            assertThat(foundPerson.getId()).isNotNull();
            assertThat(foundPerson.getName()).isEqualTo("John");
            assertThat(foundPerson.getAge()).isEqualTo(30);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    @AfterEach
    void tearDown() {
        entityManagerFactory.close();
    }
}

In the @BeforeEach method, we will create an EntityManagerFactory instance. And in the @AfterEach we call the EntityManagerFactory.close to release the resource.

In the @BeforeEach we try to clean up the Person data.

Now in the testInsertAndFindPerson test, we insert a new person, then utilize entityManager.find to find the inserted person.

The person id is annotated with @ID and @GeneratedValue, when inserting a person into table, hibernate will generate an ID automatically. After it is persisted, the returned instance is filled with the generated id, it should not be a null.

Numeric Functions

Add some extra properties in the above Person class.

public class Person{
    private Integer yearsWorked = 2;
    private LocalDateTime birthDate = LocalDateTime.now().minusYears(30);
    private BigDecimal salary = new BigDecimal("12345.678");
    private BigDecimal hourlyRate = new BigDecimal("34.56");

    // setters and getters
}

Create a new test to verify the new numeric functions: ceiling, floor, round, exp, ln, power, sign.

@Test
@DisplayName(">>> test numeric functions")
public void testNumericFunctions() throws Exception {
    var person = new Person("John", 30);
    var entityManager = entityManagerFactory.createEntityManager();
    entityManager.getTransaction().begin();
    entityManager.persist(person);
    entityManager.getTransaction().commit();
    var id = person.getId();
    assertNotNull(id);

    try {
        var queryString = """
                SELECT p.name as name,
                CEILING(p.salary) as ceiling,
                FLOOR(p.salary) as floor,
                ROUND(p.salary, 1) as round,
                EXP(p.yearsWorked) as exp,
                LN(p.yearsWorked) as ln,
                POWER(p.yearsWorked,2) as power,
                SIGN(p.yearsWorked) as sign
                FROM Person p
                WHERE p.id=:id
                """;
        var query = entityManager.createQuery(queryString);
        query.setParameter("id", id);
        var resultList = query.getResultList();
        log.debug("Result list: {}", resultList);
        resultList.forEach(result -> log.debug("result: {}", result));
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

Next, let's have a look at how to use them in the Criteria APIs.

@Test
@DisplayName(">>> test numeric functions")
public void testNumericFunctions() throws Exception {
    var person = new Person("John", 30);
    var entityManager = entityManagerFactory.createEntityManager();
    entityManager.getTransaction().begin();
    entityManager.persist(person);
    entityManager.getTransaction().commit();
    var id = person.getId();
    assertNotNull(id);

    try {
        // see: https://hibernate.zulipchat.com/#narrow/stream/132096-hibernate-user/topic/New.20functions.20in.20JPA.203.2E1/near/289429903
        var cb = (HibernateCriteriaBuilder) entityManager.getCriteriaBuilder();
        var query = cb.createTupleQuery();
        var root = query.from(Person.class);

        query.multiselect(root.get("name"),
                cb.ceiling(root.get("salary")),
                cb.floor(root.get("salary")),
                cb.round(root.get("salary"), 1),
                cb.exp(root.get("yearsWorked")),
                cb.ln(root.get("yearsWorked")),
                // see: https://hibernate.atlassian.net/browse/HHH-15395
                cb.power(root.get("yearsWorked"), 2),
                cb.sign(root.get("yearsWorked"))
        );
        query.where(cb.equal(root.get("id"), id));
        var resultList = entityManager.createQuery(query).getResultList();
        log.debug("Result list: {}", resultList);

        resultList.forEach(result ->
                log.debug(
                        "result: ({},{},{},{},{},{},{},{})",
                        result.get(0, String.class),
                        result.get(1, BigDecimal.class),
                        result.get(2, BigDecimal.class),
                        result.get(3, BigDecimal.class),
                        result.get(4, Double.class),
                        result.get(5, Double.class),
                        result.get(6, Double.class),
                        result.get(7, Integer.class)
                )
        );
    } catch (Exception ex) {
        fail(ex);
    }
}

Note, when using Hibernate 6.1, we have to cast CriteriaBuilder to HibernateCriteriaBuilder to experience the new numeric functions. Hibernate 6.2 will align to JPA 3.1 and fix the issue.

DateTime Functions

Add LOCAL TIME, LOCAL DATETIME, LOCAL DATE to the select clause, create a test to verify the query result.

 @Test
@DisplayName(">>> test datetime functions")
public void testDateTimeFunctions() throws Exception {
    var person = new Person("John", 30);
    var entityManager = entityManagerFactory.createEntityManager();
    entityManager.getTransaction().begin();
    entityManager.persist(person);
    entityManager.getTransaction().commit();
    var id = person.getId();
    assertNotNull(id);

    try {
        var queryString = """
                SELECT p.name as name,
                LOCAL TIME as localTime,
                LOCAL DATETIME as localDateTime,
                LOCAL DATE as localDate
                FROM Person p
                """;

        var query = entityManager.createQuery(queryString);
        var resultList = query.getResultList();
        log.debug("Result list: {}", resultList);
        resultList.forEach(result -> log.debug("result: {}", result));
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

Run the test, you will see LOCAL TIME, LOCAL DATETIME, LOCAL DATE results are treated as Java 8 LocalTime, LocalDateTime, LocalDate directly.

Let's have a look at the usage in the CriteriaBuilder APIs.

@Test
@DisplayName(">>> test datetime functions")
public void testDateTimeFunctions() throws Exception {
    var person = new Person("John", 30);
    var entityManager = entityManagerFactory.createEntityManager();
    entityManager.getTransaction().begin();
    entityManager.persist(person);
    entityManager.getTransaction().commit();
    var id = person.getId();
    assertNotNull(id);

    try {
        var cb = (HibernateCriteriaBuilder) entityManager.getCriteriaBuilder();
        var query = cb.createTupleQuery();
        var root = query.from(Person.class);

        query.multiselect(root.get("name"),
                cb.localTime(),
                cb.localDateTime(),
                cb.localDate()
        );
        query.where(cb.equal(root.get("id"), id));

        var resultList = entityManager.createQuery(query).getResultList();
        log.debug("Result list: {}", resultList);
        resultList.forEach(result ->
                log.debug(
                        "result: ({},{},{},{})",
                        result.get(0, String.class),
                        result.get(1, LocalTime.class),
                        result.get(2, LocalDateTime.class),
                        result.get(3, LocalDate.class)
                )
        );
    } catch (Exception ex) {
        fail(ex);
    }
}

EXTRACT function

Here we use the new extract function to read the year, quarter, month, week, day, hour, minute, second values from a Java 8 DateTime type property in JPQL query.

@Test
@DisplayName(">>> test `EXTRACT` functions")
public void testExtractFunctions() throws Exception {
    var person = new Person("John", 30);
    var entityManager = entityManagerFactory.createEntityManager();
    entityManager.getTransaction().begin();
    entityManager.persist(person);
    entityManager.getTransaction().commit();
    var id = person.getId();
    assertNotNull(id);

    try {
        var queryString = """
                SELECT p.name as name,
                EXTRACT(YEAR FROM p.birthDate) as year,
                EXTRACT(QUARTER FROM p.birthDate) as quarter,
                EXTRACT(MONTH FROM p.birthDate) as month,
                EXTRACT(WEEK FROM p.birthDate) as week,
                EXTRACT(DAY FROM p.birthDate) as day,
                EXTRACT(HOUR FROM p.birthDate) as hour,
                EXTRACT(MINUTE FROM p.birthDate) as minute,
                EXTRACT(SECOND FROM p.birthDate) as second
                FROM Person p
                """;
        var query = entityManager.createQuery(queryString);

        var resultList = query.getResultList();
        log.debug("Result list: {}", resultList);
        resultList.forEach(result -> log.debug("result: {}", result));
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

Note, there is no mapped extract function in the CriteriaBuilder APIs, for more details, check the issue jpa-api #356.

Get a copy of sample codes from my Github and experience yourself.