diff --git a/.gitignore b/.gitignore index 8c8680b24..f9e6e539d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ target/ core/btm1.tlog core/btm2.tlog *.tlog +*.log +*.lck +core/PutObjectStoreDirHere/ diff --git a/LICENSE b/LICENSE index 8f71f43fe..5585b163c 100644 --- a/LICENSE +++ b/LICENSE @@ -175,18 +175,7 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} + Copyright {2015-2024} {Mihalcea Vlad-Alexandru} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/MYSQL.md b/MYSQL.md deleted file mode 100644 index 2e886f4da..000000000 --- a/MYSQL.md +++ /dev/null @@ -1,13 +0,0 @@ -* Install MySQL 5.6 (or later) -* Create the database `high_performance_java_persistence` - ``` - create database high_performance_java_persistence; - ``` -* Create the user `mysql` and grant it the necessary privileges: - ``` - create user 'mysql'@'localhost'; - SET PASSWORD for 'mysql'@'localhost' = PASSWORD('admin'); - GRANT ALL PRIVILEGES ON high_performance_java_persistence.* TO 'mysql'@'localhost'; - GRANT SELECT ON mysql.* TO 'mysql'@'localhost'; - FLUSH PRIVILEGES; - ``` diff --git a/README.md b/README.md index 010f3dedf..90a2c8408 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,93 @@ # High-Performance Java Persistence -The [High-Performance Java Persistence](https://leanpub.com/high-performance-java-persistence?utm_source=GitHub&utm_medium=banner&utm_campaign=hpjp) book code examples. - -High-Performance Java Persistence +The [High-Performance Java Persistence](https://vladmihalcea.com/books/high-performance-java-persistence?utm_source=GitHub&utm_medium=banner&utm_campaign=hpjp) book and video course code examples. I wrote [this article](https://vladmihalcea.com/high-performance-java-persistence-github-repository/) about this repository since it's one of the best way to test JDBC, JPA, Hibernate or even jOOQ code. Or, if you prefer videos, you can watch [this presentation on YouTube](https://www.youtube.com/watch?v=U8MoOe8uMYA). + +### Are you struggling with application performance issues? + + +Hypersistence Optimizer -All examples require at least Java 1.8. +Imagine having a tool that can automatically detect if you are using JPA and Hibernate properly. No more performance issues, no more having to spend countless hours trying to figure out why your application is barely crawling. + +Imagine discovering early during the development cycle that you are using suboptimal mappings and entity relationships or that you are missing performance-related settings. + +More, with Hypersistence Optimizer, you can detect all such issues during testing and make sure you don't deploy to production a change that will affect data access layer performance. + +[Hypersistence Optimizer](https://vladmihalcea.com/hypersistence-optimizer/?utm_source=GitHub&utm_medium=banner&utm_campaign=hpjp) is the tool you've been long waiting for! + +#### Training + +If you are interested in on-site training, I can offer you my [High-Performance Java Persistence training](https://vladmihalcea.com/trainings/?utm_source=GitHub&utm_medium=banner&utm_campaign=hpjp) +which can be adapted to one, two or three days of sessions. For more details, check out [my website](https://vladmihalcea.com/trainings/?utm_source=GitHub&utm_medium=banner&utm_campaign=hpjp). + +#### Consulting + +If you want me to review your application and provide insight into how you can optimize it to run faster, +then check out my [consulting page](https://vladmihalcea.com/consulting/?utm_source=GitHub&utm_medium=banner&utm_campaign=hpjp). + +#### High-Performance Java Persistence Video Courses + +If you want the fastest way to learn how to speed up a Java database application, then you should definitely enroll in [my High-Performance Java Persistence video courses](https://vladmihalcea.com/courses/?utm_source=GitHub&utm_medium=banner&utm_campaign=hpjp). + +#### High-Performance Java Persistence Book + +Or, if you prefer reading books, you are going to love my [High-Performance Java Persistence book](https://vladmihalcea.com/books/high-performance-java-persistence?utm_source=GitHub&utm_medium=banner&utm_campaign=hpjp) as well. + + +High-Performance Java Persistence book + + + +High-Performance Java Persistence video course + + +## Java + +All examples require at least Java 17 because of the awesome [Text Blocks](https://openjdk.java.net/jeps/355) feature, which makes JPQL and SQL queries so much readable. + +## Maven + +You need to use Maven 3.6.2 or newer to build the project. -**Javac compiler is required in order to run in in any IDE environment. -Especially if you're using Eclipse, you must use the Oracle JDK compiler and not the Eclipse-based one which suffers from [this issue](https://bugs.eclipse.org/bugs/show_bug.cgi?id=434642).** +## IntelliJ IDEA -On InteliJ IDEA, the project runs just fine without any further requirements. +On IntelliJ IDEA, the project runs just fine. You will have to make sure to select Java 17 or newer. -However, on Eclipse it has been reported that you need to consider the following configurations (many thanks to [Urs Joss](https://github.com/ursjoss) for the hints): +## Database setup -1. Eclipse does not automatically treat the generated sources by jpamodelgen as source folders. You need to add a dependency on `hibernate-jpamodelgen` and use the `build-helper-maven-plugin` to source the folders with the generated sources. -2. Secondly, the Maven eclipse plugin e2m seems to have an issue with some plugin configurations. Make sure you configure e2m to ignore the false positives issues (the project runs justs fine from a Maven command line). -3. There’s an issue with Eclipse (or probably more specific ecj) to infer the types of parameters in case of method overloading with the methods `doInJpa`, `doInHibernate`, `doInJdbc`. -Until [this Eclipse issue](https://bugs.eclipse.org/bugs/show_bug.cgi?id=434642) is fixed, you need to use the Oracle JDK to compile the project. -If you can't change that, you need to rename those overloaded functions as explained by Urs Joss in [this specific commit](https://github.com/ursjoss/high-performance-java-persistence/commit/e975c1bb5c11d9557fcbc3fef88afaf67dc68a25). +The project uses various database systems for integration testing, and you can configure the JDBC connection settings using the +`DatasourceProvider` instances (e.g., `PostgreSQLDataSourceProvider`). -The Unit Tests are run against HSQLDB, so no preliminary set-ups are required. +By default, without configuring any database explicitly, HSQLDB is used for testing. -The Integration Tests require some external configurations: +However, since some integration tests are designed to work on specific relational databases, we will need to have those databases started prior to running those tests. + +Therefore, when running a DB-specific test, this GitHub repository will execute the following steps: + +1. First, the test will try to find whether there's a local RDBMS it can use to run the test. +2. If no local database is found, the integration tests will use Testcontainers to bootstrap a Docker container +with the required *Oracle*, *SQL Server*, *PostgreSQL*, *MySQL*, *MariaDB*, *YugabyteDB*, or *CockroachDB* instance on demand. + +> While you don't need to install any database manually on your local OS, this is recommended since your tests will run much faster than if they used Testcontainers. + +### Manual Database configuration - PostgreSQL - You should install PostgreSQL 9.5 (or later) and the password for the postgres user should be admin + You can install PostgreSQL, and the password for the `postgres` user should be `admin`. - Now you need to create a `high_performance_java_persistence` database - Open pgAdmin III and executed the following query: - - CREATE EXTENSION postgis; - CREATE EXTENSION pgcrypto; + Now you need to create a `high_performance_java_persistence` database. - Oracle You need to download and install Oracle XE - Set the sys password to admin + Set the `sys` password to `admin` Connect to Oracle using the "sys as sysdba" user and create a new user: + + alter session set "_ORACLE_SCRIPT"=true; create user oracle identified by admin default tablespace users; @@ -52,31 +99,36 @@ The Integration Tests require some external configurations: ALTER PROFILE DEFAULT LIMIT PASSWORD_LIFE_TIME UNLIMITED; - For the Oracle JDBC driver, you have multiple alternatives. - - 1. You can follow the steps explained in [this article](http://docs.oracle.com/middleware/1213/core/MAVEN/config_maven_repo.htm#MAVEN9010) to set up the Oracle Maven Repository. - - 2. You can also download the Oracle JDBC Driver (ojdbc7_g.jar and ojdbc8.jar), which is not available in the Maven Central Repository. - and install the ojdbc7_g.jar and ojdbc8.jar on your local Maven repository using the following command: - - $ mvn install:install-file -Dfile=ojdbc8.jar -DgroupId=com.oracle.jdbc -DartifactId=ojdbc8 -Dversion=12.2.0.1 -Dpackaging=jar - $ mvn install:install-file -Dfile=ojdbc7_g.jar -DgroupId=com.oracle -DartifactId=ojdbc7_g -Dversion=12.1.0.1 -Dpackaging=jar - - The `com.oracle:ojdbc7_g` artifact is sued just by the jooq-oracle sub-module since there is some issue with the sql-maven-plugin Oracle dependency otherwise. - + Open the `C:\app\${user.name}\product\21c\homes\OraDB21Home1\network\admin` folder where `${user.name}` is your current Windows username. + + Locate the `tnsnames.ora` and `listener.ora` files and change the port from `1522` to `1521` if that's the case. If you made these modifications, + you need to restart the `OracleOraDB21Home1TNSListener` and `OracleServiceXE` Windows services. + - MySQL - You should install MySQL 5.6 (or later) and the password for the mysql user should be admin. + You should install MySQL 8, and the password for the `mysql` user should be `admin`. - Now you need to create a `high_performance_java_persistence` schema + Now, you need to create a `high_performance_java_persistence` schema - Besides having all privileges on this schema, the user mysql also requires select persmission on `mysql.PROC`. - - Exact instructions can be found in [here](https://github.com/ursjoss/high-performance-java-persistence/blob/tb_mysql_instructions/MYSQL.md). + Besides having all privileges on this schema, the `mysql` user also requires select permission on `mysql.PROC`. + + If you don't have a `mysql` user created at database installation time, you can create one as follows: + + ```` + CREATE USER 'mysql'@'localhost'; + + SET PASSWORD for 'mysql'@'localhost'='admin'; + + GRANT ALL PRIVILEGES ON high_performance_java_persistence.* TO 'mysql'@'localhost'; + + GRANT SELECT ON mysql.* TO 'mysql'@'localhost'; + + FLUSH PRIVILEGES; + ```` - SQL Server - You should install SQL Server Express Edition with Tools Chose mixed mode authentication and set the sa user password to adm1n + You can install SQL Server Express Edition with Tools. Choose mixed mode authentication and set the `sa` user password to `adm1n`. Open SQL Server Configuration Manager -> SQL Server Network Configuration and enable Named Pipes and TCP @@ -85,10 +137,18 @@ The Integration Tests require some external configurations: Open SQL Server Management Studio and create the `high_performance_java_persistence` database -To build the project, don't use *install* or *package*. Instead, just compile test classes like this: +## Maven - mvn clean test-compile +> To build the project, don't use *install* or *package*. Instead, just compile test classes like this: +> +> mvnw clean test-compile + +Or you can just run the `build.bat` or `build.sh` scripts which run the above Maven command. -Then, just pick one test from the IDE and run it individually. -If you run all tests (e.g. `mvn clean test`), the test suite will take way to long to complete since -some performance tests require to run for long periods of time. +Afterward, just pick one test from the IDE and run it individually. + +> Don't you run all tests at once (e.g. `mvn clean test`) because the test suite will take a very long time to complete. +> +> So, run the test you are interested in individually. + +Enjoy learning more about Java Persistence, Hibernate, and database systems! diff --git a/build.bat b/build.bat new file mode 100644 index 000000000..2c51e6af1 --- /dev/null +++ b/build.bat @@ -0,0 +1,12 @@ +@echo off + +pushd core +call mvn -D skipTests clean install +popd + +pushd jooq +call mvn -D skipTests clean install +call mvn test-compile +popd + +goto:eof \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 000000000..c06551626 --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +call mvn -D skipTests clean install \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index 099107319..244f118c2 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0%20http://maven.apache.org/xsd/maven-4.0.0.xsd"> - com.vladmihalcea.book + com.vladmihalcea high-performance-java-persistence 1.0-SNAPSHOT @@ -14,15 +14,70 @@ high-performance-java-persistence-core + + + com.blazebit + blaze-persistence-core-api-jakarta + ${blaze-persistence.version} + + + com.blazebit + blaze-persistence-core-impl-jakarta + ${blaze-persistence.version} + + + + com.blazebit + blaze-persistence-jpa-criteria-api + ${blaze-persistence.version} + + + + com.blazebit + blaze-persistence-jpa-criteria-impl + ${blaze-persistence.version} + + + + com.blazebit + blaze-persistence-integration-hibernate-6.2 + ${blaze-persistence.version} + + + + com.blazebit + blaze-persistence-entity-view-api-jakarta + ${blaze-persistence.version} + compile + + + + com.blazebit + blaze-persistence-entity-view-impl-jakarta + ${blaze-persistence.version} + runtime + + + + + + net.steppschuh.markdowngenerator + markdowngenerator + 1.3.1.1 + + + + + org.apache.maven.plugins maven-jar-plugin - 2.4 + ${maven-jar-plugin} @@ -39,12 +94,11 @@ - - true - true + false false false + false enhance @@ -60,26 +114,6 @@ - - org.bsc.maven - maven-processor-plugin - 2.0.5 - - - process - - process - - generate-sources - - - org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor - - - - - - diff --git a/core/spring-loadtime-weaving.bat b/core/spring-loadtime-weaving.bat new file mode 100644 index 000000000..bb4aa7dd6 --- /dev/null +++ b/core/spring-loadtime-weaving.bat @@ -0,0 +1,5 @@ +@echo off + +call mvn -Dcmd.args="-javaagent:%M2_REPOSITORY%\org\springframework\spring-instrument\6.0.8\spring-instrument-6.0.8.jar" -Dtest=SpringDataJPARuntimeBytecodeEnhancementTest test + +goto:eof \ No newline at end of file diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/Attachment.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/Attachment.java deleted file mode 100644 index 205da229c..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/Attachment.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.forum; - -import javax.persistence.*; - -/** - * @author Vlad Mihalcea - */ -@Entity(name = "Attachment") -@Table(name = "attachment") -public class Attachment { - - @Id - @GeneratedValue - private Long id; - - private String name; - - @Enumerated - @Column(name = "media_type") - private MediaType mediaType; - - @Lob - @Basic( fetch = FetchType.LAZY ) - private byte[] content; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public MediaType getMediaType() { - return mediaType; - } - - public void setMediaType(MediaType mediaType) { - this.mediaType = mediaType; - } - - public byte[] getContent() { - return content; - } - - public void setContent(byte[] content) { - this.content = content; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/Post.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/Post.java deleted file mode 100644 index f3d7b4219..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/Post.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.forum; - -import org.hibernate.annotations.LazyToOne; -import org.hibernate.annotations.LazyToOneOption; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "post") -public class Post { - - @Id - private Long id; - - private String title; - - public Post() { - } - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", fetch = FetchType.LAZY) - @LazyToOne(LazyToOneOption.NO_PROXY) - private PostDetails details; - - @ManyToMany - @JoinTable(name = "post_tag", - joinColumns = @JoinColumn(name = "post_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id") - ) - private List tags = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public PostDetails getDetails() { - return details; - } - - public List getTags() { - return tags; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - - public void addDetails(PostDetails details) { - this.details = details; - details.setPost(this); - } - - public void removeDetails() { - this.details.setPost(null); - this.details = null; - } -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/PostComment.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/PostComment.java deleted file mode 100644 index f7ff86398..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/PostComment.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.forum; - -import javax.persistence.*; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "post_comment") -public class PostComment { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public PostComment() { - } - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/PostDetails.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/PostDetails.java deleted file mode 100644 index 5bb236910..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/PostDetails.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.forum; - -import javax.persistence.*; -import java.util.Date; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "post_details") -public class PostDetails { - - @Id - @GeneratedValue - private Long id; - - @Column(name = "created_on") - private Date createdOn; - - @Column(name = "created_by") - private String createdBy; - - public PostDetails() { - createdOn = new Date(); - } - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") - private Post post; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/Tag.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/Tag.java deleted file mode 100644 index edde8566d..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/Tag.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.forum; - -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "tag") -public class Tag { - - @Id - private Long id; - - private String name; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/AbstractArrayTypeDescriptor.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/AbstractArrayTypeDescriptor.java deleted file mode 100644 index 287104f8e..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/AbstractArrayTypeDescriptor.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.array; - -import java.sql.Array; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.Properties; - -import org.hibernate.type.descriptor.WrapperOptions; -import org.hibernate.type.descriptor.java.AbstractTypeDescriptor; -import org.hibernate.type.descriptor.java.MutabilityPlan; -import org.hibernate.type.descriptor.java.MutableMutabilityPlan; -import org.hibernate.usertype.DynamicParameterizedType; - -/** - * @author Vlad Mihalcea - */ -public abstract class AbstractArrayTypeDescriptor - extends AbstractTypeDescriptor implements DynamicParameterizedType { - - private Class arrayObjectClass; - - @Override - public void setParameterValues(Properties parameters) { - arrayObjectClass = ( (ParameterType) parameters.get( PARAMETER_TYPE ) ).getReturnedClass(); - - } - - public AbstractArrayTypeDescriptor(Class arrayObjectClass) { - super( arrayObjectClass, (MutabilityPlan) new MutableMutabilityPlan() { - @Override - protected T deepCopyNotNull(Object value) { - return ArrayUtil.deepCopy( value ); - } - } ); - this.arrayObjectClass = arrayObjectClass; - } - - @Override - public boolean areEqual(Object one, Object another) { - if ( one == another ) { - return true; - } - if ( one == null || another == null ) { - return false; - } - return ArrayUtil.isEquals( one, another ); - } - - @Override - public String toString(Object value) { - return Arrays.deepToString((Object[]) value); - } - - @Override - public T fromString(String string) { - return ArrayUtil.fromString(string, arrayObjectClass); - } - - @SuppressWarnings({ "unchecked" }) - @Override - public X unwrap(T value, Class type, WrapperOptions options) { - return (X) ArrayUtil.wrapArray( value ); - } - - @Override - public T wrap(X value, WrapperOptions options) { - if( value instanceof Array ) { - Array array = (Array) value; - try { - return ArrayUtil.unwrapArray( (Object[]) array.getArray(), arrayObjectClass ); - } - catch (SQLException e) { - throw new IllegalArgumentException( e ); - } - } - return (T) value; - } - - protected abstract String getSqlArrayType(); - -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/ArraySqlTypeDescriptor.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/ArraySqlTypeDescriptor.java deleted file mode 100644 index e30a43ae8..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/ArraySqlTypeDescriptor.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.array; - -import java.sql.CallableStatement; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Types; - -import org.hibernate.type.descriptor.ValueBinder; -import org.hibernate.type.descriptor.ValueExtractor; -import org.hibernate.type.descriptor.WrapperOptions; -import org.hibernate.type.descriptor.java.JavaTypeDescriptor; -import org.hibernate.type.descriptor.sql.BasicBinder; -import org.hibernate.type.descriptor.sql.BasicExtractor; -import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; - -/** - * @author Vlad Mihalcea - */ -public class ArraySqlTypeDescriptor implements SqlTypeDescriptor { - - public static final ArraySqlTypeDescriptor INSTANCE = new ArraySqlTypeDescriptor(); - - @Override - public int getSqlType() { - return Types.ARRAY; - } - - @Override - public boolean canBeRemapped() { - return true; - } - - @Override - public ValueBinder getBinder(JavaTypeDescriptor javaTypeDescriptor) { - return new BasicBinder( javaTypeDescriptor, this) { - @Override - protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { - AbstractArrayTypeDescriptor abstractArrayTypeDescriptor = (AbstractArrayTypeDescriptor) javaTypeDescriptor; - st.setArray( index, st.getConnection().createArrayOf( - abstractArrayTypeDescriptor.getSqlArrayType(), - abstractArrayTypeDescriptor.unwrap( value, Object[].class, options ) - )); - } - - @Override - protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) - throws SQLException { - throw new UnsupportedOperationException( "Binding by name is not supported!" ); - } - }; - } - - @Override - public ValueExtractor getExtractor(final JavaTypeDescriptor javaTypeDescriptor) { - return new BasicExtractor(javaTypeDescriptor, this) { - @Override - protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { - return javaTypeDescriptor.wrap(rs.getArray(name), options); - } - - @Override - protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { - return javaTypeDescriptor.wrap(statement.getArray(index), options); - } - - @Override - protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { - return javaTypeDescriptor.wrap(statement.getArray(name), options); - } - }; - } - -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/ArrayUtil.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/ArrayUtil.java deleted file mode 100644 index fdbedb1ce..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/ArrayUtil.java +++ /dev/null @@ -1,289 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.array; - -import java.lang.reflect.Array; -import java.util.Arrays; - -/** - * @author Vlad Mihalcea - */ -public class ArrayUtil { - - public static T deepCopy(Object objectArray) { - Class arrayClass = objectArray.getClass(); - - if( boolean[].class.equals( arrayClass ) ) { - boolean[] array = (boolean[]) objectArray; - return (T) Arrays.copyOf( array, array.length); - } - else if( byte[].class.equals( arrayClass ) ) { - byte[] array = (byte[]) objectArray; - return (T) Arrays.copyOf(array, array.length); - } - else if( short[].class.equals( arrayClass ) ) { - short[] array = (short[]) objectArray; - return (T) Arrays.copyOf(array, array.length); - } - else if( int[].class.equals( arrayClass ) ) { - int[] array = (int[]) objectArray; - return (T) Arrays.copyOf(array, array.length); - } - else if( long[].class.equals( arrayClass ) ) { - long[] array = (long[]) objectArray; - return (T) Arrays.copyOf(array, array.length); - } - else if( float[].class.equals( arrayClass ) ) { - float[] array = (float[]) objectArray; - return (T) Arrays.copyOf(array, array.length); - } - else if( double[].class.equals( arrayClass ) ) { - double[] array = (double[]) objectArray; - return (T) Arrays.copyOf(array, array.length); - } - else if( char[].class.equals( arrayClass ) ) { - char[] array = (char[]) objectArray; - return (T) Arrays.copyOf(array, array.length); - } - else { - Object[] array = (Object[]) objectArray; - return (T) Arrays.copyOf(array, array.length); - } - } - - public static Object[] wrapArray(Object objectArray) { - Class arrayClass = objectArray.getClass(); - - if( boolean[].class.equals( arrayClass ) ) { - boolean[] fromArray = (boolean[]) objectArray; - Boolean[] array = new Boolean[ fromArray.length]; - for ( int i = 0; i < fromArray.length; i++ ) { - array[i] = fromArray[i]; - } - return array; - } - else if( byte[].class.equals( arrayClass ) ) { - byte[] fromArray = (byte[]) objectArray; - Byte[] array = new Byte[ fromArray.length]; - for ( int i = 0; i < fromArray.length; i++ ) { - array[i] = fromArray[i]; - } - return array; - } - else if( short[].class.equals( arrayClass ) ) { - short[] fromArray = (short[]) objectArray; - Short[] array = new Short[ fromArray.length]; - for ( int i = 0; i < fromArray.length; i++ ) { - array[i] = fromArray[i]; - } - return array; - } - else if( int[].class.equals( arrayClass ) ) { - int[] fromArray = (int[]) objectArray; - Integer[] array = new Integer[ fromArray.length]; - for ( int i = 0; i < fromArray.length; i++ ) { - array[i] = fromArray[i]; - } - return array; - } - else if( long[].class.equals( arrayClass ) ) { - long[] fromArray = (long[]) objectArray; - Long[] array = new Long[ fromArray.length]; - for ( int i = 0; i < fromArray.length; i++ ) { - array[i] = fromArray[i]; - } - return array; - } - else if( float[].class.equals( arrayClass ) ) { - float[] fromArray = (float[]) objectArray; - Float[] array = new Float[ fromArray.length]; - for ( int i = 0; i < fromArray.length; i++ ) { - array[i] = fromArray[i]; - } - return array; - } - else if( double[].class.equals( arrayClass ) ) { - double[] fromArray = (double[]) objectArray; - Double[] array = new Double[ fromArray.length]; - for ( int i = 0; i < fromArray.length; i++ ) { - array[i] = fromArray[i]; - } - return array; - } - else if( char[].class.equals( arrayClass ) ) { - char[] fromArray = (char[]) objectArray; - Character[] array = new Character[ fromArray.length]; - for ( int i = 0; i < fromArray.length; i++ ) { - array[i] = fromArray[i]; - } - return array; - } - else { - return (Object[]) objectArray; - } - } - - public static T unwrapArray(Object[] objectArray, Class arrayClass) { - - if( boolean[].class.equals( arrayClass ) ) { - boolean[] array = new boolean[objectArray.length]; - for ( int i = 0; i < objectArray.length; i++ ) { - array[i] = objectArray[i] != null ? (Boolean) objectArray[i] : Boolean.FALSE; - } - return (T) array; - } - else if( byte[].class.equals( arrayClass ) ) { - byte[] array = new byte[objectArray.length]; - for ( int i = 0; i < objectArray.length; i++ ) { - array[i] = objectArray[i] != null ? (Byte) objectArray[i] : 0; - } - return (T) array; - } - else if( short[].class.equals( arrayClass ) ) { - short[] array = new short[objectArray.length]; - for ( int i = 0; i < objectArray.length; i++ ) { - array[i] = objectArray[i] != null ? (Short) objectArray[i] : 0; - } - return (T) array; - } - else if( int[].class.equals( arrayClass ) ) { - int[] array = new int[objectArray.length]; - for ( int i = 0; i < objectArray.length; i++ ) { - array[i] = objectArray[i] != null ? (Integer) objectArray[i] : 0; - } - return (T) array; - } - else if( long[].class.equals( arrayClass ) ) { - long[] array = new long[objectArray.length]; - for ( int i = 0; i < objectArray.length; i++ ) { - array[i] = objectArray[i] != null ? (Long) objectArray[i] : 0L; - } - return (T) array; - } - else if( float[].class.equals( arrayClass ) ) { - float[] array = new float[objectArray.length]; - for ( int i = 0; i < objectArray.length; i++ ) { - array[i] = objectArray[i] != null ? (Float) objectArray[i] : 0f; - } - return (T) array; - } - else if( double[].class.equals( arrayClass ) ) { - double[] array = new double[objectArray.length]; - for ( int i = 0; i < objectArray.length; i++ ) { - array[i] = objectArray[i] != null ? (Double) objectArray[i] : 0d; - } - return (T) array; - } - else if( char[].class.equals( arrayClass ) ) { - char[] array = new char[objectArray.length]; - for ( int i = 0; i < objectArray.length; i++ ) { - array[i] = objectArray[i] != null ? (Character) objectArray[i] : 0; - } - return (T) array; - } - else { - return (T) objectArray; - } - } - - public static T fromString(String string, Class arrayClass) { - String stringArray = string.replaceAll( "[\\[\\]]", "" ); - String[] tokens = stringArray.split( "," ); - - int length = tokens.length; - - if( boolean[].class.equals( arrayClass ) ) { - boolean[] array = new boolean[length]; - for ( int i = 0; i < tokens.length; i++ ) { - array[i] = Boolean.valueOf( tokens[i] ); - } - return (T) array; - } - else if( byte[].class.equals( arrayClass ) ) { - byte[] array = new byte[length]; - for ( int i = 0; i < tokens.length; i++ ) { - array[i] = Byte.valueOf( tokens[i] ); - } - return (T) array; - } - else if( short[].class.equals( arrayClass ) ) { - short[] array = new short[length]; - for ( int i = 0; i < tokens.length; i++ ) { - array[i] = Short.valueOf( tokens[i] ); - } - return (T) array; - } - else if( int[].class.equals( arrayClass ) ) { - int[] array = new int[length]; - for ( int i = 0; i < tokens.length; i++ ) { - array[i] = Integer.valueOf( tokens[i] ); - } - return (T) array; - } - else if( long[].class.equals( arrayClass ) ) { - long[] array = new long[length]; - for ( int i = 0; i < tokens.length; i++ ) { - array[i] = Long.valueOf( tokens[i] ); - } - return (T) array; - } - else if( float[].class.equals( arrayClass ) ) { - float[] array = new float[length]; - for ( int i = 0; i < tokens.length; i++ ) { - array[i] = Float.valueOf( tokens[i] ); - } - return (T) array; - } - else if( double[].class.equals( arrayClass ) ) { - double[] array = new double[length]; - for ( int i = 0; i < tokens.length; i++ ) { - array[i] = Double.valueOf( tokens[i] ); - } - return (T) array; - } - else if( char[].class.equals( arrayClass ) ) { - char[] array = new char[length]; - for ( int i = 0; i < tokens.length; i++ ) { - array[i] = tokens[i].length() > 0 ? tokens[i].charAt( 0 ) : Character.MIN_VALUE; - } - return (T) array; - } - else { - return (T) tokens; - } - } - - public static boolean isEquals(Object firstArray, Object secondArray) { - if(firstArray.getClass() != secondArray.getClass()) { - return false; - } - Class arrayClass = firstArray.getClass(); - - if( boolean[].class.equals( arrayClass ) ) { - return Arrays.equals( (boolean[]) firstArray, (boolean[]) secondArray ); - } - else if( byte[].class.equals( arrayClass ) ) { - return Arrays.equals( (byte[]) firstArray, (byte[]) secondArray ); - } - else if( short[].class.equals( arrayClass ) ) { - return Arrays.equals( (short[]) firstArray, (short[]) secondArray ); - } - else if( int[].class.equals( arrayClass ) ) { - return Arrays.equals( (int[]) firstArray, (int[]) secondArray ); - } - else if( long[].class.equals( arrayClass ) ) { - return Arrays.equals( (long[]) firstArray, (long[]) secondArray ); - } - else if( float[].class.equals( arrayClass ) ) { - return Arrays.equals( (float[]) firstArray, (float[]) secondArray ); - } - else if( double[].class.equals( arrayClass ) ) { - return Arrays.equals( (double[]) firstArray, (double[]) secondArray ); - } - else if( char[].class.equals( arrayClass ) ) { - return Arrays.equals( (char[]) firstArray, (char[]) secondArray ); - } - else { - return Arrays.equals( (Object[]) firstArray, (Object[]) secondArray ); - } - } - -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/IntArrayType.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/IntArrayType.java deleted file mode 100644 index 97ffdd87c..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/IntArrayType.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.array; - -import java.util.Properties; - -import org.hibernate.type.AbstractSingleColumnStandardBasicType; -import org.hibernate.usertype.DynamicParameterizedType; - -/** - * @author Vlad MIhalcea - */ -public class IntArrayType - extends AbstractSingleColumnStandardBasicType - implements DynamicParameterizedType { - - public IntArrayType() { - super( ArraySqlTypeDescriptor.INSTANCE, IntArrayTypeDescriptor.INSTANCE ); - } - - public String getName() { - return "int-array"; - } - - @Override - protected boolean registerUnderJavaType() { - return true; - } - - @Override - public void setParameterValues(Properties parameters) { - ((IntArrayTypeDescriptor) getJavaTypeDescriptor()).setParameterValues(parameters); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/IntArrayTypeDescriptor.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/IntArrayTypeDescriptor.java deleted file mode 100644 index 3cb147149..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/IntArrayTypeDescriptor.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.array; - -/** - * @author Vlad Mihalcea - */ -public class IntArrayTypeDescriptor - extends AbstractArrayTypeDescriptor { - - public static final IntArrayTypeDescriptor INSTANCE = new IntArrayTypeDescriptor(); - - public IntArrayTypeDescriptor() { - super( int[].class ); - } - - @Override - protected String getSqlArrayType() { - return "integer"; - } -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/StringArrayType.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/StringArrayType.java deleted file mode 100644 index d911ee799..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/StringArrayType.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.array; - -import java.util.Properties; - -import org.hibernate.type.AbstractSingleColumnStandardBasicType; -import org.hibernate.usertype.DynamicParameterizedType; - -/** - * @author Vlad MIhalcea - */ -public class StringArrayType - extends AbstractSingleColumnStandardBasicType - implements DynamicParameterizedType { - - public StringArrayType() { - super( ArraySqlTypeDescriptor.INSTANCE, StringArrayTypeDescriptor.INSTANCE ); - } - - public String getName() { - return "string-array"; - } - - @Override - protected boolean registerUnderJavaType() { - return true; - } - - @Override - public void setParameterValues(Properties parameters) { - ((StringArrayTypeDescriptor) getJavaTypeDescriptor()).setParameterValues(parameters); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/StringArrayTypeDescriptor.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/StringArrayTypeDescriptor.java deleted file mode 100644 index a20dfc4ae..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/array/StringArrayTypeDescriptor.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.array; - -/** - * @author Vlad Mihalcea - */ -public class StringArrayTypeDescriptor - extends AbstractArrayTypeDescriptor { - - public static final StringArrayTypeDescriptor INSTANCE = new StringArrayTypeDescriptor(); - - public StringArrayTypeDescriptor() { - super( String[].class ); - } - - @Override - protected String getSqlArrayType() { - return "text"; - } -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/AbstractJsonSqlTypeDescriptor.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/AbstractJsonSqlTypeDescriptor.java deleted file mode 100644 index 5336ef941..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/AbstractJsonSqlTypeDescriptor.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json; - -import org.hibernate.type.descriptor.ValueExtractor; -import org.hibernate.type.descriptor.WrapperOptions; -import org.hibernate.type.descriptor.java.JavaTypeDescriptor; -import org.hibernate.type.descriptor.sql.BasicExtractor; -import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; - -import java.sql.CallableStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Types; - -/** - * @author Vlad Mihalcea - */ -public abstract class AbstractJsonSqlTypeDescriptor implements SqlTypeDescriptor { - - @Override - public int getSqlType() { - return Types.OTHER; - } - - @Override - public boolean canBeRemapped() { - return true; - } - - @Override - public ValueExtractor getExtractor(final JavaTypeDescriptor javaTypeDescriptor) { - return new BasicExtractor(javaTypeDescriptor, this) { - @Override - protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { - return javaTypeDescriptor.wrap(rs.getObject(name), options); - } - - @Override - protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { - return javaTypeDescriptor.wrap(statement.getObject(index), options); - } - - @Override - protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { - return javaTypeDescriptor.wrap(statement.getObject(name), options); - } - }; - } - -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JacksonUtil.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JacksonUtil.java deleted file mode 100644 index 5204d3457..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JacksonUtil.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.io.IOException; - -/** - * @author Vlad Mihalcea - */ -public class JacksonUtil { - - public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - public static T fromString(String string, Class clazz) { - try { - return OBJECT_MAPPER.readValue(string, clazz); - } catch (IOException e) { - throw new IllegalArgumentException("The given string value: " + string + " cannot be transformed to Json object"); - } - } - - public static String toString(Object value) { - try { - return OBJECT_MAPPER.writeValueAsString(value); - } catch (JsonProcessingException e) { - throw new IllegalArgumentException("The given Json object value: " + value + " cannot be transformed to a String"); - } - } - - public static JsonNode toJsonNode(String value) { - try { - return OBJECT_MAPPER.readTree(value); - } catch (IOException e) { - throw new IllegalArgumentException(e); - } - } - - public static T clone(T value) { - return fromString(toString(value), (Class) value.getClass()); - } -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonBinarySqlTypeDescriptor.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonBinarySqlTypeDescriptor.java deleted file mode 100644 index d45274559..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonBinarySqlTypeDescriptor.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json; - -import com.fasterxml.jackson.databind.JsonNode; -import org.hibernate.type.descriptor.ValueBinder; -import org.hibernate.type.descriptor.WrapperOptions; -import org.hibernate.type.descriptor.java.JavaTypeDescriptor; -import org.hibernate.type.descriptor.sql.BasicBinder; - -import java.sql.CallableStatement; -import java.sql.PreparedStatement; -import java.sql.SQLException; - -/** - * @author Vlad Mihalcea - */ -public class JsonBinarySqlTypeDescriptor extends AbstractJsonSqlTypeDescriptor { - - public static final JsonBinarySqlTypeDescriptor INSTANCE = new JsonBinarySqlTypeDescriptor(); - - @Override - public ValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { - return new BasicBinder(javaTypeDescriptor, this) { - @Override - protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { - st.setObject(index, javaTypeDescriptor.unwrap(value, JsonNode.class, options), getSqlType()); - } - - @Override - protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) - throws SQLException { - st.setObject(name, javaTypeDescriptor.unwrap(value, JsonNode.class, options), getSqlType()); - } - }; - } -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonBinaryType.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonBinaryType.java deleted file mode 100644 index ab644686e..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonBinaryType.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json; - -import org.hibernate.type.AbstractSingleColumnStandardBasicType; -import org.hibernate.usertype.DynamicParameterizedType; - -import java.util.Properties; - -/** - * Descriptor for a Json type. - * - * @author Vlad MIhalcea - * - */ -public class JsonBinaryType - extends AbstractSingleColumnStandardBasicType implements DynamicParameterizedType { - - public JsonBinaryType() { - super( JsonBinarySqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor()); - } - - public String getName() { - return "jsonb"; - } - - @Override - public void setParameterValues(Properties parameters) { - ((JsonTypeDescriptor) getJavaTypeDescriptor()).setParameterValues(parameters); - } - -} \ No newline at end of file diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonStringSqlTypeDescriptor.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonStringSqlTypeDescriptor.java deleted file mode 100644 index 04bab85a5..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonStringSqlTypeDescriptor.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json; - -import org.hibernate.type.descriptor.ValueBinder; -import org.hibernate.type.descriptor.WrapperOptions; -import org.hibernate.type.descriptor.java.JavaTypeDescriptor; -import org.hibernate.type.descriptor.sql.BasicBinder; - -import java.sql.CallableStatement; -import java.sql.PreparedStatement; -import java.sql.SQLException; - -/** - * @author Vlad Mihalcea - */ -public class JsonStringSqlTypeDescriptor extends AbstractJsonSqlTypeDescriptor { - - public static final JsonStringSqlTypeDescriptor INSTANCE = new JsonStringSqlTypeDescriptor(); - - @Override - public ValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { - return new BasicBinder(javaTypeDescriptor, this) { - @Override - protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { - st.setString(index, javaTypeDescriptor.unwrap(value, String.class, options)); - } - - @Override - protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) - throws SQLException { - st.setString(name, javaTypeDescriptor.unwrap(value, String.class, options)); - } - }; - } - -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonStringType.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonStringType.java deleted file mode 100644 index 9b88a9e9c..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonStringType.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json; - -import org.hibernate.type.AbstractSingleColumnStandardBasicType; -import org.hibernate.usertype.DynamicParameterizedType; - -import java.util.Properties; - -/** - * @author Vlad MIhalcea - */ -public class JsonStringType - extends AbstractSingleColumnStandardBasicType implements DynamicParameterizedType { - - public JsonStringType() { - super( JsonStringSqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor() ); - } - - public String getName() { - return "json"; - } - - @Override - protected boolean registerUnderJavaType() { - return true; - } - - @Override - public void setParameterValues(Properties parameters) { - ((JsonTypeDescriptor) getJavaTypeDescriptor()).setParameterValues(parameters); - } -} \ No newline at end of file diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonTypeDescriptor.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonTypeDescriptor.java deleted file mode 100644 index 5501e16aa..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/JsonTypeDescriptor.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json; - -import org.hibernate.type.descriptor.WrapperOptions; -import org.hibernate.type.descriptor.java.AbstractTypeDescriptor; -import org.hibernate.type.descriptor.java.MutableMutabilityPlan; -import org.hibernate.usertype.DynamicParameterizedType; - -import java.util.Properties; - -/** - * @author Vlad Mihalcea - */ -public class JsonTypeDescriptor - extends AbstractTypeDescriptor implements DynamicParameterizedType { - - private Class jsonObjectClass; - - @Override - public void setParameterValues(Properties parameters) { - jsonObjectClass = ( (ParameterType) parameters.get( PARAMETER_TYPE ) ).getReturnedClass(); - - } - - public JsonTypeDescriptor() { - super( Object.class, new MutableMutabilityPlan() { - @Override - protected Object deepCopyNotNull(Object value) { - return JacksonUtil.clone(value); - } - }); - } - - @Override - public boolean areEqual(Object one, Object another) { - if ( one == another ) { - return true; - } - if ( one == null || another == null ) { - return false; - } - return JacksonUtil.toJsonNode(JacksonUtil.toString(one)).equals( - JacksonUtil.toJsonNode(JacksonUtil.toString(another))); - } - - @Override - public String toString(Object value) { - return JacksonUtil.toString(value); - } - - @Override - public Object fromString(String string) { - return JacksonUtil.fromString(string, jsonObjectClass); - } - - @SuppressWarnings({ "unchecked" }) - @Override - public X unwrap(Object value, Class type, WrapperOptions options) { - if ( value == null ) { - return null; - } - if ( String.class.isAssignableFrom( type ) ) { - return (X) toString(value); - } - if ( Object.class.isAssignableFrom( type ) ) { - return (X) JacksonUtil.toJsonNode(toString(value)); - } - throw unknownUnwrap( type ); - } - - @Override - public Object wrap(X value, WrapperOptions options) { - if ( value == null ) { - return null; - } - return fromString(value.toString()); - } - -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/BaseEntity.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/BaseEntity.java deleted file mode 100644 index 4abe529e9..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/BaseEntity.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json.model; - -import javax.persistence.Id; -import javax.persistence.MappedSuperclass; -import javax.persistence.Version; - -import org.hibernate.annotations.TypeDef; -import org.hibernate.annotations.TypeDefs; - -import com.vladmihalcea.book.hpjp.hibernate.type.array.IntArrayType; -import com.vladmihalcea.book.hpjp.hibernate.type.array.StringArrayType; -import com.vladmihalcea.book.hpjp.hibernate.type.json.JsonBinaryType; -import com.vladmihalcea.book.hpjp.hibernate.type.json.JsonStringType; - -/** - * @author Vlad Mihalcea - */ -@TypeDefs({ - @TypeDef(name = "string-array", typeClass = StringArrayType.class), - @TypeDef(name = "int-array", typeClass = IntArrayType.class), - @TypeDef(name = "json", typeClass = JsonStringType.class), - @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class), -}) -@MappedSuperclass -public class BaseEntity { - - @Id - private Long id; - - @Version - private Integer version; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Integer getVersion() { - return version; - } -} diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/Event.java b/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/Event.java deleted file mode 100644 index cf9ec5633..000000000 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/Event.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json.model; - -import org.hibernate.annotations.Type; - -import javax.persistence.*; - -/** - * - * @author Vlad Mihalcea - */ -@Entity(name = "Event") -@Table(name = "event") -public class Event extends BaseEntity { - - @Type(type = "jsonb") - @Column(columnDefinition = "jsonb") - @Basic( fetch = FetchType.LAZY ) - private Location location; - - public Location getLocation() { - return location; - } - - public void setLocation(Location location) { - this.location = location; - } -} \ No newline at end of file diff --git a/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/Attachment.java b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/Attachment.java new file mode 100644 index 000000000..9cd6b417b --- /dev/null +++ b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/Attachment.java @@ -0,0 +1,60 @@ +package com.vladmihalcea.hpjp.hibernate.forum; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Attachment") +@Table(name = "attachment") +public class Attachment { + + @Id + private Long id; + + private String name; + + @Enumerated + @Column(name = "media_type") + private MediaType mediaType; + + @Lob + @Basic(fetch = FetchType.LAZY) + private byte[] content; + + public Long getId() { + return id; + } + + public Attachment setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Attachment setName(String name) { + this.name = name; + return this; + } + + public MediaType getMediaType() { + return mediaType; + } + + public Attachment setMediaType(MediaType mediaType) { + this.mediaType = mediaType; + return this; + } + + public byte[] getContent() { + return content; + } + + public Attachment setContent(byte[] content) { + this.content = content; + return this; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/MediaType.java b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/MediaType.java similarity index 89% rename from core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/MediaType.java rename to core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/MediaType.java index ac86d3979..aa248d04f 100644 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/forum/MediaType.java +++ b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/MediaType.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.hibernate.forum; +package com.vladmihalcea.hpjp.hibernate.forum; /** * @author Vlad Mihalcea diff --git a/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/Post.java b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/Post.java new file mode 100644 index 000000000..8232cdfca --- /dev/null +++ b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/Post.java @@ -0,0 +1,104 @@ +package com.vladmihalcea.hpjp.hibernate.forum; + +import org.hibernate.annotations.LazyToOne; +import org.hibernate.annotations.LazyToOneOption; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + fetch = FetchType.LAZY + ) + @LazyToOne(LazyToOneOption.NO_PROXY) + private PostDetails details; + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable( + name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + + public List getTags() { + return tags; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } + + public Post addTag(Tag tag) { + tags.add(tag); + return this; + } +} diff --git a/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/PostComment.java b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/PostComment.java new file mode 100644 index 000000000..df4119b1b --- /dev/null +++ b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/PostComment.java @@ -0,0 +1,46 @@ +package com.vladmihalcea.hpjp.hibernate.forum; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/PostDetails.java b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/PostDetails.java new file mode 100644 index 000000000..1230ce4aa --- /dev/null +++ b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/PostDetails.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.hibernate.forum; + +import jakarta.persistence.*; +import java.util.Date; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post_details") +public class PostDetails { + + @Id + @GeneratedValue + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } +} diff --git a/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/Tag.java b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/Tag.java new file mode 100644 index 000000000..557da1cbe --- /dev/null +++ b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/Tag.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.hibernate.forum; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "tag") +public class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } +} diff --git a/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/dto/PostDTO.java b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/dto/PostDTO.java new file mode 100644 index 000000000..b85f9fe40 --- /dev/null +++ b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/forum/dto/PostDTO.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.hibernate.forum.dto; + +/** + * @author Vlad Mihalcea + */ +public class PostDTO { + + private final Long id; + + private final String title; + + public PostDTO(Number id, String title) { + this.id = id.longValue(); + this.title = title; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } +} diff --git a/core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/BaseEntity.java b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/BaseEntity.java new file mode 100644 index 000000000..b048055ed --- /dev/null +++ b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/BaseEntity.java @@ -0,0 +1,30 @@ +package com.vladmihalcea.hpjp.hibernate.type.json.model; + +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Version; + +/** + * @author Vlad Mihalcea + */ +@MappedSuperclass +public class BaseEntity { + + @Id + private Long id; + + @Version + private Short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Short getVersion() { + return version; + } +} diff --git a/core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/Event.java b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/Event.java new file mode 100644 index 000000000..9c6deb1c3 --- /dev/null +++ b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/Event.java @@ -0,0 +1,27 @@ +package com.vladmihalcea.hpjp.hibernate.type.json.model; + +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; +import jakarta.persistence.*; +import org.hibernate.annotations.Type; + +/** + * + * @author Vlad Mihalcea + */ +@Entity(name = "Event") +@Table(name = "event") +public class Event extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + @Basic( fetch = FetchType.LAZY ) + private Location location; + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/Location.java b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/Location.java similarity index 87% rename from core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/Location.java rename to core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/Location.java index 716eb8064..5a5f70818 100644 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/Location.java +++ b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/Location.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json.model; +package com.vladmihalcea.hpjp.hibernate.type.json.model; import java.io.Serializable; diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/Participant.java b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/Participant.java similarity index 77% rename from core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/Participant.java rename to core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/Participant.java index 9c4c16a27..6dd6c2146 100644 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/Participant.java +++ b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/Participant.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json.model; +package com.vladmihalcea.hpjp.hibernate.type.json.model; +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; +import jakarta.persistence.*; import org.hibernate.annotations.Type; -import javax.persistence.*; - /** * @author Vlad Mihalcea */ @@ -11,7 +11,7 @@ @Table(name = "participant") public class Participant extends BaseEntity { - @Type(type = "jsonb") + @Type(JsonBinaryType.class) @Column(columnDefinition = "jsonb") @Basic(fetch = FetchType.LAZY) private Ticket ticket; diff --git a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/Ticket.java b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/Ticket.java similarity index 89% rename from core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/Ticket.java rename to core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/Ticket.java index 1b6d8da71..ab03e7e50 100644 --- a/core/src/main/java/com/vladmihalcea/book/hpjp/hibernate/type/json/model/Ticket.java +++ b/core/src/main/java/com/vladmihalcea/hpjp/hibernate/type/json/model/Ticket.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json.model; +package com.vladmihalcea.hpjp.hibernate.type.json.model; import java.io.Serializable; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/AllAssociationTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/AllAssociationTest.java deleted file mode 100644 index 2da567342..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/AllAssociationTest.java +++ /dev/null @@ -1,286 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.Criteria; -import org.hibernate.FetchMode; -import org.hibernate.Session; -import org.hibernate.criterion.Restrictions; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class AllAssociationTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostDetails.class, - PostComment.class, - Tag.class - }; - } - - @Test - public void test() { - doInJPA(entityManager -> { - Post post = new Post(1L); - post.title = "Postit"; - - PostComment comment1 = new PostComment(); - comment1.id = 1L; - comment1.review = "Good"; - - PostComment comment2 = new PostComment(); - comment2.id = 2L; - comment2.review = "Excellent"; - - post.addComment(comment1); - post.addComment(comment2); - entityManager.persist(post); - - Session session = entityManager.unwrap(Session.class); - Criteria criteria = session.createCriteria(Post.class) - .add(Restrictions.eq("title", "post")); - LOGGER.info("Criteria: {}", criteria); - }); - - doInJPA(entityManager -> { - LOGGER.info("No alias"); - Session session = entityManager.unwrap(Session.class); - List posts = session - .createCriteria(Post.class) - .setFetchMode("comments", FetchMode.JOIN) - .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) - .add(Restrictions.eq("title", "Postit")) - .list(); - assertEquals(1, posts.size()); - }); - - doInJPA(entityManager -> { - LOGGER.info("With alias"); - Session session = entityManager.unwrap(Session.class); - List posts = session - .createCriteria(Post.class, "post") - .setFetchMode("post.comments", FetchMode.JOIN) - .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) - .add(Restrictions.eq("post.title", "Postit")) - .list(); - assertEquals(1, posts.size()); - }); - - doInJPA(entityManager -> { - LOGGER.info("With alias"); - Session session = entityManager.unwrap(Session.class); - List posts = session - .createCriteria(Post.class, "post") - .setFetchMode("post.comments", FetchMode.JOIN) - .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) - .add(Restrictions.eq("post.title", "Postit")) - .list(); - assertEquals(1, posts.size()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Post() {} - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true, fetch = FetchType.LAZY) - private PostDetails details; - - @ManyToMany - @JoinTable(name = "post_tag", - joinColumns = @JoinColumn(name = "post_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id") - ) - private List tags = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public PostDetails getDetails() { - return details; - } - - public List getTags() { - return tags; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - - public void addDetails(PostDetails details) { - this.details = details; - details.setPost(this); - } - - public void removeDetails() { - this.details.setPost(null); - this.details = null; - } - } - - @Entity(name = "PostDetails") - @Table(name = "post_details") - public static class PostDetails { - - @Id - private Long id; - - @Column(name = "created_on") - private Date createdOn; - - @Column(name = "created_by") - private String createdBy; - - public PostDetails() { - createdOn = new Date(); - } - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") - @MapsId - private Post post; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } - - @Entity(name = "Tag") - @Table(name = "tag") - public static class Tag { - - @Id - private Long id; - - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyAsOneToManyOrderColumnTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyAsOneToManyOrderColumnTest.java deleted file mode 100644 index 6db73bc0b..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyAsOneToManyOrderColumnTest.java +++ /dev/null @@ -1,299 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; - -/** - * @author Vlad Mihalcea - */ -public class BidirectionalManyAsOneToManyOrderColumnTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - Tag.class, - PostTag.class - }; - } - - @Test - public void testLifecycle() { - doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - entityManager.persist(post1); - entityManager.persist(post2); - - entityManager.persist(tag1); - entityManager.persist(tag2); - - post1.addTag(tag1); - post1.addTag(tag2); - - post2.addTag(tag1); - - entityManager.flush(); - - LOGGER.info("Remove"); - post1.removeTag(tag1); - }); - } - - @Test - public void testShuffle() { - final Long postId = doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - entityManager.persist(post1); - entityManager.persist(post2); - - entityManager.persist(tag1); - entityManager.persist(tag2); - - post1.addTag(tag1); - post1.addTag(tag2); - - post2.addTag(tag1); - - entityManager.flush(); - - return post1.getId(); - }); - doInJPA(entityManager -> { - LOGGER.info("Shuffle"); - Post post1 = entityManager.find(Post.class, postId); - post1.getTags().sort((postTag1, postTag2) -> - postTag2.getId().getTagId().compareTo(postTag1.getId().getTagId()) - ); - }); - } - - @Entity(name = "Post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) - @OrderColumn(name = "entry") - private List tags = new ArrayList<>(); - - public Post() { - } - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public List getTags() { - return tags; - } - - public void addTag(Tag tag) { - PostTag postTag = new PostTag(this, tag); - tags.add(postTag); - tag.getPosts().add(postTag); - } - - public void removeTag(Tag tag) { - for (Iterator iterator = tags.iterator(); iterator.hasNext(); ) { - PostTag postTag = iterator.next(); - if (postTag.getPost().equals(this) && - postTag.getTag().equals(tag)) { - iterator.remove(); - postTag.getTag().getPosts().remove(postTag); - postTag.setPost(null); - postTag.setTag(null); - break; - } - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Post post = (Post) o; - return Objects.equals(title, post.title); - } - - @Override - public int hashCode() { - return Objects.hash(title); - } - } - - @Embeddable - public static class PostTagId implements Serializable { - - private Long postId; - - private Long tagId; - - public PostTagId() { - } - - public PostTagId(Long postId, Long tagId) { - this.postId = postId; - this.tagId = tagId; - } - - public Long getPostId() { - return postId; - } - - public Long getTagId() { - return tagId; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PostTagId that = (PostTagId) o; - return Objects.equals(postId, that.postId) && - Objects.equals(tagId, that.tagId); - } - - @Override - public int hashCode() { - return Objects.hash(postId, tagId); - } - } - - @Entity(name = "PostTag") - public static class PostTag { - - @EmbeddedId - private PostTagId id; - - @ManyToOne - @MapsId("postId") - private Post post; - - @ManyToOne - @MapsId("tagId") - private Tag tag; - - public PostTag() { - } - - public PostTag(Post post, Tag tag) { - this.post = post; - this.tag = tag; - this.id = new PostTagId(post.getId(), tag.getId()); - } - - public PostTagId getId() { - return id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Tag getTag() { - return tag; - } - - public void setTag(Tag tag) { - this.tag = tag; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PostTag that = (PostTag) o; - return Objects.equals(post, that.post) && - Objects.equals(tag, that.tag); - } - - @Override - public int hashCode() { - return Objects.hash(post, tag); - } - } - - @Entity(name = "Tag") - public static class Tag { - - @Id - @GeneratedValue - private Long id; - - private String name; - - @OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true) - private List posts = new ArrayList<>(); - - public Tag() { - } - - public Tag(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getPosts() { - return posts; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Tag tag = (Tag) o; - return Objects.equals(name, tag.name); - } - - @Override - public int hashCode() { - return Objects.hash(name); - } - } - - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyAsOneToManyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyAsOneToManyTest.java deleted file mode 100644 index 54125a2b8..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyAsOneToManyTest.java +++ /dev/null @@ -1,295 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; - -/** - * @author Vlad Mihalcea - */ -public class BidirectionalManyAsOneToManyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - Tag.class, - PostTag.class - }; - } - - @Test - public void testLifecycle() { - doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - entityManager.persist(post1); - entityManager.persist(post2); - - entityManager.persist(tag1); - entityManager.persist(tag2); - - post1.addTag(tag1); - post1.addTag(tag2); - - post2.addTag(tag1); - - entityManager.flush(); - - LOGGER.info("Remove"); - post1.removeTag(tag1); - }); - } - - @Test - public void testShuffle() { - final Long postId = doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - entityManager.persist(post1); - entityManager.persist(post2); - - entityManager.persist(tag1); - entityManager.persist(tag2); - - post1.addTag(tag1); - post1.addTag(tag2); - - post2.addTag(tag1); - - entityManager.flush(); - - return post1.getId(); - }); - doInJPA(entityManager -> { - LOGGER.info("Shuffle"); - Post post1 = entityManager.find(Post.class, postId); - post1.getTags().sort((postTag1, postTag2) -> - postTag2.getId().getTagId().compareTo(postTag1.getId().getTagId()) - ); - }); - } - - @Entity(name = "Post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) - private List tags = new ArrayList<>(); - - public Post() { - } - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public List getTags() { - return tags; - } - - public void addTag(Tag tag) { - PostTag postTag = new PostTag(this, tag); - tags.add(postTag); - tag.getPosts().add(postTag); - } - - public void removeTag(Tag tag) { - for (Iterator iterator = tags.iterator(); iterator.hasNext(); ) { - PostTag postTag = iterator.next(); - if (postTag.getPost().equals(this) && - postTag.getTag().equals(tag)) { - iterator.remove(); - postTag.getTag().getPosts().remove(postTag); - postTag.setPost(null); - postTag.setTag(null); - } - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Post post = (Post) o; - return Objects.equals(title, post.title); - } - - @Override - public int hashCode() { - return Objects.hash(title); - } - } - - @Embeddable - public static class PostTagId implements Serializable { - - private Long postId; - - private Long tagId; - - public PostTagId() {} - - public PostTagId(Long postId, Long tagId) { - this.postId = postId; - this.tagId = tagId; - } - - public Long getPostId() { - return postId; - } - - public Long getTagId() { - return tagId; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PostTagId that = (PostTagId) o; - return Objects.equals(postId, that.postId) && - Objects.equals(tagId, that.tagId); - } - - @Override - public int hashCode() { - return Objects.hash(postId, tagId); - } - } - - @Entity(name = "PostTag") @Table(name = "post_tag") - public static class PostTag { - - @EmbeddedId - private PostTagId id; - - @ManyToOne - @MapsId("postId") - private Post post; - - @ManyToOne - @MapsId("tagId") - private Tag tag; - - private PostTag() {} - - public PostTag(Post post, Tag tag) { - this.post = post; - this.tag = tag; - this.id = new PostTagId(post.getId(), tag.getId()); - } - - public PostTagId getId() { - return id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Tag getTag() { - return tag; - } - - public void setTag(Tag tag) { - this.tag = tag; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PostTag that = (PostTag) o; - return Objects.equals(post, that.post) && - Objects.equals(tag, that.tag); - } - - @Override - public int hashCode() { - return Objects.hash(post, tag); - } - } - - @Entity(name = "Tag") - public static class Tag { - - @Id - @GeneratedValue - private Long id; - - private String name; - - @OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true) - private List posts = new ArrayList<>(); - - public Tag() { - } - - public Tag(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getPosts() { - return posts; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Tag tag = (Tag) o; - return Objects.equals(name, tag.name); - } - - @Override - public int hashCode() { - return Objects.hash(name); - } - } - - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyAsOneToManyWithoutEmbeddedIdTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyAsOneToManyWithoutEmbeddedIdTest.java deleted file mode 100644 index 3d059bd5d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyAsOneToManyWithoutEmbeddedIdTest.java +++ /dev/null @@ -1,255 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; - -/** - * @author Vlad Mihalcea - */ -public class BidirectionalManyAsOneToManyWithoutEmbeddedIdTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - Tag.class, - PostTag.class - }; - } - - @Test - public void testLifecycle() { - doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - entityManager.persist(post1); - entityManager.persist(post2); - - entityManager.persist(tag1); - entityManager.persist(tag2); - - post1.addTag(tag1); - post1.addTag(tag2); - - post2.addTag(tag1); - - entityManager.flush(); - - LOGGER.info("Remove"); - post1.removeTag(tag1); - }); - } - - @Test - public void testShuffle() { - final Long postId = doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - tag1.setId(1L); - Tag tag2 = new Tag("Hibernate"); - tag2.setId(2L); - - entityManager.persist(post1); - entityManager.persist(post2); - - entityManager.persist(tag1); - entityManager.persist(tag2); - - post1.addTag(tag1); - post1.addTag(tag2); - - post2.addTag(tag1); - - entityManager.flush(); - - return post1.getId(); - }); - doInJPA(entityManager -> { - LOGGER.info("Shuffle"); - Post post1 = entityManager.find(Post.class, postId); - Tag tag1 = entityManager.find(Tag.class, 1L); - - PostTag postTag = entityManager.find(PostTag.class, new PostTag(post1, tag1)); - - post1.getTags().sort((postTag1, postTag2) -> - postTag2.getTag().getId().compareTo(postTag1.getTag().getId()) - ); - }); - } - - @Entity(name = "Post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) - private List tags = new ArrayList<>(); - - public Post() { - } - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public List getTags() { - return tags; - } - - public void addTag(Tag tag) { - PostTag postTag = new PostTag(this, tag); - tags.add(postTag); - tag.getPosts().add(postTag); - } - - public void removeTag(Tag tag) { - for (Iterator iterator = tags.iterator(); iterator.hasNext(); ) { - PostTag postTag = iterator.next(); - if (postTag.getPost().equals(this) && - postTag.getTag().equals(tag)) { - iterator.remove(); - postTag.getTag().getPosts().remove(postTag); - postTag.setPost(null); - postTag.setTag(null); - } - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Post post = (Post) o; - return Objects.equals(title, post.title); - } - - @Override - public int hashCode() { - return Objects.hash(title); - } - } - - @Entity(name = "PostTag") @Table(name = "post_tag") - public static class PostTag implements Serializable { - - @Id - @ManyToOne - private Post post; - - @Id - @ManyToOne - private Tag tag; - - private PostTag() {} - - public PostTag(Post post, Tag tag) { - this.post = post; - this.tag = tag; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Tag getTag() { - return tag; - } - - public void setTag(Tag tag) { - this.tag = tag; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PostTag that = (PostTag) o; - return Objects.equals(post, that.post) && - Objects.equals(tag, that.tag); - } - - @Override - public int hashCode() { - return Objects.hash(post, tag); - } - } - - @Entity(name = "Tag") - public static class Tag { - - @Id - private Long id; - - private String name; - - @OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true) - private List posts = new ArrayList<>(); - - public Tag() { - } - - public Tag(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getPosts() { - return posts; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Tag tag = (Tag) o; - return Objects.equals(name, tag.name); - } - - @Override - public int hashCode() { - return Objects.hash(name); - } - } - - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyToManyListTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyToManyListTest.java deleted file mode 100644 index def489e1e..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyToManyListTest.java +++ /dev/null @@ -1,239 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.JoinTable; -import javax.persistence.ManyToMany; -import javax.persistence.Table; - -import org.hibernate.annotations.NaturalId; - -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; - -/** - * @author Vlad Mihalcea - */ -public class BidirectionalManyToManyListTest extends AbstractMySQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - Tag.class - }; - } - - @Test - public void testLifecycle() { - doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - post1.addTag(tag1); - post1.addTag(tag2); - - post2.addTag(tag1); - - entityManager.persist(post1); - entityManager.persist(post2); - - entityManager.flush(); - - post1.removeTag(tag1); - }); - } - - @Test - public void testRemove() { - final Long postId = doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - post1.addTag(tag1); - post1.addTag(tag2); - - post2.addTag(tag1); - - entityManager.persist(post1); - entityManager.persist(post2); - - return post1.id; - }); - doInJPA(entityManager -> { - LOGGER.info("Remove"); - Post post1 = entityManager.find(Post.class, postId); - - entityManager.remove(post1); - }); - } - - @Test - public void testShuffle() { - final Long postId = doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - post1.addTag(tag1); - post1.addTag(tag2); - - post2.addTag(tag1); - - entityManager.persist(post1); - entityManager.persist(post2); - - return post1.id; - }); - doInJPA(entityManager -> { - LOGGER.info("Shuffle"); - Tag tag1 = new Tag("Java"); - Post post1 = entityManager - .createQuery( - "select p " + - "from Post p " + - "join fetch p.tags " + - "where p.id = :id", Post.class) - .setParameter( "id", postId ) - .getSingleResult(); - - post1.removeTag(tag1); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Post() {} - - public Post(String title) { - this.title = title; - } - - @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE}) - @JoinTable(name = "post_tag", - joinColumns = @JoinColumn(name = "post_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id") - ) - private List tags = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getTags() { - return tags; - } - - public void addTag(Tag tag) { - tags.add(tag); - tag.getPosts().add(this); - } - - public void removeTag(Tag tag) { - tags.remove(tag); - tag.getPosts().remove(this); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Post)) return false; - return id != null && id.equals(((Post) o).id); - } - - @Override - public int hashCode() { - return 31; - } - } - - @Entity(name = "Tag") - @Table(name = "tag") - public static class Tag { - - @Id - @GeneratedValue - private Long id; - - @NaturalId - private String name; - - @ManyToMany(mappedBy = "tags") - private List posts = new ArrayList<>(); - - public Tag() {} - - public Tag(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getPosts() { - return posts; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Tag tag = (Tag) o; - return Objects.equals(name, tag.name); - } - - @Override - public int hashCode() { - return Objects.hash(name); - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyToManySetTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyToManySetTest.java deleted file mode 100644 index f56cd91ae..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyToManySetTest.java +++ /dev/null @@ -1,229 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.*; - -import org.hibernate.annotations.NaturalId; - -/** - * @author Vlad Mihalcea - */ -public class BidirectionalManyToManySetTest extends AbstractMySQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - Tag.class - }; - } - - @Test - public void testLifecycle() { - doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - post1.addTag(tag1); - post1.addTag(tag2); - - post2.addTag(tag1); - - entityManager.persist(post1); - entityManager.persist(post2); - - entityManager.flush(); - - post1.removeTag(tag1); - }); - } - - @Test - public void testRemove() { - final Long postId = doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - post1.addTag(tag1); - post1.addTag(tag2); - - post2.addTag(tag1); - - entityManager.persist(post1); - entityManager.persist(post2); - - return post1.id; - }); - doInJPA(entityManager -> { - LOGGER.info("Remove"); - Post post1 = entityManager.find(Post.class, postId); - - entityManager.remove(post1); - }); - } - - @Test - public void testShuffle() { - final Long postId = doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - post1.addTag(tag1); - post1.addTag(tag2); - - post2.addTag(tag1); - - entityManager.persist(post1); - entityManager.persist(post2); - - return post1.id; - }); - doInJPA(entityManager -> { - LOGGER.info("Shuffle"); - Tag tag1 = new Tag("Java"); - Post post1 = entityManager - .createQuery( - "select p " + - "from Post p " + - "join fetch p.tags " + - "where p.id = :id", Post.class) - .setParameter( "id", postId ) - .getSingleResult(); - - post1.removeTag(tag1); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Post() {} - - public Post(String title) { - this.title = title; - } - - @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE}) - @JoinTable(name = "post_tag", - joinColumns = @JoinColumn(name = "post_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id") - ) - private Set tags = new HashSet<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public Set getTags() { - return tags; - } - - public void addTag(Tag tag) { - tags.add(tag); - tag.getPosts().add(this); - } - - public void removeTag(Tag tag) { - tags.remove(tag); - tag.getPosts().remove(this); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Post)) return false; - return id != null && id.equals(((Post) o).id); - } - - @Override - public int hashCode() { - return 31; - } - } - - @Entity(name = "Tag") - @Table(name = "tag") - public static class Tag { - - @Id - @GeneratedValue - private Long id; - - @NaturalId - private String name; - - @ManyToMany(mappedBy = "tags") - private Set posts = new HashSet<>(); - - public Tag() {} - - public Tag(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Set getPosts() { - return posts; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Tag tag = (Tag) o; - return Objects.equals(name, tag.name); - } - - @Override - public int hashCode() { - return Objects.hash(name); - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToManyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToManyTest.java deleted file mode 100644 index 15dc607f2..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToManyTest.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public class BidirectionalOneToManyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - }; - } - - @Test - public void testLifecycle() { - doInJPA(entityManager -> { - Post post = new Post("First post"); - - post.addComment( - new PostComment("My first review") - ); - post.addComment( - new PostComment("My second review") - ); - post.addComment( - new PostComment( "My third review") - ); - - entityManager.persist(post); - }); - doInJPA(entityManager -> { - - Post post = entityManager.find( Post.class, 1L ); - PostComment comment1 = post.getComments().get( 0 ); - - post.removeComment(comment1); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) - private List comments = new ArrayList<>(); - - public Post() {} - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - - public void removeComment(PostComment comment) { - comments.remove(comment); - comment.setPost(null); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - @GeneratedValue - private Long id; - - private String review; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") - private Post post; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof PostComment )) return false; - return id != null && id.equals(((PostComment) o).id); - } - @Override - public int hashCode() { - return 31; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToOneLazyNoProxyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToOneLazyNoProxyTest.java deleted file mode 100644 index 8e72267ed..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToOneLazyNoProxyTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import java.util.List; - -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.hibernate.forum.Post; -import com.vladmihalcea.book.hpjp.hibernate.forum.PostComment; -import com.vladmihalcea.book.hpjp.hibernate.forum.PostDetails; -import com.vladmihalcea.book.hpjp.hibernate.forum.Tag; -import com.vladmihalcea.book.hpjp.util.AbstractTest; - -import static junit.framework.TestCase.assertNull; -import static junit.framework.TestCase.fail; -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -public class BidirectionalOneToOneLazyNoProxyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostDetails.class, - PostComment.class, - Tag.class - }; - } - - @Test - public void testLifecycle() { - doInJPA(entityManager -> { - Post post1 = new Post("First post"); - post1.setId( 1L ); - - PostDetails details1 = new PostDetails(); - details1.setCreatedBy( "John Doe" ); - post1.addDetails(details1); - - Post post2 = new Post("Second post"); - post2.setId( 2L ); - - entityManager.persist(post1); - entityManager.persist(post2); - }); - List posts = doInJPA(entityManager -> { - return entityManager.createQuery( - "select p " + - "from Post p ", Post.class) - .getResultList(); - }); - - try { - assertNotNull(posts.get( 0 ).getDetails()); - fail("Should throw LazyInitializationException"); - } - catch (Exception expected) { - LOGGER.info( "The @OneToOne association was fetched lazily" ); - } - - doInJPA(entityManager -> { - Post post = entityManager.createQuery( - "select p " + - "from Post p " + - "where p.id = :postId", Post.class) - .setParameter( "postId", 2L ) - .getSingleResult(); - - LOGGER.info( "Fetched Post" ); - assertNull(post.getDetails()); - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/ElementCollectionTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/ElementCollectionTest.java deleted file mode 100644 index 515b33197..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/ElementCollectionTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public class ElementCollectionTest extends AbstractMySQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Test - public void testLifecycle() { - doInJPA(entityManager -> { - Post post = new Post("First post"); - - post.getComments().add("My first review"); - post.getComments().add("My second review"); - post.getComments().add("My third review"); - - entityManager.persist(post); - entityManager.flush(); - - post.getComments().remove(2); - entityManager.flush(); - - LOGGER.info("Remove head"); - post.getComments().remove(0); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Post() {} - - public Post(String title) { - this.title = title; - } - - @ElementCollection - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/IntroAssociationTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/IntroAssociationTest.java deleted file mode 100644 index 5bff09407..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/IntroAssociationTest.java +++ /dev/null @@ -1,286 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.Criteria; -import org.hibernate.FetchMode; -import org.hibernate.Session; -import org.hibernate.criterion.Restrictions; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class IntroAssociationTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostDetails.class, - PostComment.class, - Tag.class - }; - } - - @Test - public void test() { - doInJPA(entityManager -> { - Post post = new Post(1L); - post.title = "Postit"; - - PostComment comment1 = new PostComment(); - comment1.id = 1L; - comment1.review = "Good"; - - PostComment comment2 = new PostComment(); - comment2.id = 2L; - comment2.review = "Excellent"; - - post.addComment(comment1); - post.addComment(comment2); - entityManager.persist(post); - - Session session = entityManager.unwrap(Session.class); - Criteria criteria = session.createCriteria(Post.class) - .add(Restrictions.eq("title", "post")); - LOGGER.info("Criteria: {}", criteria); - }); - - doInJPA(entityManager -> { - LOGGER.info("No alias"); - Session session = entityManager.unwrap(Session.class); - List posts = session - .createCriteria(Post.class) - .setFetchMode("comments", FetchMode.JOIN) - .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) - .add(Restrictions.eq("title", "Postit")) - .list(); - assertEquals(1, posts.size()); - }); - - doInJPA(entityManager -> { - LOGGER.info("With alias"); - Session session = entityManager.unwrap(Session.class); - List posts = session - .createCriteria(Post.class, "post") - .setFetchMode("post.comments", FetchMode.JOIN) - .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) - .add(Restrictions.eq("post.title", "Postit")) - .list(); - assertEquals(1, posts.size()); - }); - - doInJPA(entityManager -> { - LOGGER.info("With alias"); - Session session = entityManager.unwrap(Session.class); - List posts = session - .createCriteria(Post.class, "post") - .setFetchMode("post.comments", FetchMode.JOIN) - .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) - .add(Restrictions.eq("post.title", "Postit")) - .list(); - assertEquals(1, posts.size()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Post() {} - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true, fetch = FetchType.LAZY) - private PostDetails details; - - @ManyToMany - @JoinTable(name = "post_tag", - joinColumns = @JoinColumn(name = "post_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id") - ) - private List tags = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public PostDetails getDetails() { - return details; - } - - public List getTags() { - return tags; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - - public void addDetails(PostDetails details) { - this.details = details; - details.setPost(this); - } - - public void removeDetails() { - this.details.setPost(null); - this.details = null; - } - } - - @Entity(name = "PostDetails") - @Table(name = "post_details") - public static class PostDetails { - - @Id - private Long id; - - @Column(name = "created_on") - private Date createdOn; - - @Column(name = "created_by") - private String createdBy; - - public PostDetails() { - createdOn = new Date(); - } - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") - @MapsId - private Post post; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } - - @Entity(name = "Tag") - @Table(name = "tag") - public static class Tag { - - @Id - private Long id; - - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/ManyToOneTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/ManyToOneTest.java deleted file mode 100644 index 6d2806ade..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/ManyToOneTest.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import java.util.List; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; - -/** - * @author Vlad Mihalcea - */ -public class ManyToOneTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - }; - } - - @Test - public void testLifecycle() { - doInJPA(entityManager -> { - Post post = new Post("First post"); - entityManager.persist(post); - }); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - PostComment comment = new PostComment("My review"); - comment.setPost(post); - entityManager.persist(comment); - - entityManager.flush(); - comment.setPost(null); - }); - } - - @Test - public void testThreePostComments() { - doInJPA(entityManager -> { - Post post = new Post("First post"); - entityManager.persist(post); - }); - doInJPA(entityManager -> { - Post post = entityManager.getReference(Post.class, 1L); - - PostComment comment1 = new PostComment( "My first review"); - comment1.setPost( post ); - PostComment comment2 = new PostComment( "My second review"); - comment2.setPost( post ); - PostComment comment3 = new PostComment( "My third review"); - comment3.setPost( post ); - - entityManager.persist(comment1); - entityManager.persist(comment2); - entityManager.persist(comment3); - }); - - doInJPA(entityManager -> { - PostComment comment1 = entityManager.getReference(PostComment.class, 2L); - - entityManager.remove(comment1); - }); - - doInJPA(entityManager -> { - List comments = entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "where pc.post.id = :postId", PostComment.class) - .setParameter( "postId", 1L ) - .getResultList(); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Post() {} - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - @GeneratedValue - private Long id; - - private String review; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") - private Post post; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalManyToManyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalManyToManyTest.java deleted file mode 100644 index af6088c70..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalManyToManyTest.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public class UnidirectionalManyToManyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - Tag.class - }; - } - - @Test - public void testLifecycle() { - doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - post1.getTags().add(tag1); - post1.getTags().add(tag2); - - post2.getTags().add(tag1); - - entityManager.persist(post1); - entityManager.persist(post2); - - entityManager.flush(); - - LOGGER.info("Remove"); - - post1.getTags().remove(tag1); - }); - } - - @Test - public void testRemove() { - final Long postId = doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - post1.getTags().add(tag1); - post1.getTags().add(tag2); - - post2.getTags().add(tag1); - - entityManager.persist(post1); - entityManager.persist(post2); - - return post1.id; - }); - doInJPA(entityManager -> { - LOGGER.info("Remove"); - Post post1 = entityManager.find(Post.class, postId); - entityManager.remove(post1); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Post() {} - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE}) - @JoinTable(name = "post_tag", - joinColumns = @JoinColumn(name = "post_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id") - ) - private List tags = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getTags() { - return tags; - } - } - - @Entity(name = "Tag") - @Table(name = "tag") - public static class Tag { - - @Id - @GeneratedValue - private Long id; - - private String name; - - public Tag() {} - - public Tag(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnTest.java deleted file mode 100644 index eb3147b12..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public class UnidirectionalOneToManyJoinColumnTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - }; - } - - @Test - public void testRemoveTail() { - doInJPA(entityManager -> { - Post post = new Post("First post"); - - post.getComments().add(new PostComment("My first review")); - post.getComments().add(new PostComment("My second review")); - post.getComments().add(new PostComment("My third review")); - - entityManager.persist(post); - entityManager.flush(); - - LOGGER.info("Remove tail"); - post.getComments().remove(2); - }); - } - - @Test - public void testRemoveHead() { - doInJPA(entityManager -> { - Post post = new Post("First post"); - - post.getComments().add(new PostComment("My first review")); - post.getComments().add(new PostComment("My second review")); - post.getComments().add(new PostComment("My third review")); - - entityManager.persist(post); - entityManager.flush(); - - entityManager.flush(); - LOGGER.info("Remove head"); - post.getComments().remove(0); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Post() {} - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - @JoinColumn(name = "post_id") - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - @GeneratedValue - private Long id; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManySetTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManySetTest.java deleted file mode 100644 index 6bec0613d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManySetTest.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.nio.ByteBuffer; -import java.util.*; - -/** - * @author Vlad Mihalcea - */ -public class UnidirectionalOneToManySetTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - }; - } - - @Test - public void testLifecycle() { - doInJPA(entityManager -> { - Post post = new Post("First post"); - - post.getComments().add(new PostComment("My first review")); - post.getComments().add(new PostComment("My second review")); - post.getComments().add(new PostComment("My third review")); - - entityManager.persist(post); - entityManager.flush(); - - for(PostComment comment: new ArrayList<>(post.getComments())) { - post.getComments().remove(comment); - } - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Post() {} - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - private Set comments = new HashSet<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public Set getComments() { - return comments; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - @GeneratedValue - private Long id; - - private String slug; - - private String review; - - public PostComment() { - byte[] bytes = new byte[8]; - ByteBuffer.wrap(bytes).putDouble(Math.random()); - slug = Base64.getEncoder().encodeToString(bytes); - } - - public PostComment(String review) { - this(); - this.review = review; - } - - public String getSlug() { - return slug; - } - - public void setSlug(String slug) { - this.slug = slug; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PostComment comment = (PostComment) o; - return Objects.equals(slug, comment.slug); - } - - @Override - public int hashCode() { - return Objects.hash(slug); - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyTest.java deleted file mode 100644 index 8407add3f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyTest.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public class UnidirectionalOneToManyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - }; - } - - @Test - public void testLifecycle() { - doInJPA(entityManager -> { - Post post = new Post("First post"); - - post.getComments().add(new PostComment("My first review")); - post.getComments().add(new PostComment("My second review")); - post.getComments().add(new PostComment("My third review")); - - entityManager.persist(post); - entityManager.flush(); - - LOGGER.info("Remove tail"); - post.getComments().remove(2); - entityManager.flush(); - LOGGER.info("Remove head"); - post.getComments().remove(0); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Post() {} - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - @GeneratedValue - private Long id; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOrderedOneToManyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOrderedOneToManyTest.java deleted file mode 100644 index 9a85ba69d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOrderedOneToManyTest.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public class UnidirectionalOrderedOneToManyTest extends AbstractMySQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - }; - } - - @Test - public void testLifecycle() { - doInJPA(entityManager -> { - Post post = new Post("First post"); - - post.getComments().add(new PostComment("My first review")); - post.getComments().add(new PostComment("My second review")); - post.getComments().add(new PostComment("My third review")); - - entityManager.persist(post); - entityManager.flush(); - - LOGGER.info("Remove tail"); - post.getComments().remove(2); - entityManager.flush(); - LOGGER.info("Remove head"); - post.getComments().remove(0); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Post() {} - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - @OrderColumn(name = "entry") - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - @GeneratedValue - private Long id; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/BatchExceptionTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/BatchExceptionTest.java deleted file mode 100644 index 56209f458..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/BatchExceptionTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.batch; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.Session; -import org.hibernate.jdbc.Work; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.*; -import java.util.*; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class BatchExceptionTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.jdbc.batch_size", "5"); - properties.put("hibernate.order_inserts", "true"); - properties.put("hibernate.order_updates", "true"); - properties.put("hibernate.jdbc.batch_versioned_data", "true"); - return properties; - } - - @Test - public void testInsertConstraintViolation() { - LOGGER.info("testInsertPosts"); - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - session.doWork(connection -> { - - try (PreparedStatement st = connection.prepareStatement( - "INSERT INTO post (id, title) " + - "VALUES (?, ?)")) { - for (long i = 0; i < 5; i++) { - st.setLong(1, i % 2); - st.setString(2, String.format("High-Performance Java Persistence, Part %d", i)); - st.addBatch(); - } - st.executeBatch(); - } catch (BatchUpdateException e) { - LOGGER.info("Batch has managed to process {} entries", e.getUpdateCounts().length); - } - }); - - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/BatchTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/BatchTest.java deleted file mode 100644 index ad5938f74..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/BatchTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.batch; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.Session; -import org.jboss.logging.Logger; -import org.junit.Test; - -import javax.persistence.*; - -/** - * @author Vlad Mihalcea - */ -public class BatchTest extends AbstractTest { - - private static final Logger log = Logger.getLogger( BatchTest.class ); - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Test - public void testScroll() { - withBatchAndSessionManagement(); - } - - private void withBatch() { - int entityCount = 20; - EntityManager entityManager = null; - EntityTransaction txn = null; - try { - entityManager = entityManagerFactory().createEntityManager(); - entityManager.unwrap(Session.class).setJdbcBatchSize(10); - - txn = entityManager.getTransaction(); - txn.begin(); - - int entityManagerBatchSize = 20; - - for ( long i = 0; i < entityCount; ++i ) { - Post person = new Post( i, String.format( "Post nr %d", i )); - entityManager.persist( person ); - - if ( i > 0 && i % entityManagerBatchSize == 0 ) { - entityManager.flush(); - entityManager.clear(); - } - } - - txn.commit(); - } catch (RuntimeException e) { - if ( txn != null && txn.isActive()) { - txn.rollback(); - } - throw e; - } finally { - if (entityManager != null) { - entityManager.close(); - } - } - } - - private void withBatchAndSessionManagement() { - int entityCount = 20; - - doInJPA(entityManager -> { - entityManager.unwrap(Session.class).setJdbcBatchSize(10); - - for ( long i = 0; i < entityCount; ++i ) { - Post person = new Post( i, String.format( "Post nr %d", i )); - entityManager.persist( person ); - } - }); - } - - private void withBatchAndResetBackToGlobalSetting() { - EntityManager entityManager = null; - try { - entityManager = entityManagerFactory().createEntityManager(); - entityManager.getTransaction().begin(); - - - } finally { - if (entityManager != null) { - entityManager.getTransaction().rollback(); - entityManager.close(); - } - } - } - - @Entity(name = "Post") - public static class Post { - - @Id - private Long id; - - private String name; - - public Post() {} - - public Post(long id, String name) { - this.id = id; - this.name = name; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/BatchingTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/BatchingTest.java deleted file mode 100644 index e8e9797ab..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/BatchingTest.java +++ /dev/null @@ -1,251 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.batch; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Properties; - -/** - * BatchingTest - Test to check the JDBC batch support - * - * @author Vlad Mihalcea - */ -public class BatchingTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.jdbc.batch_size", "5"); - properties.put("hibernate.order_inserts", "true"); - properties.put("hibernate.order_updates", "true"); - properties.put("hibernate.jdbc.batch_versioned_data", "true"); - return properties; - } - - @Test - public void testInsertPosts() { - LOGGER.info("testInsertPosts"); - insertPosts(); - } - - @Test - public void testInsertPostsAndComments() { - LOGGER.info("testInsertPostsAndComments"); - insertPostsAndComments(); - } - - @Test - public void testUpdatePosts() { - insertPosts(); - - LOGGER.info("testUpdatePosts"); - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select p " + - "from Post p ", Post.class) - .getResultList(); - - posts.forEach(post -> post.setTitle(post.getTitle().replaceAll("no", "nr"))); - }); - } - - @Test - public void testUpdatePostsAndComments() { - insertPostsAndComments(); - - LOGGER.info("testUpdatePostsAndComments"); - doInJPA(entityManager -> { - List comments = entityManager.createQuery( - "select c " + - "from PostComment c " + - "join fetch c.post ", PostComment.class) - .getResultList(); - - comments.forEach(comment -> { - comment.setReview(comment.getReview().replaceAll("Good", "Very good")); - Post post = comment.getPost(); - post.setTitle(post.getTitle().replaceAll("no", "nr")); - }); - }); - } - - @Test - public void testDeletePosts() { - insertPosts(); - - LOGGER.info("testDeletePosts"); - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select p " + - "from Post p ", Post.class) - .getResultList(); - - posts.forEach(entityManager::remove); - }); - } - - @Test - public void testDeletePostsAndComments() { - insertPostsAndComments(); - - LOGGER.info("testDeletePostsAndComments"); - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.comments ", Post.class) - .getResultList(); - - posts.forEach(entityManager::remove); - }); - } - - @Test - public void testDeletePostsAndCommentsWithManualChildRemoval() { - insertPostsAndComments(); - - LOGGER.info("testDeletePostsAndCommentsWithManualChildRemoval"); - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.comments ", Post.class) - .getResultList(); - - for (Post post : posts) { - for (Iterator commentIterator = post.getComments().iterator(); - commentIterator.hasNext(); ) { - PostComment comment = commentIterator.next(); - comment.setPost(null); - commentIterator.remove(); - } - } - entityManager.flush(); - posts.forEach(entityManager::remove); - }); - } - - private void insertPosts() { - doInJPA(entityManager -> { - for (int i = 0; i < 3; i++) { - entityManager.persist(new Post(String.format("Post no. %d", i + 1))); - } - }); - } - - private void insertPostsAndComments() { - doInJPA(entityManager -> { - for (int i = 0; i < 3; i++) { - Post post = new Post(String.format("Post no. %d", i)); - post.addComment(new PostComment("Good")); - entityManager.persist(post); - } - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE) - private Long id; - - private String title; - - public Post() {} - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE) - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/DeletingWithSQLCascadeBatchingTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/DeletingWithSQLCascadeBatchingTest.java deleted file mode 100644 index c7fa5dfe1..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/DeletingWithSQLCascadeBatchingTest.java +++ /dev/null @@ -1,212 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.batch; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.OnDelete; -import org.hibernate.annotations.OnDeleteAction; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -import static org.junit.Assert.assertEquals; - -/** - * DeletingWithoutCascadeBatchingTest - Test to check the JDBC batch support for delete - * - * @author Vlad Mihalcea - */ -public class DeletingWithSQLCascadeBatchingTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.jdbc.batch_size", "5"); - properties.put("hibernate.order_inserts", "true"); - properties.put("hibernate.order_updates", "true"); - properties.put("hibernate.jdbc.batch_versioned_data", "true"); - return properties; - } - - @Test - public void testDeletePostsAndCommentsWithSQLCascade() { - insertPostsAndComments(); - - LOGGER.info("testDeletePostsAndCommentsWithSQLCascade"); - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select p " + - "from Post p ", Post.class) - .getResultList(); - - posts.forEach(entityManager::remove); - }); - } - - @Test - public void testDeletePostsAndCommentsWithSQLCascadeAndManagedChildren() { - insertPostsAndComments(); - - LOGGER.info("testDeletePostsAndCommentsWithSQLCascadeAndManagedChildren"); - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select p " + - "from Post p ", Post.class) - .getResultList(); - - List comments = entityManager.createQuery( - "select c " + - "from PostComment c " + - "where c.post in :posts", PostComment.class) - .setParameter("posts", posts) - .getResultList(); - - posts.forEach(entityManager::remove); - - comments.forEach(comment -> comment.setReview("Excellent")); - }); - } - - @Test - public void testDeletePostsAndCommentsWithSQLCascadeAndManagedChildrenFloating() { - insertPostsAndComments(); - - LOGGER.info("testDeletePostsAndCommentsWithSQLCascadeAndManagedChildrenFloating"); - try { - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select p " + - "from Post p ", Post.class) - .getResultList(); - - List comments = entityManager.createQuery( - "select c " + - "from PostComment c " + - "where c.post in :posts", PostComment.class) - .setParameter("posts", posts) - .getResultList(); - - posts.forEach(entityManager::remove); - entityManager.flush(); - - comments.forEach(comment -> comment.setReview("Excellent")); - }); - } catch (PersistenceException e) { - assertEquals(OptimisticLockException.class, e.getCause().getClass()); - } - } - - private void insertPostsAndComments() { - doInJPA(entityManager -> { - for (int i = 0; i < 3; i++) { - Post post = new Post(String.format("Post no. %d", i)); - post.addComment(new PostComment("Good")); - entityManager.persist(post); - } - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE) - private Long id; - - private String title; - - public Post() {} - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "post") - @OnDelete(action = OnDeleteAction.CASCADE) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE) - private Long id; - - @ManyToOne - @org.hibernate.annotations.ForeignKey(name = "fk_post_comment_post") - private Post post; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/DeletingWithoutCascadeBatchingTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/DeletingWithoutCascadeBatchingTest.java deleted file mode 100644 index 41ec58e1d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/DeletingWithoutCascadeBatchingTest.java +++ /dev/null @@ -1,161 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.batch; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * DeletingWithoutCascadeBatchingTest - Test to check the JDBC batch support for delete - * - * @author Vlad Mihalcea - */ -public class DeletingWithoutCascadeBatchingTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.jdbc.batch_size", "5"); - properties.put("hibernate.order_inserts", "true"); - properties.put("hibernate.order_updates", "true"); - properties.put("hibernate.jdbc.batch_versioned_data", "true"); - return properties; - } - - @Test - public void testDeletePostsAndCommentsWithBulkDelete() { - insertPostsAndComments(); - - LOGGER.info("testDeletePostsAndCommentsWithBulkDelete"); - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select p " + - "from Post p " + - "where p.title like 'Post no%'", Post.class) - .getResultList(); - - entityManager.createQuery( - "delete " + - "from PostComment c " + - "where c.post in :posts") - .setParameter("posts", posts) - .executeUpdate(); - - posts.forEach(entityManager::remove); - }); - } - - private void insertPostsAndComments() { - doInJPA(entityManager -> { - for (int i = 0; i < 3; i++) { - Post post = new Post(String.format("Post no. %d", i)); - post.addComment(new PostComment("Good")); - entityManager.persist(post); - } - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE) - private Long id; - - private String title; - - public Post() {} - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "post") - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE) - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/binding/EntityGraphMapperTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/binding/EntityGraphMapperTest.java deleted file mode 100644 index f746fd550..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/binding/EntityGraphMapperTest.java +++ /dev/null @@ -1,166 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.binding; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.exception.DataAccessException; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import org.junit.Test; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.Post; -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.PostComment; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * EntityGraphMapperTest - Test mapping to entity - * - * @author Vlad Mihalcea - */ -public class EntityGraphMapperTest extends AbstractTest { - - public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; - - public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"; - - public static final String INSERT_POST_DETAILS= "insert into post_details (id, created_on, version) values (?, ?, ?)"; - - private BlogEntityProvider entityProvider = new BlogEntityProvider(); - - private Long id = 1L; - private long expectedCount = 2; - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Override - public void init() { - super.init(); - doInJDBC(connection -> { - try ( - PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); - PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); - PreparedStatement postDetailsStatement = connection.prepareStatement(INSERT_POST_DETAILS); - ) { - - int postCount = getPostCount(); - int postCommentCount = getPostCommentCount(); - - int index; - - for (int i = 0; i < postCount; i++) { - if (i > 0 && i % 100 == 0) { - postStatement.executeBatch(); - postDetailsStatement.executeBatch(); - } - - index = 0; - postStatement.setString(++index, String.format("Post no. %1$d", i)); - postStatement.setInt(++index, i); - postStatement.setLong(++index, i); - postStatement.addBatch(); - - index = 0; - postDetailsStatement.setInt(++index, i); - postDetailsStatement.setTimestamp(++index, new Timestamp(System.currentTimeMillis())); - postDetailsStatement.setInt(++index, i); - postDetailsStatement.addBatch(); - } - postStatement.executeBatch(); - postDetailsStatement.executeBatch(); - - for (int i = 0; i < postCount; i++) { - for (int j = 0; j < postCommentCount; j++) { - index = 0; - postCommentStatement.setLong(++index, i); - postCommentStatement.setString(++index, String.format("Post comment %1$d", j)); - postCommentStatement.setInt(++index, i); - postCommentStatement.setLong(++index, (postCommentCount * i) + j); - postCommentStatement.addBatch(); - if (j % 100 == 0) { - postCommentStatement.executeBatch(); - } - } - } - postCommentStatement.executeBatch(); - } catch (SQLException e) { - fail(e.getMessage()); - } - }); - } - - @Test - public void testJdbcOneToManyMapping() { - doInJDBC(connection -> { - try (PreparedStatement statement = connection.prepareStatement( - "SELECT * " + - "FROM post AS p " + - "JOIN post_comment AS pc ON p.id = pc.post_id " + - "WHERE " + - " p.id BETWEEN ? AND ? + 1" - )) { - statement.setLong(1, id); - statement.setLong(2, id); - try (ResultSet resultSet = statement.executeQuery()) { - List posts = toPosts(resultSet); - assertEquals(expectedCount, posts.size()); - } - } catch (SQLException e) { - throw new DataAccessException( e); - } - }); - } - - private List toPosts(ResultSet resultSet) throws SQLException { - Map postMap = new LinkedHashMap<>(); - while (resultSet.next()) { - Long postId = resultSet.getLong(1); - Post post = postMap.get(postId); - if(post == null) { - post = new Post(postId); - postMap.put(postId, post); - post.setTitle(resultSet.getString(2)); - post.setVersion(resultSet.getInt(3)); - } - PostComment comment = new PostComment(); - comment.setId(resultSet.getLong(4)); - comment.setReview(resultSet.getString(5)); - comment.setVersion(resultSet.getInt(6)); - post.addComment(comment); - } - return new ArrayList<>(postMap.values()); - } - - @Test - public void testJPAParameterBinding() { - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select distinct p " + - "from Post p " + - "join fetch p.comments " + - "where " + - " p.id BETWEEN :id AND :id + 1", - Post.class) - .setParameter("id", id) - .getResultList(); - assertEquals(expectedCount, posts.size()); - }); - } - - protected int getPostCount() { - return 10; - } - - protected int getPostCommentCount() { - return 10; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/bytecode/BytecodeEnhancedOneToOneTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/bytecode/BytecodeEnhancedOneToOneTest.java deleted file mode 100644 index c33d62073..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/bytecode/BytecodeEnhancedOneToOneTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.bytecode; - -import com.vladmihalcea.book.hpjp.hibernate.forum.Post; -import com.vladmihalcea.book.hpjp.hibernate.forum.PostComment; -import com.vladmihalcea.book.hpjp.hibernate.forum.PostDetails; -import com.vladmihalcea.book.hpjp.hibernate.forum.Tag; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -/** - * @author Vlad Mihalcea - */ -public class BytecodeEnhancedOneToOneTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostDetails.class, - PostComment.class, - Tag.class - }; - } - - @Test - public void testDirtyChecking() { - doInJPA(entityManager -> { - Post post = new Post("First post"); - post.setId(1L); - PostDetails details = new PostDetails(); - post.addDetails(details); - entityManager.persist(post); - }); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/bytecode/BytecodeEnhancedTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/bytecode/BytecodeEnhancedTest.java deleted file mode 100644 index 7fc67de1a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/bytecode/BytecodeEnhancedTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.bytecode; - -import com.vladmihalcea.book.hpjp.hibernate.forum.Post; -import com.vladmihalcea.book.hpjp.hibernate.forum.PostComment; -import com.vladmihalcea.book.hpjp.hibernate.forum.PostDetails; -import com.vladmihalcea.book.hpjp.hibernate.forum.Tag; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -/** - * BytecodeEnhancedTest - Test to check dirty checking capabilities - * - * @author Vlad Mihalcea - */ -public class BytecodeEnhancedTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostDetails.class, - PostComment.class, - Tag.class - }; - } - - @Test - public void testDirtyChecking() { - doInJPA(entityManager -> { - Post post = new Post(1L); - post.setTitle("Postit"); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("Good"); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Excellent"); - - post.addComment(comment1); - post.addComment(comment2); - entityManager.persist(post); - }); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - - post.setTitle("Post it"); - entityManager.flush(); - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/CollectionHydratedStateTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/CollectionHydratedStateTest.java deleted file mode 100644 index 75c1bbdb5..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/CollectionHydratedStateTest.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.Cache; -import org.hibernate.annotations.CacheConcurrencyStrategy; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class CollectionHydratedStateTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); - properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); - return properties; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("JDBC part review"); - post.addComment(comment1); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Hibernate part review"); - post.addComment(comment2); - - entityManager.persist(post); - }); - } - - @Test - public void testEntityLoad() { - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - }); - - doInJPA(entityManager -> { - LOGGER.info("Load from cache"); - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - }); - - printCacheRegionStatistics(Post.class.getName() + ".comments"); - } - - @Entity(name = "Post") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - public static class Post { - - @Id - private Long id; - - private String title; - - @Version - private int version; - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - public static class PostComment { - - @Id - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/EntityHydratedStateTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/EntityHydratedStateTest.java deleted file mode 100644 index 917d8ef27..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/EntityHydratedStateTest.java +++ /dev/null @@ -1,210 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.Cache; -import org.hibernate.annotations.CacheConcurrencyStrategy; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.*; -import java.util.Date; -import java.util.Properties; - -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -public class EntityHydratedStateTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostDetails.class, - PostComment.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); - properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); - return properties; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - - PostDetails details = new PostDetails(); - details.setCreatedBy("Vlad Mihalcea"); - details.setCreatedOn(new Date()); - details.setPost(post); - entityManager.persist(details); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("JDBC part review"); - comment1.setPost(post); - entityManager.persist(comment1); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Hibernate part review"); - comment2.setPost(post); - entityManager.persist(comment2); - }); - } - - @Test - public void testEntityLoad() { - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertNotNull(post); - PostDetails details = entityManager.find(PostDetails.class, 1L); - assertNotNull(details); - PostComment comment = entityManager.find(PostComment.class, 1L); - assertNotNull(comment); - }); - - printCacheRegionStatistics(Post.class.getName()); - - doInJPA(entityManager -> { - LOGGER.info("Load from cache"); - Post post = entityManager.find(Post.class, 1L); - assertNotNull(post); - PostDetails details = entityManager.find(PostDetails.class, 1L); - assertNotNull(details); - PostComment comment = entityManager.find(PostComment.class, 1L); - assertNotNull(comment); - }); - - printCacheRegionStatistics(Post.class.getName()); - - } - - @Entity(name = "Post") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - public static class Post { - - @Id - private Long id; - - private String title; - - @Version - private int version; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } - - @Entity(name = "PostDetails") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - public static class PostDetails { - - @Id - private Long id; - - @Column(name = "created_on") - private Date createdOn; - - @Column(name = "created_by") - private String createdBy; - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") - @MapsId - private Post post; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - } - - @Entity(name = "PostComment") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - public static class PostComment { - - @Id - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/HydratedStateBenchmarkTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/HydratedStateBenchmarkTest.java deleted file mode 100644 index a0a52158a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/HydratedStateBenchmarkTest.java +++ /dev/null @@ -1,196 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache; - -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Slf4jReporter; -import com.codahale.metrics.Timer; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.CacheConcurrencyStrategy; -import org.hibernate.annotations.Immutable; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import javax.persistence.*; -import java.io.Serializable; -import java.util.*; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertNotNull; - - -/** - * @author Vlad Mihalcea - */ -@RunWith(Parameterized.class) -public class HydratedStateBenchmarkTest extends AbstractTest { - - private MetricRegistry metricRegistry = new MetricRegistry(); - - private Timer timer = metricRegistry.timer(getClass().getSimpleName()); - - private Slf4jReporter logReporter = Slf4jReporter - .forRegistry(metricRegistry) - .outputTo(LOGGER) - .build(); - - private int insertCount; - - public HydratedStateBenchmarkTest(int insertCount) { - this.insertCount = insertCount; - } - - @Parameterized.Parameters - public static Collection dataProvider() { - List providers = new ArrayList<>(); - providers.add(new Object[]{100}); - providers.add(new Object[]{500}); - providers.add(new Object[]{1000}); - providers.add(new Object[]{5000}); - providers.add(new Object[]{10000}); - return providers; - } - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostDetails.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); - - properties.put("hibernate.jdbc.batch_size", "100"); - properties.put("hibernate.order_inserts", "true"); - return properties; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - for (long i = 0; i < insertCount; i++) { - Post post = new Post(); - post.setId(i); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); -/* - PostDetails details = new PostDetails(); - details.setCreatedBy("Vlad Mihalcea"); - details.setCreatedOn(new Date()); - details.setPost(post); - entityManager.persist(details);*/ - } - }); - } - - @Test - public void testReadOnlyFetchPerformance() { - //warming-up - doInJPA(entityManager -> { - for (long i = 0; i < 10000; i++) { - Post post = entityManager.find(Post.class, i % insertCount); - //PostDetails details = entityManager.find(PostDetails.class, i); - assertNotNull(post); - } - }); - doInJPA(entityManager -> { - long startNanos = System.nanoTime(); - for (long i = 0; i < insertCount; i++) { - Post post = entityManager.find(Post.class, i); - //PostDetails details = entityManager.find(PostDetails.class, i); - } - timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - }); - logReporter.report(); - } - - - @Entity(name = "Post") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - @org.hibernate.annotations.Immutable - public static class Post implements Serializable { - - @Id - private Long id; - - private String title; - - @Version - private int version; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } - - @Entity(name = "PostDetails") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - @Immutable - //This does not work since it features an association type - public static class PostDetails implements Serializable { - - @Id - private Long id; - - @Column(name = "created_on") - private Date createdOn; - - @Column(name = "created_by") - private String createdBy; - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") - @MapsId - private Post post; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/HydratedStateReferenceEntitiesTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/HydratedStateReferenceEntitiesTest.java deleted file mode 100644 index c9604267a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/HydratedStateReferenceEntitiesTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache; - -import java.util.Properties; - - -/** - * ReadOnlyCacheConcurrencyStrategyReferenceEntitiesTest - Test to check CacheConcurrencyStrategy.READ_ONLY - * with hibernate.cache.use_reference_entries doesn't work because Commit has a collection of CommitChanges - * - * @author Vlad Mihalcea - */ -public class HydratedStateReferenceEntitiesTest extends HydratedStateBenchmarkTest { - - public HydratedStateReferenceEntitiesTest(int insertCount) { - super(insertCount); - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.cache.use_reference_entries", Boolean.TRUE.toString()); - return properties; - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/QueryCacheTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/QueryCacheTest.java deleted file mode 100644 index 15b1522de..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/QueryCacheTest.java +++ /dev/null @@ -1,350 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.SQLQuery; -import org.hibernate.annotations.CacheConcurrencyStrategy; -import org.hibernate.cache.internal.StandardQueryCache; -import org.hibernate.jpa.QueryHints; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -/** - * QueryCacheTest - Test to check the 2nd level query cache - * - * @author Vlad Mihalcea - */ -public class QueryCacheTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class, - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); - properties.put("hibernate.cache.use_query_cache", Boolean.TRUE.toString()); - return properties; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - PostComment comment = new PostComment(); - comment.setId(1L); - comment.setReview("JDBC part review"); - post.addComment(comment); - - entityManager.persist(post); - }); - } - - @After - public void destroy() { - entityManagerFactory().getCache().evictAll(); - super.destroy(); - } - - public List getLatestPostComments(EntityManager entityManager) { - return entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "order by pc.post.id desc", PostComment.class) - .setMaxResults(10) - .setHint(QueryHints.HINT_CACHEABLE, true) - .getResultList(); - } - - private List getLatestPostCommentsByPostId(EntityManager entityManager) { - return entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "where pc.post.id = :postId", PostComment.class) - .setParameter("postId", 1L) - .setMaxResults(10) - .setHint(QueryHints.HINT_CACHEABLE, true) - .getResultList(); - } - - private List getLatestPostCommentsByPost(EntityManager entityManager) { - Post post = entityManager.find(Post.class, 1L); - return entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "where pc.post = :post ", PostComment.class) - .setParameter("post", post) - .setMaxResults(10) - .setHint(QueryHints.HINT_CACHEABLE, true) - .getResultList(); - } - - private List getPostCommentSummaryByPost(EntityManager entityManager) { - return entityManager.createQuery( - "select new com.vladmihalcea.book.hpjp.hibernate.cache.QueryCacheTest$PostCommentSummary(pc.id, p.title, pc.review) " + - "from PostComment pc " + - "left join pc.post p " + - "where p.id = :postId ", PostCommentSummary.class) - .setParameter("postId", 1L) - .setMaxResults(10) - .setHint(QueryHints.HINT_CACHEABLE, true) - .getResultList(); - } - - @Test - public void test2ndLevelCacheWithoutResults() { - doInJPA(entityManager -> { - entityManager.createQuery("delete from PostComment").executeUpdate(); - }); - doInJPA(entityManager -> { - LOGGER.info("Query cache with basic type parameter"); - List comments = getLatestPostCommentsByPostId(entityManager); - assertTrue(comments.isEmpty()); - }); - doInJPA(entityManager -> { - LOGGER.info("Query cache with entity type parameter"); - List comments = getLatestPostCommentsByPostId(entityManager); - assertTrue(comments.isEmpty()); - }); - } - - @Test - public void test2ndLevelCacheWithQuery() { - doInJPA(entityManager -> { - printCacheRegionStatistics(StandardQueryCache.class.getName()); - assertEquals(1, getLatestPostComments(entityManager).size()); - printCacheRegionStatistics(StandardQueryCache.class.getName()); - }); - } - - @Test - public void test2ndLevelCacheWithParameters() { - doInJPA(entityManager -> { - LOGGER.info("Query cache with basic type parameter"); - List comments = getLatestPostCommentsByPostId(entityManager); - assertEquals(1, comments.size()); - }); - doInJPA(entityManager -> { - LOGGER.info("Query cache with entity type parameter"); - List comments = getLatestPostCommentsByPost(entityManager); - assertEquals(1, comments.size()); - }); - } - - @Test - public void test2ndLevelCacheWithProjection() { - doInJPA(entityManager -> { - LOGGER.info("Query cache with projection"); - List comments = getPostCommentSummaryByPost(entityManager); - assertEquals(1, comments.size()); - }); - doInJPA(entityManager -> { - LOGGER.info("Query cache with projection"); - List comments = getPostCommentSummaryByPost(entityManager); - assertEquals(1, comments.size()); - }); - } - - @Test - public void test2ndLevelCacheWithQueryInvalidation() { - doInJPA(entityManager -> { - - assertEquals(1, getLatestPostComments(entityManager).size()); - printCacheRegionStatistics(StandardQueryCache.class.getName()); - - LOGGER.info("Insert a new PostComment"); - PostComment newComment = new PostComment(); - newComment.setId(2L); - newComment.setReview("JDBC part review"); - Post post = entityManager.find(Post.class, 1L); - post.addComment(newComment); - entityManager.flush(); - - assertEquals(2, getLatestPostComments(entityManager).size()); - printCacheRegionStatistics(StandardQueryCache.class.getName()); - }); - - LOGGER.info("After transaction commit"); - printCacheRegionStatistics(StandardQueryCache.class.getName()); - - doInJPA(entityManager -> { - LOGGER.info("Check query cache"); - assertEquals(2, getLatestPostComments(entityManager).size()); - }); - printCacheRegionStatistics(StandardQueryCache.class.getName()); - } - - @Test - public void test2ndLevelCacheWithNativeQueryInvalidation() { - doInJPA(entityManager -> { - assertEquals(1, getLatestPostComments(entityManager).size()); - printCacheRegionStatistics(StandardQueryCache.class.getName()); - - int postCount = ((Number) entityManager.createNativeQuery( - "SELECT count(*) FROM post") - .getSingleResult()).intValue(); - - assertEquals(postCount, getLatestPostComments(entityManager).size()); - printCacheRegionStatistics(StandardQueryCache.class.getName()); - }); - } - - @Test - public void test2ndLevelCacheWithNativeUpdateStatementInvalidation() { - doInJPA(entityManager -> { - assertEquals(1, getLatestPostComments(entityManager).size()); - printCacheRegionStatistics(StandardQueryCache.class.getName()); - - entityManager.createNativeQuery( - "UPDATE post SET title = '\"'||title||'\"' ") - .executeUpdate(); - - assertEquals(1, getLatestPostComments(entityManager).size()); - printCacheRegionStatistics(StandardQueryCache.class.getName()); - }); - } - - @Test - public void test2ndLevelCacheWithNativeUpdateStatementSynchronization() { - doInJPA(entityManager -> { - assertEquals(1, getLatestPostComments(entityManager).size()); - printCacheRegionStatistics(StandardQueryCache.class.getName()); - - LOGGER.info("Execute native query with synchronization"); - entityManager.createNativeQuery( - "UPDATE post SET title = '\"'||title||'\"' ") - .unwrap(SQLQuery.class) - .addSynchronizedEntityClass(Post.class) - .executeUpdate(); - - assertEquals(1, getLatestPostComments(entityManager).size()); - printCacheRegionStatistics(StandardQueryCache.class.getName()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - public static class Post { - - @Id - private Long id; - - private String title; - - @Version - private int version; - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - public static class PostComment { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } - - public static class PostCommentSummary { - - private Long commentId; - - private String title; - - private String review; - - public PostCommentSummary(Long commentId, String title, String review) { - this.commentId = commentId; - this.title = title; - this.review = review; - } - - public Long getCommentId() { - return commentId; - } - - public String getTitle() { - return title; - } - - public String getReview() { - return review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/QueryHydratedStateTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/QueryHydratedStateTest.java deleted file mode 100644 index 694cde591..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/QueryHydratedStateTest.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.Session; -import org.hibernate.annotations.Cache; -import org.hibernate.annotations.CacheConcurrencyStrategy; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Version; -import java.util.List; -import java.util.Properties; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class QueryHydratedStateTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); - properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); - properties.put("hibernate.cache.use_query_cache", Boolean.TRUE.toString()); - return properties; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post1 = new Post(); - post1.setId(1L); - post1.setTitle("High-Performance Java Persistence"); - - entityManager.persist(post1); - - Post post2 = new Post(); - post2.setId(2L); - post2.setTitle("High-Performance Hibernate"); - - entityManager.persist(post2); - }); - } - - @Test - public void testEntityLoad() { - - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select p " + - "from Post p " + - "where p.title like :token", Post.class) - .setParameter("token", "High-Performance%") - .setHint("org.hibernate.cacheable", true) - .getResultList(); - assertEquals(1, posts.size()); - }); - - doInJPA(entityManager -> { - LOGGER.info("Load from cache"); - List posts = entityManager.createQuery( - "select p " + - "from Post p " + - "where p.title like :token", Post.class) - .setParameter("token", "High-Performance%") - .setHint("org.hibernate.cacheable", true) - .getResultList(); - assertEquals(1, posts.size()); - }); - - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - List posts = (List) session.createQuery( - "select p " + - "from Post p " + - "where p.title like :token") - .setParameter("token", "High-Performance%") - .setCacheable(true) - .list(); - assertEquals(1, posts.size()); - }); - } - - @Entity(name = "Post") - @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - public static class Post { - - @Id - private Long id; - - private String title; - - @Version - private int version; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/nonstrictreadwrite/NonStrictReadWriteCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/nonstrictreadwrite/NonStrictReadWriteCacheConcurrencyStrategyTest.java deleted file mode 100644 index 0b7fa2698..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/nonstrictreadwrite/NonStrictReadWriteCacheConcurrencyStrategyTest.java +++ /dev/null @@ -1,234 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.nonstrictreadwrite; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.CacheConcurrencyStrategy; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - - -/** - * @author Vlad Mihalcea - */ -public class NonStrictReadWriteCacheConcurrencyStrategyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); - return properties; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("JDBC part review"); - post.addComment(comment1); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Hibernate part review"); - post.addComment(comment2); - - entityManager.persist(post); - }); - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - LOGGER.info("Post entity inserted"); - } - - @Test - public void testPostEntityLoad() { - - LOGGER.info("Load Post entity and comments collection"); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - printCacheRegionStatistics(post.getClass().getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - }); - } - - @Test - public void testPostEntityEvictModifyLoad() { - - LOGGER.info("Evict, modify, load"); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.detach(post); - - post.setTitle("High-Performance Hibernate"); - entityManager.merge(post); - entityManager.flush(); - - entityManager.detach(post); - post = entityManager.find(Post.class, 1L); - printCacheRegionStatistics(post.getClass().getName()); - }); - } - - @Test - public void testEntityUpdate() { - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - }); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - post.setTitle("High-Performance Hibernate"); - PostComment comment = post.getComments().remove(0); - comment.setPost(null); - }); - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - } - - @Test - public void testNonVersionedEntityUpdate() { - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - }); - printCacheRegionStatistics(PostComment.class.getName()); - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - comment.setReview("JDBC and Database part review"); - }); - printCacheRegionStatistics(PostComment.class.getName()); - } - - @Test - public void testEntityDelete() { - LOGGER.info("Cache entries can be deleted"); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - }); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.remove(post); - }); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertNull(post); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) - public static class Post { - - @Id - private Long id; - - private String title; - - @Version - private int version; - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) - public static class PostComment { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readonly/IdentityReadOnlyCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readonly/IdentityReadOnlyCacheConcurrencyStrategyTest.java deleted file mode 100644 index f41ffbc59..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readonly/IdentityReadOnlyCacheConcurrencyStrategyTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.readonly; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.CacheConcurrencyStrategy; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.*; -import java.util.Properties; - - -/** - * CacheConcurrencyStrategyTest - Test to check CacheConcurrencyStrategy.READ_ONLY - * - * @author Vlad Mihalcea - */ -public class IdentityReadOnlyCacheConcurrencyStrategyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); - return properties; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - }); - printCacheRegionStatistics(Post.class.getName()); - LOGGER.info("Post entity inserted"); - } - - @Test - public void testPostEntityLoad() { - - LOGGER.info("Entities are not loaded from cache for identity as it's read-through"); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - printCacheRegionStatistics(post.getClass().getName()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) - public static class Post { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String title; - - @Version - private int version; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } -} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readonly/ReadOnlyCacheConcurrencyStrategyImmutableTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readonly/ReadOnlyCacheConcurrencyStrategyImmutableTest.java deleted file mode 100644 index bf9eb7c1a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readonly/ReadOnlyCacheConcurrencyStrategyImmutableTest.java +++ /dev/null @@ -1,167 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.readonly; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.CacheConcurrencyStrategy; -import org.hibernate.annotations.Immutable; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - - -/** - * @author Vlad Mihalcea - */ -public class ReadOnlyCacheConcurrencyStrategyImmutableTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); - return properties; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("JDBC part review"); - post.addComment(comment1); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Hibernate part review"); - post.addComment(comment2); - - entityManager.persist(post); - }); - printCacheRegionStatistics(Post.class.getName()); - LOGGER.info("Post entity inserted"); - } - - @Test - public void testReadOnlyEntityUpdate() { - LOGGER.info("Read-only cache entries cannot be updated"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - post.setTitle("High-Performance Hibernate"); - }); - } - - @Test - public void testCollectionCacheUpdate() { - - LOGGER.info("Read-only collection cache entries cannot be updated"); - - try { - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - PostComment comment = post.getComments().remove(0); - comment.setPost(null); - }); - } catch (Exception e) { - LOGGER.error("Expected", e); - } - } - - @Entity(name = "Post") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) - @Immutable - public static class Post { - - @Id - private Long id; - - private String title; - - @Version - private int version; - - @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "post") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) - @Immutable - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) - @Immutable - public static class PostComment { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readonly/ReadOnlyCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readonly/ReadOnlyCacheConcurrencyStrategyTest.java deleted file mode 100644 index 8344d1a20..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readonly/ReadOnlyCacheConcurrencyStrategyTest.java +++ /dev/null @@ -1,275 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.readonly; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.CacheConcurrencyStrategy; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -import static org.junit.Assert.*; - - -/** - * CacheConcurrencyStrategyTest - Test to check CacheConcurrencyStrategy.READ_ONLY - * - * @author Vlad Mihalcea - */ -public class ReadOnlyCacheConcurrencyStrategyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); - return properties; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - }); - printCacheRegionStatistics(Post.class.getName()); - LOGGER.info("Post entity inserted"); - } - - @Test - public void testPostEntityLoad() { - - LOGGER.info("Entities are loaded from cache"); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - printCacheRegionStatistics(post.getClass().getName()); - }); - } - - @Test - public void testCollectionCacheLoad() { - LOGGER.info("Collections require separate caching"); - doInJPA(entityManager -> { - - Post post = entityManager.find(Post.class, 1L); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("JDBC part review"); - post.addComment(comment1); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Hibernate part review"); - post.addComment(comment2); - }); - - printCacheRegionStatistics(Post.class.getName() + ".comments"); - - doInJPA(entityManager -> { - LOGGER.info("Load PostComment from database"); - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - }); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(PostComment.class.getName()); - - doInJPA(entityManager -> { - LOGGER.info("Load PostComment from cache"); - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - }); - } - - @Test - public void testCollectionCacheUpdate() { - - doInJPA(entityManager -> { - - Post post = entityManager.find(Post.class, 1L); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("JDBC part review"); - post.addComment(comment1); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Hibernate part review"); - post.addComment(comment2); - }); - - LOGGER.info("Collection cache entries cannot be updated"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - PostComment comment = post.getComments().remove(0); - comment.setPost(null); - }); - - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - try { - doInJPA(entityManager -> { - LOGGER.info("Load PostComment from cache"); - Post post = entityManager.find(Post.class, 1L); - assertEquals(1, post.getComments().size()); - }); - } catch (Exception e) { - LOGGER.error("Expected", e); - } - } - - @Test - public void testEntityUpdate() { - try { - LOGGER.info("Cache entries cannot be updated"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - post.setTitle("High-Performance Hibernate"); - }); - } catch (Exception e) { - LOGGER.error("Expected", e); - } - } - - @Test - public void testEntityDelete() { - LOGGER.info("Cache entries can be deleted"); - - doInJPA(entityManager -> { - - Post post = entityManager.find(Post.class, 1L); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("JDBC part review"); - post.addComment(comment1); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Hibernate part review"); - post.addComment(comment2); - }); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - }); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.remove(post); - }); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertNull(post); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) - public static class Post { - - @Id - private Long id; - - private String title; - - @Version - private int version; - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) - public static class PostComment { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/IdentityReadWriteCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/IdentityReadWriteCacheConcurrencyStrategyTest.java deleted file mode 100644 index 7ac9e25cf..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/IdentityReadWriteCacheConcurrencyStrategyTest.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.readwrite; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.CacheConcurrencyStrategy; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - - -/** - * @author Vlad Mihalcea - */ -public class IdentityReadWriteCacheConcurrencyStrategyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); - return properties; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - }); - printCacheRegionStatistics(Post.class.getName()); - LOGGER.info("Post entity inserted"); - } - - @Test - public void testPostEntityLoad() { - - LOGGER.info("Load Post entity and comments collection"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - printCacheRegionStatistics(post.getClass().getName()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - public static class Post { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String title; - - @Version - private int version; - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - public static class PostComment { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/ReadWriteCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/ReadWriteCacheConcurrencyStrategyTest.java deleted file mode 100644 index 6250abac7..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/ReadWriteCacheConcurrencyStrategyTest.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.readwrite; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.CacheConcurrencyStrategy; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - - -/** - * @author Vlad Mihalcea - */ -public class ReadWriteCacheConcurrencyStrategyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); - return properties; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("JDBC part review"); - post.addComment(comment1); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Hibernate part review"); - post.addComment(comment2); - - entityManager.persist(post); - }); - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - LOGGER.info("Post entity inserted"); - } - - @Test - public void testPostEntityLoad() { - - LOGGER.info("Load Post entity and comments collection"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - printCacheRegionStatistics(post.getClass().getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - }); - } - - @Test - public void testPostEntityEvictModifyLoad() { - - LOGGER.info("Evict, modify, load"); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.detach(post); - - post.setTitle("High-Performance Hibernate"); - entityManager.merge(post); - entityManager.flush(); - - entityManager.detach(post); - post = entityManager.find(Post.class, 1L); - printCacheRegionStatistics(post.getClass().getName()); - }); - } - - @Test - public void testEntityUpdate() { - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - }); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - post.setTitle("High-Performance Hibernate"); - PostComment comment = post.getComments().remove(0); - comment.setPost(null); - - entityManager.flush(); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - LOGGER.debug("Commit after flush"); - }); - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - } - - @Test - public void testNonVersionedEntityUpdate() { - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - }); - printCacheRegionStatistics(PostComment.class.getName()); - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - comment.setReview("JDBC and Database part review"); - }); - printCacheRegionStatistics(PostComment.class.getName()); - } - - @Test - public void testEntityDelete() { - LOGGER.info("Cache entries can be deleted"); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - }); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.remove(post); - }); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertNull(post); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - public static class Post { - - @Id - private Long id; - - private String title; - - @Version - private int version; - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) - public static class PostComment { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/TransactionalCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/TransactionalCacheConcurrencyStrategyTest.java deleted file mode 100644 index 55758760b..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/TransactionalCacheConcurrencyStrategyTest.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.transactional; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.vladmihalcea.book.hpjp.util.transaction.JPATransactionVoidFunction; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.persistence.EntityManager; -import javax.persistence.EntityManagerFactory; -import javax.persistence.PersistenceContext; - -import static com.vladmihalcea.book.hpjp.hibernate.cache.transactional.TransactionalEntities.Post; -import static com.vladmihalcea.book.hpjp.hibernate.cache.transactional.TransactionalEntities.PostComment; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = TransactionalCacheConcurrencyStrategyTestConfiguration.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class TransactionalCacheConcurrencyStrategyTest extends AbstractTest { - - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - @PersistenceContext - private EntityManager entityManager; - - @Autowired - private TransactionTemplate transactionTemplate; - - @Override - protected void doInJPA(JPATransactionVoidFunction function) { - transactionTemplate.execute((TransactionCallback) status -> { - function.accept(entityManager); - return null; - }); - } - - @Before - public void init() { - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("JDBC part review"); - post.addComment(comment1); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Hibernate part review"); - post.addComment(comment2); - - entityManager.persist(post); - }); - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - LOGGER.info("Post entity inserted"); - } - - @Override - public void destroy() { - - } - - @Override - protected Class[] entities() { - return new Class[0]; - } - - @Override - public EntityManagerFactory entityManagerFactory() { - return entityManager.getEntityManagerFactory(); - } - - @Test - public void testPostEntityLoad() { - - LOGGER.info("Load Post entity and comments collection"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - printCacheRegionStatistics(post.getClass().getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - }); - } - - @Test - public void testPostEntityEvictModifyLoad() { - - LOGGER.info("Evict, modify, load"); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.detach(post); - - post.setTitle("High-Performance Hibernate"); - entityManager.merge(post); - entityManager.flush(); - - entityManager.detach(post); - post = entityManager.find(Post.class, 1L); - printCacheRegionStatistics(post.getClass().getName()); - }); - } - - @Test - public void testEntityUpdate() { - LOGGER.debug("testEntityUpdate"); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - }); - - doInJPA(entityManager -> { - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - Post post = entityManager.find(Post.class, 1L); - post.setTitle("High-Performance Hibernate"); - PostComment comment = post.getComments().remove(0); - comment.setPost(null); - - entityManager.flush(); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - LOGGER.debug("Commit after flush"); - }); - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - } - - @Test - public void testEntityUpdateWithRollback() { - LOGGER.debug("testEntityUpdate"); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - }); - - try { - doInJPA(entityManager -> { - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - Post post = entityManager.find(Post.class, 1L); - post.setTitle("High-Performance Hibernate"); - PostComment comment = post.getComments().remove(0); - comment.setPost(null); - - entityManager.flush(); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - if(comment.getId() != null) { - throw new IllegalStateException("Intentional roll back!"); - } - }); - } catch (Exception expected) { - LOGGER.info("Expected", expected); - } - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - } - - @Test - public void testNonVersionedEntityUpdate() { - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - }); - printCacheRegionStatistics(PostComment.class.getName()); - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - comment.setReview("JDBC and Database part review"); - }); - printCacheRegionStatistics(PostComment.class.getName()); - } - - @Test - public void testEntityDelete() { - LOGGER.info("Cache entries can be deleted"); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - }); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.remove(post); - }); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertNull(post); - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/TransactionalCacheConcurrencyStrategyTestConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/TransactionalCacheConcurrencyStrategyTestConfiguration.java deleted file mode 100644 index 8307ab0f9..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/TransactionalCacheConcurrencyStrategyTestConfiguration.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.transactional; - -import com.vladmihalcea.book.hpjp.util.spring.config.jta.HsqldbJtaTransactionManagerConfiguration; -import org.springframework.context.annotation.Configuration; - -import java.util.Properties; - -@Configuration -public class TransactionalCacheConcurrencyStrategyTestConfiguration extends HsqldbJtaTransactionManagerConfiguration { - - @Override - protected Properties additionalProperties() { - Properties properties = super.additionalProperties(); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); - properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); - return properties; - } - - @Override - protected Class configurationClass() { - return TransactionalEntities.class; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalCacheConcurrencyStrategyTest.java deleted file mode 100644 index 0adb94c9a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalCacheConcurrencyStrategyTest.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.transactional.identity; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.vladmihalcea.book.hpjp.util.transaction.JPATransactionVoidFunction; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.persistence.EntityManager; -import javax.persistence.EntityManagerFactory; -import javax.persistence.PersistenceContext; - -import static com.vladmihalcea.book.hpjp.hibernate.cache.transactional.identity.IdentityTransactionalEntities.Post; -import static com.vladmihalcea.book.hpjp.hibernate.cache.transactional.identity.IdentityTransactionalEntities.PostComment; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = IdentityTransactionalCacheConcurrencyStrategyTestConfiguration.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class IdentityTransactionalCacheConcurrencyStrategyTest extends AbstractTest { - - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - @PersistenceContext - private EntityManager entityManager; - - @Autowired - private TransactionTemplate transactionTemplate; - - @Override - protected void doInJPA(JPATransactionVoidFunction function) { - transactionTemplate.execute((TransactionCallback) status -> { - function.accept(entityManager); - return null; - }); - } - - @Before - public void init() { - doInJPA(entityManager -> { - Post post = new Post(); - post.setTitle("High-Performance Java Persistence"); - - PostComment comment1 = new PostComment(); - comment1.setReview("JDBC part review"); - post.addComment(comment1); - - PostComment comment2 = new PostComment(); - comment2.setReview("Hibernate part review"); - post.addComment(comment2); - - entityManager.persist(post); - }); - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - LOGGER.info("Post entity inserted"); - } - - @Override - public void destroy() { - - } - - @Override - protected Class[] entities() { - return new Class[0]; - } - - @Override - public EntityManagerFactory entityManagerFactory() { - return entityManager.getEntityManagerFactory(); - } - - @Test - public void testPostEntityLoad() { - - LOGGER.info("Load Post entity and comments collection"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - printCacheRegionStatistics(post.getClass().getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - }); - } - - @Test - public void testPostEntityEvictModifyLoad() { - - LOGGER.info("Evict, modify, load"); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.detach(post); - - post.setTitle("High-Performance Hibernate"); - entityManager.merge(post); - entityManager.flush(); - - entityManager.detach(post); - post = entityManager.find(Post.class, 1L); - printCacheRegionStatistics(post.getClass().getName()); - }); - } - - @Test - public void testEntityUpdate() { - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - post.setTitle("High-Performance Hibernate"); - PostComment comment = post.getComments().remove(0); - comment.setPost(null); - - entityManager.flush(); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - LOGGER.debug("Commit after flush"); - }); - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - } - - @Test - public void testNonVersionedEntityUpdate() { - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - }); - printCacheRegionStatistics(PostComment.class.getName()); - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - comment.setReview("JDBC and Database part review"); - }); - printCacheRegionStatistics(PostComment.class.getName()); - } - - @Test - public void testEntityDelete() { - LOGGER.info("Cache entries can be deleted"); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - }); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.remove(post); - }); - - printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); - printCacheRegionStatistics(PostComment.class.getName()); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertNull(post); - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalCacheConcurrencyStrategyTestConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalCacheConcurrencyStrategyTestConfiguration.java deleted file mode 100644 index 04afcd74d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalCacheConcurrencyStrategyTestConfiguration.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.transactional.identity; - -import com.vladmihalcea.book.hpjp.util.spring.config.jta.HsqldbJtaTransactionManagerConfiguration; -import org.springframework.context.annotation.Configuration; - -import java.util.Properties; - -@Configuration -public class IdentityTransactionalCacheConcurrencyStrategyTestConfiguration extends HsqldbJtaTransactionManagerConfiguration { - - @Override - protected Properties additionalProperties() { - Properties properties = super.additionalProperties(); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); - properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); - return properties; - } - - @Override - protected Class configurationClass() { - return IdentityTransactionalEntities.class; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/CascadeLockTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/CascadeLockTest.java deleted file mode 100644 index a2666daa7..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/CascadeLockTest.java +++ /dev/null @@ -1,451 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.LockMode; -import org.hibernate.LockOptions; -import org.hibernate.Session; -import org.hibernate.jpa.AvailableSettings; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; - - -/** - * CascadeLockTest - Test to check CascadeType.LOCK - * - * @author Vlad Mihalcea - */ -public class CascadeLockTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostDetails.class, - PostComment.class - }; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setTitle("Hibernate Master Class"); - entityManager.persist(post); - - post.addDetails(new PostDetails()); - post.addComment(new PostComment("Good post!")); - post.addComment(new PostComment("Nice post!")); - }); - } - - @Test - public void testCascadeLockOnManagedEntityWithScope() throws InterruptedException { - LOGGER.info("Test lock cascade for managed entity"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.unwrap(Session.class) - .buildLockRequest( - new LockOptions(LockMode.PESSIMISTIC_WRITE)) - .setScope(true) - .lock(post); - }); - } - - @Test - public void testCascadeLockOnManagedEntityWithJPA() throws InterruptedException { - LOGGER.info("Test lock cascade for managed entity"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, Collections.singletonMap( - AvailableSettings.LOCK_SCOPE, PessimisticLockScope.EXTENDED - )); - }); - } - - @Test - public void testCascadeLockOnManagedEntityWithQuery() throws InterruptedException { - LOGGER.info("Test lock cascade for managed entity"); - doInJPA(entityManager -> { - Post post = entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.details " + - "join fetch p.comments " + - "where p.id = :id", Post.class) - .setParameter("id", 1L) - .setLockMode(LockModeType.PESSIMISTIC_WRITE) - .getSingleResult(); - }); - } - - @Test - public void testCascadeLockOnManagedEntityWithAssociationsInitialzied() throws InterruptedException { - LOGGER.info("Test lock cascade for managed entity"); - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - Post post = (Post) entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.details " + - "join fetch p.comments " + - "where " + - " p.id = :id" - ).setParameter("id", 1L) - .getSingleResult(); - session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).setScope(true).lock(post); - }); - } - - @Test - public void testCascadeLockOnManagedEntityWithAssociationsInitializedAndJpa() throws InterruptedException { - LOGGER.info("Test lock cascade for managed entity"); - doInJPA(entityManager -> { - Post post = entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.details " + - "where p.id = :id", Post.class) - .setParameter("id", 1L) - .getSingleResult(); - entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, Collections.singletonMap( - AvailableSettings.LOCK_SCOPE, PessimisticLockScope.EXTENDED - )); - }); - } - - private void containsPost(EntityManager entityManager, Post post, boolean expected) { - assertEquals(expected, entityManager.contains(post)); - assertEquals(expected, (entityManager.contains(post.getDetails()))); - for(PostComment comment : post.getComments()) { - assertEquals(expected, (entityManager.contains(comment))); - } - } - - @Test - public void testCascadeLockOnDetachedEntityWithoutScope() { - LOGGER.info("Test lock cascade for detached entity without scope"); - - //Load the Post entity, which will become detached - Post post = doInJPA(entityManager -> - (Post) entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.details " + - "join fetch p.comments " + - "where p.id = :id" - ).setParameter("id", 1L) - .getSingleResult()); - - //Change the detached entity state - post.setTitle("Hibernate Training"); - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - //The Post entity graph is detached - containsPost(entityManager, post, false); - - //The Lock request associates the entity graph and locks the requested entity - session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(post); - - //Hibernate doesn't know if the entity is dirty - assertEquals("Hibernate Training", post.getTitle()); - - //The Post entity graph is attached - containsPost(entityManager, post, true); - }); - doInJPA(entityManager -> { - //The detached Post entity changes have been lost - Post _post = (Post) entityManager.find(Post.class, 1L); - assertEquals("Hibernate Master Class", _post.getTitle()); - }); - } - - @Test - public void testCascadeLockOnDetachedEntityWithScope() { - LOGGER.info("Test lock cascade for detached entity with scope"); - - //Load the Post entity, which will become detached - Post post = doInJPA(entityManager -> { - return entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.details " + - "join fetch p.comments " + - "where p.id = :id", Post.class) - .setParameter("id", 1L) - .getSingleResult(); - }); - - doInJPA(entityManager -> { - LOGGER.info("Reattach and lock"); - entityManager.unwrap(Session.class) - .buildLockRequest( - new LockOptions(LockMode.PESSIMISTIC_WRITE)) - .setScope(true) - .lock(post); - - //The Post entity graph is attached - containsPost(entityManager, post, true); - }); - doInJPA(entityManager -> { - //The detached Post entity changes have been lost - Post _post = (Post) entityManager.find(Post.class, 1L); - assertEquals("Hibernate Master Class", _post.getTitle()); - }); - } - - @Test - public void testCascadeLockOnDetachedEntityUninitializedWithScope() { - LOGGER.info("Test lock cascade for detached entity with scope"); - - //Load the Post entity, which will become detached - Post post = doInJPA(entityManager -> (Post) entityManager.find(Post.class, 1L)); - - doInJPA(entityManager -> { - LOGGER.info("Reattach and lock entity with associations not initialized"); - entityManager.unwrap(Session.class) - .buildLockRequest( - new LockOptions(LockMode.PESSIMISTIC_WRITE)) - .setScope(true) - .lock(post); - - LOGGER.info("Check entities are reattached"); - //The Post entity graph is attached - containsPost(entityManager, post, true); - }); - } - - @Test - public void testCascadeLockOnDetachedChildEntityUninitializedWithScope() { - LOGGER.info("Test lock cascade for detached entity with scope"); - - //Load the Post entity, which will become detached - PostComment postComment = doInJPA(entityManager -> (PostComment) entityManager.find(PostComment.class, 3L)); - - doInJPA(entityManager -> { - LOGGER.info("Reattach and lock entity with associations not initialized"); - entityManager.unwrap(Session.class) - .buildLockRequest( - new LockOptions(LockMode.PESSIMISTIC_WRITE)) - .lock(postComment); - }); - } - - @Test - public void testUpdateOnDetachedEntity() { - LOGGER.info("Test update for detached entity"); - //Load the Post entity, which will become detached - Post post = doInJPA(entityManager -> (Post) entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.details " + - "join fetch p.comments " + - "where p.id = :id", Post.class) - .setParameter("id", 1L) - .getSingleResult()); - - //Change the detached entity state - post.setTitle("Hibernate Training"); - - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - //The Post entity graph is detached - containsPost(entityManager, post, false); - - //The update will trigger an entity state flush and attach the entity graph - session.update(post); - - //The Post entity graph is attached - containsPost(entityManager, post, true); - }); - doInJPA(entityManager -> { - Post _post = (Post) entityManager.find(Post.class, 1L); - assertEquals("Hibernate Training", _post.getTitle()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - private String body; - - @Version - private int version; - - public Post() {} - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) - private List comments = new ArrayList<>(); - - @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true, fetch = FetchType.LAZY, optional = false) - private PostDetails details; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public PostDetails getDetails() { - return details; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - - public void addDetails(PostDetails details) { - this.details = details; - details.setPost(this); - } - - public void removeDetails() { - this.details.setPost(null); - this.details = null; - } - } - - @Entity(name = "PostDetails") - @Table(name = "post_details") - public static class PostDetails { - - @Id - private Long id; - - @Column(name = "created_on") - private Date createdOn; - - @Column(name = "created_by") - private String createdBy; - - @Version - private int version; - - public PostDetails() { - createdOn = new Date(); - } - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") - @MapsId - private Post post; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - @GeneratedValue - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - @Version - private int version; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/CascadeLockUnidirectionalOneToManyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/CascadeLockUnidirectionalOneToManyTest.java deleted file mode 100644 index ac1080d07..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/CascadeLockUnidirectionalOneToManyTest.java +++ /dev/null @@ -1,437 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.LockMode; -import org.hibernate.LockOptions; -import org.hibernate.Session; -import org.hibernate.jpa.AvailableSettings; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; - - -/** - * CascadeLockTest - Test to check CascadeType.LOCK - * - * @author Vlad Mihalcea - */ -public class CascadeLockUnidirectionalOneToManyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostDetails.class, - PostComment.class - }; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setTitle("Hibernate Master Class"); - entityManager.persist(post); - - post.addDetails(new PostDetails()); - post.addComment(new PostComment("Good post!")); - post.addComment(new PostComment("Nice post!")); - }); - } - - @Test - public void testCascadeLockOnManagedEntityWithScope() throws InterruptedException { - LOGGER.info("Test lock cascade for managed entity"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.unwrap(Session.class) - .buildLockRequest( - new LockOptions(LockMode.PESSIMISTIC_WRITE)) - .setScope(true) - .lock(post); - }); - } - - @Test - public void testCascadeLockOnManagedEntityWithJPA() throws InterruptedException { - LOGGER.info("Test lock cascade for managed entity"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, Collections.singletonMap( - AvailableSettings.LOCK_SCOPE, PessimisticLockScope.EXTENDED - )); - }); - } - - @Test - public void testCascadeLockOnManagedEntityWithQuery() throws InterruptedException { - LOGGER.info("Test lock cascade for managed entity"); - doInJPA(entityManager -> { - Post post = entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.details " + - "join fetch p.comments " + - "where p.id = :id", Post.class) - .setParameter("id", 1L) - .setLockMode(LockModeType.PESSIMISTIC_WRITE) - .getSingleResult(); - }); - } - - @Test - public void testCascadeLockOnManagedEntityWithAssociationsInitialzied() throws InterruptedException { - LOGGER.info("Test lock cascade for managed entity"); - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - Post post = (Post) entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.details " + - "join fetch p.comments " + - "where " + - " p.id = :id" - ).setParameter("id", 1L) - .getSingleResult(); - session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).setScope(true).lock(post); - }); - } - - @Test - public void testCascadeLockOnManagedEntityWithAssociationsInitializedAndJpa() throws InterruptedException { - LOGGER.info("Test lock cascade for managed entity"); - doInJPA(entityManager -> { - Post post = entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.details " + - "where p.id = :id", Post.class) - .setParameter("id", 1L) - .getSingleResult(); - entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, Collections.singletonMap( - AvailableSettings.LOCK_SCOPE, PessimisticLockScope.EXTENDED - )); - }); - } - - private void containsPost(EntityManager entityManager, Post post, boolean expected) { - assertEquals(expected, entityManager.contains(post)); - assertEquals(expected, (entityManager.contains(post.getDetails()))); - for(PostComment comment : post.getComments()) { - assertEquals(expected, (entityManager.contains(comment))); - } - } - - @Test - public void testCascadeLockOnDetachedEntityWithoutScope() { - LOGGER.info("Test lock cascade for detached entity without scope"); - - //Load the Post entity, which will become detached - Post post = doInJPA(entityManager -> - (Post) entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.details " + - "join fetch p.comments " + - "where p.id = :id" - ).setParameter("id", 1L) - .getSingleResult()); - - //Change the detached entity state - post.setTitle("Hibernate Training"); - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - //The Post entity graph is detached - containsPost(entityManager, post, false); - - //The Lock request associates the entity graph and locks the requested entity - session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(post); - - //Hibernate doesn't know if the entity is dirty - assertEquals("Hibernate Training", post.getTitle()); - - //The Post entity graph is attached - containsPost(entityManager, post, true); - }); - doInJPA(entityManager -> { - //The detached Post entity changes have been lost - Post _post = (Post) entityManager.find(Post.class, 1L); - assertEquals("Hibernate Master Class", _post.getTitle()); - }); - } - - @Test - public void testCascadeLockOnDetachedEntityWithScope() { - LOGGER.info("Test lock cascade for detached entity with scope"); - - //Load the Post entity, which will become detached - Post post = doInJPA(entityManager -> (Post) entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.details " + - "join fetch p.comments " + - "where p.id = :id", Post.class) - .setParameter("id", 1L) - .getSingleResult()); - - doInJPA(entityManager -> { - LOGGER.info("Reattach and lock"); - entityManager.unwrap(Session.class) - .buildLockRequest( - new LockOptions(LockMode.PESSIMISTIC_WRITE)) - .setScope(true) - .lock(post); - - //The Post entity graph is attached - containsPost(entityManager, post, true); - }); - doInJPA(entityManager -> { - //The detached Post entity changes have been lost - Post _post = (Post) entityManager.find(Post.class, 1L); - assertEquals("Hibernate Master Class", _post.getTitle()); - }); - } - - @Test - public void testCascadeLockOnDetachedEntityUninitializedWithScope() { - LOGGER.info("Test lock cascade for detached entity with scope"); - - //Load the Post entity, which will become detached - Post post = doInJPA(entityManager -> (Post) entityManager.find(Post.class, 1L)); - - doInJPA(entityManager -> { - LOGGER.info("Reattach and lock entity with associations not initialized"); - entityManager.unwrap(Session.class) - .buildLockRequest( - new LockOptions(LockMode.PESSIMISTIC_WRITE)) - .setScope(true) - .lock(post); - - LOGGER.info("Check entities are reattached"); - //The Post entity graph is attached - containsPost(entityManager, post, true); - }); - } - - @Test - public void testCascadeLockOnDetachedChildEntityUninitializedWithScope() { - LOGGER.info("Test lock cascade for detached entity with scope"); - - //Load the Post entity, which will become detached - PostComment postComment = doInJPA(entityManager -> (PostComment) entityManager.find(PostComment.class, 3L)); - - doInJPA(entityManager -> { - LOGGER.info("Reattach and lock entity with associations not initialized"); - entityManager.unwrap(Session.class) - .buildLockRequest( - new LockOptions(LockMode.PESSIMISTIC_WRITE)) - .lock(postComment); - }); - } - - @Test - public void testUpdateOnDetachedEntity() { - LOGGER.info("Test update for detached entity"); - //Load the Post entity, which will become detached - Post post = doInJPA(entityManager -> (Post) entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.details " + - "join fetch p.comments " + - "where p.id = :id", Post.class) - .setParameter("id", 1L) - .getSingleResult()); - - //Change the detached entity state - post.setTitle("Hibernate Training"); - - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - //The Post entity graph is detached - containsPost(entityManager, post, false); - - //The update will trigger an entity state flush and attach the entity graph - session.update(post); - - //The Post entity graph is attached - containsPost(entityManager, post, true); - }); - doInJPA(entityManager -> { - Post _post = (Post) entityManager.find(Post.class, 1L); - assertEquals("Hibernate Training", _post.getTitle()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - private String body; - - @Version - private int version; - - public Post() {} - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - private List comments = new ArrayList<>(); - - @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true, fetch = FetchType.LAZY, optional = false) - private PostDetails details; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public PostDetails getDetails() { - return details; - } - - public void addComment(PostComment comment) { - comments.add(comment); - } - - public void addDetails(PostDetails details) { - this.details = details; - details.setPost(this); - } - - public void removeDetails() { - this.details.setPost(null); - this.details = null; - } - } - - @Entity(name = "PostDetails") - @Table(name = "post_details") - public static class PostDetails { - - @Id - private Long id; - - @Column(name = "created_on") - private Date createdOn; - - @Column(name = "created_by") - private String createdBy; - - @Version - private int version; - - public PostDetails() { - createdOn = new Date(); - } - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") - @MapsId - private Post post; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - @GeneratedValue - private Long id; - - private String review; - - @Version - private int version; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/DefaultOptimisticLockingTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/DefaultOptimisticLockingTest.java deleted file mode 100644 index 460493366..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/DefaultOptimisticLockingTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.StaleStateException; -import org.junit.Test; - -import javax.persistence.*; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class DefaultOptimisticLockingTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class - }; - } - - @Test - public void testOptimisticLocking() { - - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - - entityManager.flush(); - post.setTitle("High-Performance Hibernate"); - }); - - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(1, post.getVersion()); - }); - } - - @Test - public void testStaleStateException() { - - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - }); - - try { - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - - executeSync(() -> { - doInJPA(_entityManager -> { - Post _post = _entityManager.find(Post.class, 1L); - _post.setTitle("High-Performance JDBC"); - }); - }); - - post.setTitle("High-Performance Hibernate"); - }); - } catch (Exception expected) { - LOGGER.error("Throws", expected); - assertEquals(OptimisticLockException.class, expected.getCause().getClass()); - assertEquals(StaleStateException.class, expected.getCause().getCause().getClass()); - } - } - - @Entity(name = "Post") @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - @Version - private int version; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public int getVersion() { - return version; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModePessimisticReadWriteIntegrationTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModePessimisticReadWriteIntegrationTest.java deleted file mode 100644 index aa3cda701..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModePessimisticReadWriteIntegrationTest.java +++ /dev/null @@ -1,312 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; - -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.LockMode; -import org.hibernate.LockOptions; -import org.hibernate.Session; -import org.hibernate.StaleObjectStateException; -import org.hibernate.dialect.lock.PessimisticEntityLockException; - -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.*; -import java.util.Collections; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - - -/** - * LockModePessimisticReadWriteIntegrationTest - Test to check LockMode.PESSIMISTIC_READ and LockMode.PESSIMISTIC_WRITE - * - * @author Vlad Mihalcea - */ -public class LockModePessimisticReadWriteIntegrationTest extends AbstractPostgreSQLIntegrationTest { - - public static final int WAIT_MILLIS = 500; - - private interface LockRequestCallable { - void lock(Session session, Post post); - } - - private final CountDownLatch endLatch = new CountDownLatch(1); - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class - }; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - post.setBody("Chapter 17 summary"); - entityManager.persist(post); - }); - } - - private void testPessimisticLocking(LockRequestCallable primaryLockRequestCallable, LockRequestCallable secondaryLockRequestCallable) { - doInJPA(entityManager -> { - try { - Session session = entityManager.unwrap(Session.class); - Post post = entityManager.find(Post.class, 1L); - primaryLockRequestCallable.lock(session, post); - executeAsync( - () -> { - doInJPA(_entityManager -> { - Session _session = _entityManager.unwrap(Session.class); - Post _post = _entityManager.find(Post.class, 1L); - secondaryLockRequestCallable.lock(_session, _post); - }); - }, - endLatch::countDown - ); - sleep(WAIT_MILLIS); - } catch (StaleObjectStateException e) { - LOGGER.info("Optimistic locking failure: ", e); - } - }); - awaitOnLatch(endLatch); - } - - @Test - public void testPessimisticRead() { - LOGGER.info("Test PESSIMISTIC_READ"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L, LockModeType.PESSIMISTIC_READ); - }); - } - - @Test - public void testPessimisticWrite() { - LOGGER.info("Test PESSIMISTIC_WRITE"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L, LockModeType.PESSIMISTIC_WRITE); - }); - } - - @Test - public void testPessimisticWriteAfterFetch() { - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE); - }); - } - - @Test - public void testPessimisticWriteAfterFetchWithDetachedForJPA() { - Post post = doInJPA(entityManager -> { - return entityManager.find(Post.class, 1L); - }); - try { - doInJPA(entityManager -> { - entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE); - }); - } catch (IllegalArgumentException e) { - assertEquals("entity not in the persistence context", e.getMessage()); - } - } - - @Test - public void testPessimisticWriteAfterFetchWithDetachedForHibernate() { - Post post = doInJPA(entityManager -> { - return entityManager.find(Post.class, 1L); - }); - doInJPA(entityManager -> { - LOGGER.info("Lock and reattach"); - entityManager.unwrap(Session.class) - .buildLockRequest( - new LockOptions(LockMode.PESSIMISTIC_WRITE)) - .lock(post); - post.setTitle("High-Performance Hibernate"); - }); - } - - @Test - public void testPessimisticReadDoesNotBlockPessimisticRead() throws InterruptedException { - LOGGER.info("Test PESSIMISTIC_READ doesn't block PESSIMISTIC_READ"); - testPessimisticLocking( - (session, post) -> { - session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(post); - LOGGER.info("PESSIMISTIC_READ acquired"); - }, - (session, post) -> { - session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(post); - LOGGER.info("PESSIMISTIC_READ acquired"); - } - ); - } - - @Test - public void testPessimisticReadBlocksUpdate() throws InterruptedException { - LOGGER.info("Test PESSIMISTIC_READ blocks UPDATE"); - testPessimisticLocking( - (session, post) -> { - session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(post); - LOGGER.info("PESSIMISTIC_READ acquired"); - }, - (session, post) -> { - post.setBody("Chapter 16 summary"); - session.flush(); - LOGGER.info("Implicit lock acquired"); - } - ); - } - - @Test - public void testPessimisticReadWithPessimisticWriteNoWait() throws InterruptedException { - LOGGER.info("Test PESSIMISTIC_READ blocks PESSIMISTIC_WRITE, NO WAIT fails fast"); - testPessimisticLocking( - (session, post) -> { - session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(post); - LOGGER.info("PESSIMISTIC_READ acquired"); - }, - (session, post) -> { - session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).setTimeOut(Session.LockRequest.PESSIMISTIC_NO_WAIT).lock(post); - LOGGER.info("PESSIMISTIC_WRITE acquired"); - } - ); - } - - @Test - public void testPessimisticWriteBlocksPessimisticRead() throws InterruptedException { - LOGGER.info("Test PESSIMISTIC_WRITE blocks PESSIMISTIC_READ"); - testPessimisticLocking( - (session, post) -> { - session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(post); - LOGGER.info("PESSIMISTIC_WRITE acquired"); - }, - (session, post) -> { - session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(post); - LOGGER.info("PESSIMISTIC_READ acquired"); - } - ); - } - - @Test - public void testPessimisticWriteBlocksPessimisticWrite() throws InterruptedException { - LOGGER.info("Test PESSIMISTIC_WRITE blocks PESSIMISTIC_WRITE"); - testPessimisticLocking( - (session, post) -> { - session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(post); - LOGGER.info("PESSIMISTIC_WRITE acquired"); - }, - (session, post) -> { - session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(post); - LOGGER.info("PESSIMISTIC_WRITE acquired"); - } - ); - } - - @Test - public void testPessimisticNoWait() { - LOGGER.info("Test PESSIMISTIC_READ blocks PESSIMISTIC_WRITE, NO WAIT fails fast"); - Post post = doInJPA(entityManager -> { - return entityManager.find(Post.class, 1L); - }); - - doInJPA(entityManager -> { - entityManager.unwrap( Session.class ).lock(post, LockMode.PESSIMISTIC_WRITE); - - executeSync( () -> { - doInJPA(_entityManager -> { - try { - _entityManager - .unwrap(Session.class) - .buildLockRequest( - new LockOptions(LockMode.PESSIMISTIC_WRITE) - .setTimeOut(LockOptions.NO_WAIT)) - .lock(post); - fail("Should throw PessimisticEntityLockException"); - } - catch (PessimisticEntityLockException expected) { - //This is expected since the first transaction already acquired this lock - } - }); - } ); - }); - } - - @Test - public void testPessimisticNoWaitJPA() throws InterruptedException { - LOGGER.info("Test PESSIMISTIC_READ blocks PESSIMISTIC_WRITE, NO WAIT fails fast"); - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, - Collections.singletonMap("javax.persistence.lock.timeout", 0) - ); - }); - } - - @Test - public void testPessimisticTimeout() throws InterruptedException { - doInJPA(entityManager -> { - Post post = entityManager.getReference(Post.class, 1L); - - entityManager.unwrap(Session.class) - .buildLockRequest( - new LockOptions(LockMode.PESSIMISTIC_WRITE) - .setTimeOut((int) TimeUnit.SECONDS.toMillis(3))) - .lock(post); - }); - } - - @Test - public void testPessimisticTimeoutJPA() throws InterruptedException { - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, - Collections.singletonMap("javax.persistence.lock.timeout", - TimeUnit.SECONDS.toMillis(3)) - ); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - private String body; - - @Version - private int version; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getBody() { - return body; - } - - public void setBody(String body) { - this.body = body; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingChildUpdatesRootVersionTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingChildUpdatesRootVersionTest.java deleted file mode 100644 index e327af2dc..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingChildUpdatesRootVersionTest.java +++ /dev/null @@ -1,336 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.HibernateException; -import org.hibernate.LockMode; -import org.hibernate.boot.Metadata; -import org.hibernate.engine.spi.EntityEntry; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.engine.spi.SessionImplementor; -import org.hibernate.engine.spi.Status; -import org.hibernate.event.service.spi.EventListenerRegistry; -import org.hibernate.event.spi.*; -import org.hibernate.integrator.spi.Integrator; -import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.service.spi.SessionFactoryServiceRegistry; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.persistence.*; -import java.util.Map; - -/** - * @author Vlad Mihalcea - */ -public class OptimisticLockingChildUpdatesRootVersionTest extends AbstractTest { - - public static class RootAwareEventListenerIntegrator implements org.hibernate.integrator.spi.Integrator { - - public static final RootAwareEventListenerIntegrator INSTANCE = new RootAwareEventListenerIntegrator(); - - @Override - public void integrate( - Metadata metadata, - SessionFactoryImplementor sessionFactory, - SessionFactoryServiceRegistry serviceRegistry) { - - final EventListenerRegistry eventListenerRegistry = - serviceRegistry.getService( EventListenerRegistry.class ); - - eventListenerRegistry.appendListeners(EventType.PERSIST, RootAwareInsertEventListener.INSTANCE); - eventListenerRegistry.appendListeners(EventType.FLUSH_ENTITY, RootAwareUpdateAndDeleteEventListener.INSTANCE); - } - - @Override - public void disintegrate( - SessionFactoryImplementor sessionFactory, - SessionFactoryServiceRegistry serviceRegistry) { - - } - } - - public static class RootAwareInsertEventListener implements PersistEventListener { - - private static final Logger LOGGER = LoggerFactory.getLogger(RootAwareInsertEventListener.class); - - public static final RootAwareInsertEventListener INSTANCE = new RootAwareInsertEventListener(); - - @Override - public void onPersist(PersistEvent event) throws HibernateException { - final Object entity = event.getObject(); - - if(entity instanceof RootAware) { - RootAware rootAware = (RootAware) entity; - Object root = rootAware.root(); - event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT); - - LOGGER.info("Incrementing {} entity version because a {} child entity has been inserted", root, entity); - } - } - - @Override - public void onPersist(PersistEvent event, Map createdAlready) throws HibernateException { - onPersist(event); - } - } - - public static class RootAwareUpdateAndDeleteEventListener implements FlushEntityEventListener { - - private static final Logger LOGGER = LoggerFactory.getLogger(RootAwareUpdateAndDeleteEventListener.class); - - public static final RootAwareUpdateAndDeleteEventListener INSTANCE = new RootAwareUpdateAndDeleteEventListener(); - - @Override - public void onFlushEntity(FlushEntityEvent event) throws HibernateException { - final EntityEntry entry = event.getEntityEntry(); - final Object entity = event.getEntity(); - final boolean mightBeDirty = entry.requiresDirtyCheck( entity ); - - if(mightBeDirty && entity instanceof RootAware) { - RootAware rootAware = (RootAware) entity; - if(updated(event)) { - Object root = rootAware.root(); - LOGGER.info("Incrementing {} entity version because a {} child entity has been updated", root, entity); - incrementRootVersion(event, root); - } - else if (deleted(event)) { - Object root = rootAware.root(); - LOGGER.info("Incrementing {} entity version because a {} child entity has been deleted", root, entity); - incrementRootVersion(event, root); - } - } - } - - private void incrementRootVersion(FlushEntityEvent event, Object root) { - event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT); - } - - private boolean deleted(FlushEntityEvent event) { - return event.getEntityEntry().getStatus() == Status.DELETED; - } - - private boolean updated(FlushEntityEvent event) { - final EntityEntry entry = event.getEntityEntry(); - final Object entity = event.getEntity(); - - int[] dirtyProperties; - EntityPersister persister = entry.getPersister(); - final Object[] values = event.getPropertyValues(); - SessionImplementor session = event.getSession(); - - if ( event.hasDatabaseSnapshot() ) { - dirtyProperties = persister.findModified( event.getDatabaseSnapshot(), values, entity, session ); - } - else { - dirtyProperties = persister.findDirty( values, entry.getLoadedState(), entity, session ); - } - - return dirtyProperties != null; - } - } - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class, - PostCommentDetails.class, - }; - } - - @Override - protected Integrator integrator() { - return RootAwareEventListenerIntegrator.INSTANCE; - } - - @Test - public void test() { - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("Good"); - comment1.setPost(post); - - PostCommentDetails details1 = new PostCommentDetails(); - details1.setComment(comment1); - details1.setVotes(10); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Excellent"); - comment2.setPost(post); - - PostCommentDetails details2 = new PostCommentDetails(); - details2.setComment(comment2); - details2.setVotes(10); - - entityManager.persist(post); - entityManager.persist(comment1); - entityManager.persist(comment2); - entityManager.persist(details1); - entityManager.persist(details2); - }); - - doInJPA(entityManager -> { - PostCommentDetails postCommentDetails = entityManager.createQuery( - "select pcd " + - "from PostCommentDetails pcd " + - "join fetch pcd.comment pc " + - "join fetch pc.post p " + - "where pcd.id = :id", PostCommentDetails.class) - .setParameter("id", 2L) - .getSingleResult(); - - postCommentDetails.setVotes(15); - }); - - doInJPA(entityManager -> { - PostComment postComment = entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "join fetch pc.post p " + - "where pc.id = :id", PostComment.class) - .setParameter("id", 2L) - .getSingleResult(); - - postComment.setReview("Brilliant!"); - }); - - doInJPA(entityManager -> { - Post post = entityManager.getReference(Post.class, 1L); - - PostComment postComment = new PostComment(); - postComment.setId(3L); - postComment.setReview("Worth it!"); - postComment.setPost(post); - entityManager.persist(postComment); - }); - - doInJPA(entityManager -> { - PostComment postComment = entityManager.getReference(PostComment.class, 3l); - entityManager.remove(postComment); - }); - } - - public interface RootAware { - T root(); - } - - @Entity(name = "Post") @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - @Version - private Integer version; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment implements RootAware { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - - @Override - public Post root() { - return post; - } - } - - @Entity(name = "PostCommentDetails") - @Table(name = "post_comment_details") - public static class PostCommentDetails implements RootAware { - - @Id - private Long id; - - @OneToOne(fetch = FetchType.LAZY) - @MapsId - private PostComment comment; - - private int votes; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public PostComment getComment() { - return comment; - } - - public void setComment(PostComment comment) { - this.comment = comment; - } - - public int getVotes() { - return votes; - } - - public void setVotes(int votes) { - this.votes = votes; - } - - @Override - public Post root() { - return comment.root(); - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/SkipLockJobQueueTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/SkipLockJobQueueTest.java deleted file mode 100644 index 936639eba..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/SkipLockJobQueueTest.java +++ /dev/null @@ -1,236 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; - -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.apache.commons.lang.exception.ExceptionUtils; -import org.hibernate.LockMode; -import org.hibernate.LockOptions; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.*; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static java.util.stream.Collectors.toList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - - -/** - * @author Vlad Mihalcea - */ -public class SkipLockJobQueueTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class - }; - } - - @Before - public void init() { - super.init(); - doInJPA(entityManager -> { - for (long i = 0; i < 10; i++) { - Post post = new Post(); - post.setId(i); - post.setTitle("High-Performance Java Persistence"); - post.setBody(String.format("Chapter %d summary", i)); - post.setStatus(PostStatus.PENDING); - entityManager.persist(post); - } - }); - } - - @Test - public void testLockContention() { - LOGGER.info("Test lock contention"); - doInJPA(entityManager -> { - List pendingPosts = entityManager.createQuery( - "select p " + - "from Post p " + - "where p.status = :status", - Post.class) - .setParameter("status", PostStatus.PENDING) - .setFirstResult(0) - .setMaxResults(2) - .setLockMode(LockModeType.PESSIMISTIC_WRITE) - .setHint("javax.persistence.lock.timeout", 0) - .getResultList(); - - assertEquals(2, pendingPosts.size()); - - try { - executeSync(() -> { - doInJPA(_entityManager -> { - List _pendingPosts = _entityManager.createQuery( - "select p " + - "from Post p " + - "where p.status = :status", Post.class) - .setParameter("status", PostStatus.PENDING) - .setFirstResult(0) - .setMaxResults(2) - .setLockMode(LockModeType.PESSIMISTIC_WRITE) - .setHint("javax.persistence.lock.timeout", 0) - .getResultList(); - }); - }); - } catch (Exception e) { - assertEquals(1, Arrays.asList(ExceptionUtils.getThrowables(e)) - .stream() - .map(Throwable::getClass) - .filter(clazz -> clazz.equals(PessimisticLockException.class)) - .count()); - } - }); - } - - @Test - public void testSkipLocked() { - LOGGER.info("Test lock contention"); - doInJPA(entityManager -> { - final int lockCount = 2; - LOGGER.debug("Alice wants to moderate {} Post(s)", lockCount); - List pendingPosts = pendingPosts(entityManager, lockCount); - List ids = pendingPosts.stream().map(Post::getId).collect(toList()); - assertTrue(ids.size() == 2 && ids.contains(0L) && ids.contains(1L)); - - executeSync(() -> { - doInJPA(_entityManager -> { - LOGGER.debug("Bob wants to moderate {} Post(s)", lockCount); - List _pendingPosts = pendingPosts(_entityManager, lockCount); - List _ids = _pendingPosts.stream().map(Post::getId).collect(toList()); - assertTrue(_ids.size() == 2 && _ids.contains(2L) && _ids.contains(3L)); - }); - }); - }); - } - - @Test - public void testAliceLocksAll() { - LOGGER.info("Test lock contention"); - doInJPA(entityManager -> { - List pendingPosts = pendingPosts(entityManager, 10); - assertTrue(pendingPosts.size() == 10); - - executeSync(() -> { - doInJPA(_entityManager -> { - List _pendingPosts = pendingPosts(_entityManager, 2); - assertTrue(_pendingPosts.size() == 0); - }); - }); - }); - } - - @Test - public void testSkipLockedMaxCountLessThanLockCount() { - LOGGER.info("Test lock contention"); - doInJPA(entityManager -> { - List pendingPosts = pendingPosts(entityManager, 11); - assertEquals(10, pendingPosts.size()); - }); - } - - public List pendingPosts(EntityManager entityManager, int lockCount) { - return pendingPosts(entityManager, lockCount, lockCount, null); - } - - private List pendingPosts(EntityManager entityManager, int lockCount, - int maxResults, Integer maxCount) { - LOGGER.debug("Attempting to lock {} Post(s) entities", maxResults); - List posts= entityManager.createQuery( - "select p from Post p where p.status = :status", Post.class) - .setParameter("status", PostStatus.PENDING) - .setMaxResults(maxResults) - .unwrap(org.hibernate.query.Query.class) - //Legacy hack - UPGRADE_SKIPLOCKED bypasses follow-on-locking - //.setLockOptions(new LockOptions(LockMode.UPGRADE_SKIPLOCKED)) - .setLockOptions(new LockOptions(LockMode.PESSIMISTIC_WRITE) - .setTimeOut(LockOptions.SKIP_LOCKED) - //This is not really needed for this query but shows that you can control the follow-on locking mechanism - .setFollowOnLocking(false) - ) - .list(); - - if(posts.isEmpty()) { - if(maxCount == null) { - maxCount = pendingPostCount(entityManager); - } - if(maxResults < maxCount || maxResults == lockCount) { - maxResults += lockCount; - return pendingPosts(entityManager, lockCount, maxResults, maxCount); - } - } - LOGGER.debug("{} Post(s) entities have been locked", posts.size()); - return posts; - } - - private int pendingPostCount(EntityManager entityManager) { - int postCount = ((Number) entityManager.createQuery( - "select count(*) from Post where status = :status") - .setParameter("status", PostStatus.PENDING) - .getSingleResult()).intValue(); - - LOGGER.debug("There are {} PENDING Post(s)", postCount); - return postCount; - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - private String body; - - @Enumerated - private PostStatus status; - - @Version - private int version; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getBody() { - return body; - } - - public void setBody(String body) { - this.body = body; - } - - public PostStatus getStatus() { - return status; - } - - public void setStatus(PostStatus status) { - this.status = status; - } - } - - public enum PostStatus { - PENDING, - APPROVED, - SPAM - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/C3P0CockroachDBConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/C3P0CockroachDBConnectionProviderTest.java deleted file mode 100644 index da8869ccd..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/C3P0CockroachDBConnectionProviderTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; - -import java.util.Properties; - -import com.vladmihalcea.book.hpjp.util.providers.CockroachDBDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; - -public class C3P0CockroachDBConnectionProviderTest extends JPADriverConnectionProviderTest { - - protected DataSourceProvider dataSourceProvider() { - return new CockroachDBDataSourceProvider(); - } - - @Override - protected void appendDriverProperties(Properties properties) { - super.appendDriverProperties(properties); - properties.put("hibernate.c3p0.min_size", 1); - properties.put("hibernate.c3p0.max_size", 5); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/C3P0ConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/C3P0ConnectionProviderTest.java deleted file mode 100644 index 707edee34..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/C3P0ConnectionProviderTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; - -import java.util.Properties; - -public class C3P0ConnectionProviderTest extends JPADriverConnectionProviderTest { - - @Override - protected void appendDriverProperties(Properties properties) { - super.appendDriverProperties(properties); - properties.put("hibernate.c3p0.min_size", 1); - properties.put("hibernate.c3p0.max_size", 5); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/C3P0JPAConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/C3P0JPAConnectionProviderTest.java deleted file mode 100644 index babad3967..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/C3P0JPAConnectionProviderTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; - -import java.util.Properties; - -public class C3P0JPAConnectionProviderTest extends DriverConnectionProviderTest { - - @Override - protected void appendDriverProperties(Properties properties) { - super.appendDriverProperties(properties); - properties.put("hibernate.c3p0.min_size", 1); - properties.put("hibernate.c3p0.max_size", 5); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/DriverConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/DriverConnectionProviderTest.java deleted file mode 100644 index b73886ad8..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/DriverConnectionProviderTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; - -import org.junit.Test; - -import javax.sql.DataSource; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicLong; - -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.Post; -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.PostComment; - -public class DriverConnectionProviderTest extends AbstractTest { - - private BlogEntityProvider entityProvider = new BlogEntityProvider(); - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - protected DataSource newDataSource() { - return null; - } - - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.hbm2ddl.auto", "update"); - appendDriverProperties(properties); - return properties; - } - - protected void appendDriverProperties(Properties properties) { - DataSourceProvider dataSourceProvider = dataSourceProvider(); - properties.put("hibernate.connection.driver_class", "org.hsqldb.jdbcDriver"); - properties.put("hibernate.connection.url", dataSourceProvider.url()); - properties.put("hibernate.connection.username", dataSourceProvider.username()); - properties.put("hibernate.connection.password", dataSourceProvider.password()); - } - - @Test - public void testConnection() { - for (final AtomicLong i = new AtomicLong(); i.get() < 5; i.incrementAndGet()) { - doInJPA(em -> { - em.persist(new Post(i.get())); - }); - } - - doInJPA(em -> { - Post post = em.find(Post.class, 1L); - PostComment comment = new PostComment("abc"); - comment.setId(1L); - post.addComment(comment); - em.persist(comment); - }); - doInJPA(em -> { - em.createQuery("select p from Post p join fetch p.comments", Post.class).getResultList(); - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/ExternalDataSourceConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/ExternalDataSourceConnectionProviderTest.java deleted file mode 100644 index 0a0809696..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/ExternalDataSourceConnectionProviderTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; - -public class ExternalDataSourceConnectionProviderTest { - - /*protected Properties getProperties() { - Properties properties = new Properties(); - properties.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect"); - //log settings - properties.put("hibernate.hbm2ddl.auto", "update"); - //data source settings - properties.put("hibernate.connection.datasource", newDataSource()); - return properties; - } - - @Override - protected SessionFactory newSessionFactory() { - Properties properties = getProperties(); - - return new Configuration() - .addProperties(properties) - .addAnnotatedClass(SecurityId.class) - .buildSessionFactory( - new StandardServiceRegistryBuilder() - .applySettings(properties) - .build() - ); - } - - protected ProxyDataSource newDataSource() { - JDBCDataSource actualDataSource = new JDBCDataSource(); - actualDataSource.setUrl("jdbc:hsqldb:mem:test"); - actualDataSource.setUser("sa"); - actualDataSource.setPassword(""); - ProxyDataSource proxyDataSource = new ProxyDataSource(); - proxyDataSource.setDataSource(actualDataSource); - proxyDataSource.setListener(new SLF4JQueryLoggingListener()); - return proxyDataSource; - }*/ - - - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/FlexyPoolHibernateConnectionProvider.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/FlexyPoolHibernateConnectionProvider.java deleted file mode 100644 index 598d55721..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/FlexyPoolHibernateConnectionProvider.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; - -import com.vladmihalcea.flexypool.FlexyPoolDataSource; -import org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl; - -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Map; - -/** - * @author Vlad Mihalcea - */ -public class FlexyPoolHibernateConnectionProvider - extends DatasourceConnectionProviderImpl { - - private transient FlexyPoolDataSource flexyPoolDataSource; - - @Override - public void configure(Map props) { - super.configure(props); - flexyPoolDataSource = new FlexyPoolDataSource<>(getDataSource()); - } - - @Override - public Connection getConnection() throws SQLException { - return flexyPoolDataSource.getConnection(); - } - - @Override - public boolean isUnwrappableAs(Class unwrapType) { - return super.isUnwrappableAs(unwrapType) || - getClass().isAssignableFrom(unwrapType); - } - - @Override public void stop() { - flexyPoolDataSource.stop(); - super.stop(); - } -} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/FlexyPoolTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/FlexyPoolTest.java deleted file mode 100644 index 7867c352c..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/FlexyPoolTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; - -import com.vladmihalcea.book.hpjp.hibernate.connection.jta.FlexyPoolEntities; -import com.vladmihalcea.flexypool.FlexyPoolDataSource; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import javax.sql.DataSource; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.*; - -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = FlexyPoolTestConfiguration.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class FlexyPoolTest { - - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - @PersistenceContext - private EntityManager entityManager; - - @Autowired - private TransactionTemplate transactionTemplate; - - @Autowired - private DataSource dataSource; - - private int threadCount = 1; - private int seconds = 60; - - private ExecutorService executorService = Executors.newFixedThreadPool(threadCount); - - @Before - public void init() { - FlexyPoolDataSource flexyPoolDataSource = (FlexyPoolDataSource) dataSource; - flexyPoolDataSource.start(); - } - - @After - public void destroy() { - FlexyPoolDataSource flexyPoolDataSource = (FlexyPoolDataSource) dataSource; - flexyPoolDataSource.stop(); - } - - @Test - public void test() throws InterruptedException, ExecutionException { - long startNanos = System.nanoTime(); - while (TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startNanos) < seconds){ - List> futures = new ArrayList<>(); - for (int i = 0; i < threadCount; i++) { - futures.add(executorService.submit((Callable) () -> { - transactionTemplate.execute((TransactionCallback) transactionStatus -> { - for (int j = 0; j < 1000; j++) { - entityManager.persist(new FlexyPoolEntities.Post()); - } - entityManager.createQuery("select count(p) from Post p").getSingleResult(); - return null; - }); - return null; - })); - } - for (Future future : futures) { - future.get(); - } - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/FlexyPoolTestConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/FlexyPoolTestConfiguration.java deleted file mode 100644 index 9580bb02c..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/FlexyPoolTestConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; - -import com.vladmihalcea.book.hpjp.hibernate.connection.jta.FlexyPoolEntities; -import com.vladmihalcea.book.hpjp.util.spring.config.jpa.PostgreSQLJpaConfiguration; -import com.vladmihalcea.flexypool.FlexyPoolDataSource; -import com.vladmihalcea.flexypool.adaptor.DataSourcePoolAdapter; -import org.springframework.context.annotation.Configuration; - -import javax.sql.DataSource; - -@Configuration -public class FlexyPoolTestConfiguration extends PostgreSQLJpaConfiguration { - - @Override - protected Class configurationClass() { - return FlexyPoolEntities.class; - } - - @Override - public DataSource actualDataSource() { - final DataSource dataSource = super.actualDataSource(); - com.vladmihalcea.flexypool.config.Configuration configuration = new com.vladmihalcea.flexypool.config.Configuration.Builder<>( - getClass().getSimpleName(), dataSource, DataSourcePoolAdapter.FACTORY).build(); - FlexyPoolDataSource flexyPoolDataSource = new FlexyPoolDataSource<>(configuration); - return flexyPoolDataSource; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/HikariCPCockroachDBConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/HikariCPCockroachDBConnectionProviderTest.java deleted file mode 100644 index a6d4f25cb..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/HikariCPCockroachDBConnectionProviderTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; - -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicLong; - -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.JoinTable; -import javax.persistence.ManyToMany; -import javax.persistence.OneToMany; -import javax.persistence.OneToOne; -import javax.persistence.Table; -import javax.persistence.Version; - -import org.hibernate.hikaricp.internal.HikariCPConnectionProvider; - -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.providers.CockroachDBDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.HsqldbDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; - -public class HikariCPCockroachDBConnectionProviderTest extends DriverConnectionProviderTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - protected DataSourceProvider dataSourceProvider() { - return new CockroachDBDataSourceProvider(); - } - - @Override - protected void appendDriverProperties(Properties properties) { - DataSourceProvider dataSourceProvider = dataSourceProvider(); - properties.put("hibernate.connection.provider_class", HikariCPConnectionProvider.class.getName()); - properties.put("hibernate.hikari.minimumPoolSize", "1"); - properties.put("hibernate.hikari.maximumPoolSize", "2"); - properties.put("hibernate.hikari.transactionIsolation", "TRANSACTION_SERIALIZABLE"); - properties.put("hibernate.hikari.dataSourceClassName", dataSourceProvider.dataSourceClassName().getName()); - properties.put("hibernate.hikari.dataSource.url", dataSourceProvider.url()); - properties.put("hibernate.hikari.dataSource.user", dataSourceProvider.username()); - properties.put("hibernate.hikari.dataSource.password", dataSourceProvider.password()); - } - - @Test - public void testConnection() { - for (final AtomicLong i = new AtomicLong(); i.get() < 5; i.incrementAndGet()) { - doInJPA(em -> { - em.persist(new Post(i.get())); - }); - } - - doInJPA(em -> { - Post post = em.find(Post.class, 1L); - }); - doInJPA(em -> { - em.createQuery("select p from Post p", Post.class).getResultList(); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Post() {} - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/HikariCPConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/HikariCPConnectionProviderTest.java deleted file mode 100644 index 16f19e626..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/HikariCPConnectionProviderTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; - -import org.hibernate.hikaricp.internal.HikariCPConnectionProvider; - -import java.util.Properties; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; - -public class HikariCPConnectionProviderTest extends DriverConnectionProviderTest { - - @Override - protected void appendDriverProperties(Properties properties) { - DataSourceProvider dataSourceProvider = dataSourceProvider(); - properties.put("hibernate.connection.provider_class", HikariCPConnectionProvider.class.getName()); - properties.put("hibernate.hikari.maximumPoolSize", "5"); - properties.put("hibernate.hikari.dataSourceClassName", dataSourceProvider.dataSourceClassName().getName()); - properties.put("hibernate.hikari.dataSource.url", dataSourceProvider.url()); - properties.put("hibernate.hikari.dataSource.user", dataSourceProvider.username()); - properties.put("hibernate.hikari.dataSource.password", dataSourceProvider.password()); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/JPADataSourceConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/JPADataSourceConnectionProviderTest.java deleted file mode 100644 index e974585a7..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/JPADataSourceConnectionProviderTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; - -import com.vladmihalcea.book.hpjp.util.PersistenceUnitInfoImpl; - -import java.util.Properties; - -public class JPADataSourceConnectionProviderTest extends DriverConnectionProviderTest { - - protected void appendDriverProperties(Properties properties) { - - } - - @Override - protected PersistenceUnitInfoImpl persistenceUnitInfo(String name) { - PersistenceUnitInfoImpl persistenceUnitInfo = super.persistenceUnitInfo(name); - return persistenceUnitInfo.setNonJtaDataSource(dataSourceProvider().dataSource()); - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/JPADriverConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/JPADriverConnectionProviderTest.java deleted file mode 100644 index 09fbc29e4..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/JPADriverConnectionProviderTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; - -import java.util.Properties; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; - -public class JPADriverConnectionProviderTest extends DriverConnectionProviderTest { - - protected void appendDriverProperties(Properties properties) { - DataSourceProvider dataSourceProvider = dataSourceProvider(); - properties.put("javax.persistence.jdbc.driver", "org.hsqldb.jdbcDriver"); - properties.put("javax.persistence.jdbc.url", dataSourceProvider.url()); - properties.put("javax.persistence.jdbc.user", dataSourceProvider.username()); - properties.put("javax.persistence.jdbc.password", dataSourceProvider.password()); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/ResourceLocalDelayConnectionAcquisitionTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/ResourceLocalDelayConnectionAcquisitionTest.java deleted file mode 100644 index 225e1a2f1..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/ResourceLocalDelayConnectionAcquisitionTest.java +++ /dev/null @@ -1,267 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; - -import java.io.InputStream; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.TimeUnit; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; -import javax.sql.DataSource; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - -import org.junit.Test; - -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Slf4jReporter; -import com.codahale.metrics.Timer; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.flexypool.FlexyPoolDataSource; -import com.vladmihalcea.flexypool.adaptor.DataSourcePoolAdapter; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -public class ResourceLocalDelayConnectionAcquisitionTest extends AbstractTest { - - public static final String DATA_FILE_PATH = "data/weather.xml"; - - private SimpleDateFormat simpleDateFormat = new SimpleDateFormat( "yyyy-MM-dd" ); - - private long warmUpDuration = TimeUnit.SECONDS.toNanos( 10 ); - - private long measurementsDuration = TimeUnit.SECONDS.toNanos( 17 ); - - private int parseCount = 100; - - private FlexyPoolDataSource flexyPoolDataSource; - - private MetricRegistry metricRegistry = new MetricRegistry(); - - private Timer timer = metricRegistry.timer( getClass().getSimpleName() ); - - private Slf4jReporter logReporter = Slf4jReporter - .forRegistry(metricRegistry) - .outputTo(LOGGER) - .build(); - - @Override - protected Class[] entities() { - return new Class[] { - Forecast.class - }; - } - - protected boolean connectionPooling() { - return false; - } - - protected HikariConfig hikariConfig(DataSource dataSource) { - HikariConfig hikariConfig = super.hikariConfig( dataSource ); - hikariConfig.setAutoCommit( false ); - return hikariConfig; - } - - protected HikariDataSource connectionPoolDataSource(DataSource dataSource) { - HikariConfig hikariConfig = new HikariConfig(); - int cpuCores = Runtime.getRuntime().availableProcessors(); - hikariConfig.setMaximumPoolSize(cpuCores * 4); - hikariConfig.setDataSource(dataSource); - - return new HikariDataSource(hikariConfig); - } - - @Override - protected DataSource newDataSource() { - DataSource dataSource = super.newDataSource(); - - com.vladmihalcea.flexypool.config.Configuration configuration = new com.vladmihalcea.flexypool.config.Configuration.Builder<>( - getClass().getSimpleName(), dataSource, DataSourcePoolAdapter.FACTORY) - .setMetricLogReporterMillis( TimeUnit.SECONDS.toMillis( 15 ) ) - .build(); - flexyPoolDataSource = new FlexyPoolDataSource<>( configuration); - return flexyPoolDataSource; - } - - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.jdbc.batch_size", "50"); - properties.put("hibernate.connection.provider_disables_autocommit", "true"); - return properties; - } - - @Test - public void testConnection() { - long warmUpThreshold = System.nanoTime() + warmUpDuration; - LOGGER.info( "Warming up" ); - - while ( System.nanoTime() < warmUpThreshold ) { - importForecasts(); - } - - long measurementsThreshold = System.nanoTime() + measurementsDuration; - - LOGGER.info( "Measuring" ); - flexyPoolDataSource.start(); - int transactionCount = 0; - while ( System.nanoTime() < measurementsThreshold ) { - importForecasts(); - transactionCount++; - } - flexyPoolDataSource.stop(); - logReporter.report(); - LOGGER.info( "Transaction throughput: {}", transactionCount); - } - - private void importForecasts() { - doInJPA(entityManager -> { - long startNanos = System.nanoTime(); - List forecasts = null; - for ( int i = 0; i < parseCount; i++ ) { - Document forecastXmlDocument = readXmlDocument( DATA_FILE_PATH ); - forecasts = parseForecasts(forecastXmlDocument); - } - timer.update( System.nanoTime() - startNanos, TimeUnit.NANOSECONDS ); - - if ( forecasts != null ) { - for(Forecast forecast : forecasts.subList( 0, 50 )) { - entityManager.persist( forecast ); - } - } - }); - } - - private Document readXmlDocument(String filePath) { - try (InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream( filePath )) { - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); - Document doc = dBuilder.parse( inputStream ); - - doc.getDocumentElement().normalize(); - - return doc; - } - catch (Exception e) { - throw new IllegalArgumentException( e ); - } - } - - private List parseForecasts(Document xmlDocument) { - NodeList cityNodes = xmlDocument.getElementsByTagName( "localitate"); - List forecasts = new ArrayList<>( ); - for ( int i = 0; i < cityNodes.getLength(); i++ ) { - Node cityNode = cityNodes.item(i); - String city = cityNode.getAttributes().getNamedItem( "nume" ).getNodeValue(); - - NodeList forecastNodes = cityNode.getChildNodes(); - for ( int j = 0; j < forecastNodes.getLength(); j++ ) { - Node forecastNode = forecastNodes.item(j); - if( !"prognoza".equals( forecastNode.getNodeName() ) ) { - continue; - } - - Forecast forecast = new Forecast(); - forecast.setCity( city ); - - String dateValue = forecastNode.getAttributes().getNamedItem( "data" ).getNodeValue(); - try { - forecast.setDate( simpleDateFormat.parse( dateValue ) ); - } - catch (ParseException e) { - throw new IllegalArgumentException( e ); - } - - NodeList forecastDetailsNodes = forecastNode.getChildNodes(); - for ( int k = 0; k < forecastDetailsNodes.getLength(); k++ ) { - Node forecastDetailsNode = forecastDetailsNodes.item(k); - switch ( forecastDetailsNode.getNodeName() ) { - case "temp_min": - forecast.setTemperatureMin( Byte.valueOf( forecastDetailsNode.getTextContent() ) ); - break; - case "temp_max": - forecast.setTemperatureMax( Byte.valueOf( forecastDetailsNode.getTextContent() ) ); - break; - case "fenomen_descriere": - forecast.setDescription( forecastDetailsNode.getTextContent() ); - break; - } - } - - forecasts.add( forecast ); - } - } - return forecasts; - } - - @Entity(name = "Forecast") - public static class Forecast { - - @Id - @GeneratedValue - private Long id; - - private String city; - - @Temporal( TemporalType.DATE ) - @Column(name = "forecast_date") - private Date date; - - @Column(name = "temperature_min") - private byte temperatureMin; - - @Column(name = "temperature_max") - private byte temperatureMax; - - private String description; - - public String getCity() { - return city; - } - - public void setCity(String city) { - this.city = city; - } - - public Date getDate() { - return date; - } - - public void setDate(Date date) { - this.date = date; - } - - public byte getTemperatureMin() { - return temperatureMin; - } - - public void setTemperatureMin(byte temperatureMin) { - this.temperatureMin = temperatureMin; - } - - public byte getTemperatureMax() { - return temperatureMax; - } - - public void setTemperatureMax(byte temperatureMax) { - this.temperatureMax = temperatureMax; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/JtaConnectionReleaseConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/JtaConnectionReleaseConfiguration.java deleted file mode 100644 index 73880c033..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/JtaConnectionReleaseConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection.jta; - -import com.vladmihalcea.book.hpjp.hibernate.statistics.TransactionStatisticsFactory; -import com.vladmihalcea.book.hpjp.util.spring.config.jta.PostgreSQLJtaTransactionManagerConfiguration; -import org.springframework.context.annotation.Configuration; - -import java.util.Properties; - -@Configuration -public class JtaConnectionReleaseConfiguration extends PostgreSQLJtaTransactionManagerConfiguration { - - @Override - protected Class configurationClass() { - return this.getClass(); - } - - @Override - protected Properties additionalProperties() { - Properties properties = super.additionalProperties(); - properties.put("hibernate.generate_statistics", "true"); - properties.put("hibernate.stats.factory", TransactionStatisticsFactory.class.getName()); - - //properties.setProperty("hibernate.connection.release_mode", "after_transaction"); - properties.setProperty("hibernate.connection.release_mode", "after_statement"); - return properties; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/JtaConnectionReleaseTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/JtaConnectionReleaseTest.java deleted file mode 100644 index 74bebcef4..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/JtaConnectionReleaseTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection.jta; - -import org.hibernate.Session; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertNotNull; - -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = JtaConnectionReleaseConfiguration.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class JtaConnectionReleaseTest { - - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - @PersistenceContext - private EntityManager entityManager; - - @Autowired - private TransactionTemplate transactionTemplate; - - private int[] batches = {10, 50, 100, 500, 1000, 5000, 10000}; - - @Test - public void test() { - //Warming up - for (int i = 0; i < 100; i++) { - transactionTemplate.execute((TransactionCallback) transactionStatus -> { - assertNotNull(entityManager.createNativeQuery("select now()").getSingleResult()); - return null; - }); - } - for (int batch : batches) { - long startNanos = System.nanoTime(); - transactionTemplate.execute((TransactionCallback) transactionStatus -> { - for (int i = 0; i < batch; i++) { - assertNotNull(entityManager.createNativeQuery("select now()").getSingleResult()); - } - return null; - }); - LOGGER.info("Transaction took {} millis", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); - } - transactionTemplate.execute((TransactionCallback) transactionStatus -> { - entityManager.unwrap(Session.class).getSessionFactory().getStatistics().logSummary(); - return null; - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/JtaFlexyPoolTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/JtaFlexyPoolTest.java deleted file mode 100644 index 1a06a5032..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/JtaFlexyPoolTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection.jta; - -import com.vladmihalcea.flexypool.FlexyPoolDataSource; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import javax.sql.DataSource; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.*; - -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = JtaFlexyPoolTestConfiguration.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class JtaFlexyPoolTest { - - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - @PersistenceContext - private EntityManager entityManager; - - @Autowired - private TransactionTemplate transactionTemplate; - - @Autowired - private DataSource dataSource; - - private int threadCount = 10; - private int seconds = 60; - - private ExecutorService executorService = Executors.newFixedThreadPool(threadCount); - - @Before - public void init() { - FlexyPoolDataSource flexyPoolDataSource = (FlexyPoolDataSource) dataSource; - flexyPoolDataSource.start(); - } - - @After - public void destroy() { - FlexyPoolDataSource flexyPoolDataSource = (FlexyPoolDataSource) dataSource; - flexyPoolDataSource.stop(); - } - - @Test - public void test() throws InterruptedException, ExecutionException { - long startNanos = System.nanoTime(); - while (TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startNanos) < seconds){ - List> futures = new ArrayList<>(); - for (int i = 0; i < threadCount; i++) { - futures.add(executorService.submit((Callable) () -> { - transactionTemplate.execute((TransactionCallback) transactionStatus -> { - for (int j = 0; j < 1000; j++) { - entityManager.persist(new FlexyPoolEntities.Post()); - } - entityManager.createQuery("select count(p) from Post p").getSingleResult(); - return null; - }); - return null; - })); - } - for (Future future : futures) { - future.get(); - } - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/JtaFlexyPoolTestConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/JtaFlexyPoolTestConfiguration.java deleted file mode 100644 index ce15fa6fd..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/JtaFlexyPoolTestConfiguration.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection.jta; - -import bitronix.tm.resource.jdbc.PoolingDataSource; -import com.vladmihalcea.book.hpjp.util.spring.config.jta.PostgreSQLJtaTransactionManagerConfiguration; -import com.vladmihalcea.flexypool.FlexyPoolDataSource; -import com.vladmihalcea.flexypool.adaptor.BitronixPoolAdapter; -import org.springframework.context.annotation.Configuration; - -import javax.sql.DataSource; - -@Configuration -public class JtaFlexyPoolTestConfiguration extends PostgreSQLJtaTransactionManagerConfiguration { - - @Override - protected Class configurationClass() { - return FlexyPoolEntities.class; - } - - @Override - public DataSource actualDataSource() { - final PoolingDataSource poolingDataSource = (PoolingDataSource) super.actualDataSource(); - com.vladmihalcea.flexypool.config.Configuration configuration = new com.vladmihalcea.flexypool.config.Configuration.Builder<>( - getClass().getSimpleName(), poolingDataSource, BitronixPoolAdapter.FACTORY).build(); - - FlexyPoolDataSource flexyPoolDataSource = new FlexyPoolDataSource(configuration) { - @Override - public void start() { - poolingDataSource.init(); - super.start(); - } - - @Override - public void stop() { - super.stop(); - poolingDataSource.close(); - } - }; - return flexyPoolDataSource; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/criteria/CriteriaFetchAliasTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/criteria/CriteriaFetchAliasTest.java deleted file mode 100644 index 6d9c4cfb1..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/criteria/CriteriaFetchAliasTest.java +++ /dev/null @@ -1,209 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.criteria; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.Criteria; -import org.hibernate.FetchMode; -import org.hibernate.Session; -import org.hibernate.criterion.Restrictions; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class CriteriaFetchAliasTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - }; - } - - @Test - public void test() { - doInJPA(entityManager -> { - Post post = new Post(1L); - post.title = "Postit"; - - PostComment comment1 = new PostComment(); - comment1.id = 1L; - comment1.review = "Good"; - - PostComment comment2 = new PostComment(); - comment2.id = 2L; - comment2.review = "Excellent"; - - post.addComment(comment1); - post.addComment(comment2); - entityManager.persist(post); - - Session session = entityManager.unwrap(Session.class); - Criteria criteria = session.createCriteria(Post.class) - .add(Restrictions.eq("title", "post")); - - LOGGER.info("Criteria: {}", criteria); - }); - - doInJPA(entityManager -> { - LOGGER.info("No alias"); - Session session = entityManager.unwrap(Session.class); - List posts = session - .createCriteria(Post.class) - .setFetchMode("comments", FetchMode.JOIN) - .add(Restrictions.eq("title", "Postit")) - .list(); - assertEquals(2, posts.size()); - }); - - try { - doInJPA(entityManager -> { - LOGGER.info("With alias"); - Session session = entityManager.unwrap(Session.class); - List posts = session - .createCriteria(Post.class, "post") - .setFetchMode("post.comments", FetchMode.JOIN) - .add(Restrictions.eq("post.title", "Postit")) - .list(); - assertEquals(2, posts.size()); - }); - } catch (Throwable e) { - LOGGER.error("Failure", e); - } - - doInJPA(entityManager -> { - Post newPost = new Post(2L); - entityManager.persist(newPost); - }); - doInJPA(entityManager -> { - LOGGER.info("In query"); - Session session = entityManager.unwrap(Session.class); - - - List postComments = new ArrayList<>(); - postComments.add(new PostComment()); - postComments.get(0).setId(2L); - postComments.add(new PostComment()); - postComments.get(1).setId(3L); - - - List ids = postComments.stream().map(PostComment::getId).collect(Collectors.toList()); - List filtered = session.createCriteria(Post.class) - .createAlias("comments", "c") - .add( Restrictions.in( "c.id", ids ) ) - .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) - .list(); - assertEquals(1, filtered.size()); - - ids = new ArrayList<>(); - ids.add(3L); - ids.add(4L); - - filtered = session.createCriteria(Post.class) - .createAlias("comments", "c") - .add( Restrictions.in( "c.id", ids ) ) - .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) - .list(); - assertEquals(0, filtered.size()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Post() {} - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/criteria/CriteriaNestedQueryTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/criteria/CriteriaNestedQueryTest.java deleted file mode 100644 index c7a3b679e..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/criteria/CriteriaNestedQueryTest.java +++ /dev/null @@ -1,165 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.criteria; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.Criteria; -import org.hibernate.Session; -import org.hibernate.criterion.DetachedCriteria; -import org.hibernate.criterion.Projections; -import org.hibernate.criterion.Restrictions; -import org.hibernate.criterion.Subqueries; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class CriteriaNestedQueryTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - }; - } - - @Test - public void test() { - doInJPA(entityManager -> { - Post post = new Post(1L); - post.title = "Postit"; - - PostComment comment1 = new PostComment(); - comment1.id = 1L; - comment1.review = "Good"; - - PostComment comment2 = new PostComment(); - comment2.id = 2L; - comment2.review = "Excellent"; - - post.addComment(comment1); - post.addComment(comment2); - entityManager.persist(post); - - Session session = entityManager.unwrap(Session.class); - Criteria criteria = session.createCriteria(Post.class) - .add(Restrictions.eq("title", "post")); - - LOGGER.info("Criteria: {}", criteria); - }); - - doInJPA(entityManager -> { - LOGGER.info("No alias"); - Session session = entityManager.unwrap(Session.class); - - DetachedCriteria innerCriteria = DetachedCriteria.forClass(PostComment.class, "inner") - .add(Restrictions.eqProperty("inner.post.id","upper.id")) - .setProjection(Projections.projectionList().add(Projections.max("inner.id"))); - - DetachedCriteria outerCriteria= DetachedCriteria.forClass(Post.class, "upper"); - outerCriteria.createAlias("upper.comments", "comments"); - outerCriteria.add(Subqueries.propertyEq("comments.id", innerCriteria )); - outerCriteria.add(Restrictions.eq("comments.review", "Excellent")); - - List posts = outerCriteria.getExecutableCriteria(session).list(); - assertEquals(1, posts.size()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Post() {} - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/AbstractEqualityCheckTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/AbstractEqualityCheckTest.java deleted file mode 100644 index 951e76447..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/AbstractEqualityCheckTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.equality; - -import com.vladmihalcea.book.hpjp.hibernate.identifier.Identifiable; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.Session; - -import java.io.Serializable; -import java.util.HashSet; -import java.util.Set; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * @author Vlad Mihalcea - */ -public abstract class AbstractEqualityCheckTest extends AbstractTest { - - protected > void assertEqualityConstraints(Class clazz, T entity) { - Set tuples = new HashSet<>(); - - assertFalse(tuples.contains(entity)); - tuples.add(entity); - assertTrue(tuples.contains(entity)); - - doInJPA(entityManager -> { - entityManager.persist(entity); - entityManager.flush(); - assertTrue("The entity is found after it's persisted", - tuples.contains(entity)); - }); - - //The entity is found after the entity is detached - assertTrue(tuples.contains(entity)); - - doInJPA(entityManager -> { - T _entity = entityManager.merge(entity); - assertTrue("The entity is found after it's merged", - tuples.contains(_entity)); - }); - - doInJPA(entityManager -> { - entityManager.unwrap(Session.class).update(entity); - assertTrue("The entity is found after it's reattached", - tuples.contains(entity)); - }); - - doInJPA(entityManager -> { - T _entity = entityManager.find(clazz, entity.getId()); - assertTrue("The entity is found after it's loaded " + - "in an other Persistence Context", - tuples.contains(_entity)); - }); - - doInJPA(entityManager -> { - T _entity = entityManager.getReference(clazz, entity.getId()); - assertTrue("The entity is found after it's loaded as a Proxy " + - "in an other Persistence Context", - tuples.contains(_entity)); - }); - - executeSync(() -> { - doInJPA(entityManager -> { - T _entity = entityManager.find(clazz, entity.getId()); - assertTrue("The entity is found after it's loaded " + - "in an other Persistence Context and " + - "in an other thread", - tuples.contains(_entity)); - }); - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/DefaultEqualityTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/DefaultEqualityTest.java deleted file mode 100644 index 664847301..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/DefaultEqualityTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.equality; - -import com.vladmihalcea.book.hpjp.hibernate.identifier.Identifiable; -import org.junit.Test; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Table; - -/** - * @author Vlad Mihalcea - */ -public class DefaultEqualityTest extends AbstractEqualityCheckTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class - }; - } - - @Test - public void testEquality() { - Post post = new Post(); - post.setTitle("High-PerformanceJava Persistence"); - - assertEqualityConstraints(Post.class, post); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post implements Identifiable { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/DefaultIdEqualityTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/DefaultIdEqualityTest.java deleted file mode 100644 index f231763ac..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/DefaultIdEqualityTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.equality; - -import com.vladmihalcea.book.hpjp.hibernate.identifier.Identifiable; -import org.junit.Test; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Table; -import java.util.Objects; - -/** - * @author Vlad Mihalcea - */ -public class DefaultIdEqualityTest extends AbstractEqualityCheckTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Test - public void testEquality() { - Post post = new Post(); - post.setTitle("High-PerformanceJava Persistence"); - - assertEqualityConstraints(Post.class, post); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post implements Identifiable { - - @Id - @GeneratedValue - private Long id; - - private String title; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Post)) return false; - return Objects.equals(id, ((Post) o).id); - } - @Override - public int hashCode() { - return Objects.hash(id); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/ProperIdEqualityTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/ProperIdEqualityTest.java deleted file mode 100644 index 9c71f9311..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/ProperIdEqualityTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.equality; - -import com.vladmihalcea.book.hpjp.hibernate.identifier.Identifiable; -import org.junit.Test; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Table; - -/** - * @author Vlad Mihalcea - */ -public class ProperIdEqualityTest extends AbstractEqualityCheckTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Test - public void testEquality() { - Post post = new Post(); - post.setTitle("High-PerformanceJava Persistence"); - - assertEqualityConstraints(Post.class, post); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post implements Identifiable { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Post() {} - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Post)) return false; - return id != null && id.equals(((Post) o).id); - } - - @Override - public int hashCode() { - return 31; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/CriteriaAPITest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/CriteriaAPITest.java deleted file mode 100644 index 84f6529c7..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/CriteriaAPITest.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import com.vladmihalcea.book.hpjp.hibernate.forum.*; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.junit.Test; - -import javax.persistence.EntityManager; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Predicate; -import javax.persistence.criteria.Root; -import java.util.List; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * @author Vlad Mihalcea - */ -public class CriteriaAPITest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - Tag.class, - PostDetails.class - }; - } - - - @Override - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("high-performance-java-persistence"); - entityManager.persist(post); - }); - } - - @Test - public void testFind() { - doInJPA(entityManager -> { - List posts = filterPosts(entityManager, "high-performance%"); - assertFalse(posts.isEmpty()); - }); - doInJPA(entityManager -> { - List posts = filterPosts(entityManager, null); - assertTrue(posts.isEmpty()); - }); - } - - private List filterPosts(EntityManager entityManager, String titlePattern) { - CriteriaBuilder builder = entityManager.getCriteriaBuilder(); - CriteriaQuery criteria = builder.createQuery(Post.class); - Root fromPost = criteria.from(Post.class); - - Predicate titlePredicate = titlePattern == null ? - builder.isNull(fromPost.get(Post_.title)) : - builder.like(fromPost.get(Post_.title), titlePattern); - - criteria.where(titlePredicate); - List posts = entityManager.createQuery(criteria).getResultList(); - - return posts; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/DistinctTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/DistinctTest.java deleted file mode 100644 index e186174db..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/DistinctTest.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.jpa.QueryHints; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public class DistinctTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class, - }; - } - - - @Override - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - post.addComment(new PostComment("Excellent!")); - post.addComment(new PostComment("Great!")); - - entityManager.persist(post); - }); - } - - @Test - public void testWithoutDistinct() { - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select p " + - "from Post p " + - "left join fetch p.comments " + - "where p.title = :title", Post.class) - .setParameter("title", "High-Performance Java Persistence") - .getResultList(); - - LOGGER.info("Fetched {} post entities: {}", posts.size(), posts); - }); - } - - @Test - public void testWithDistinctScalarQuery() { - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select distinct p.title " + - "from Post p ", String.class) - .getResultList(); - - LOGGER.info("Fetched {} post entities: {}", posts.size(), posts); - }); - } - - @Test - public void testWithDistinct() { - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select distinct p " + - "from Post p " + - "left join fetch p.comments " + - "where p.title = :title", Post.class) - .setParameter("title", "High-Performance Java Persistence") - .getResultList(); - - LOGGER.info("Fetched {} post entities: {}", posts.size(), posts); - }); - } - - @Test - public void testWithDistinctAndQueryHint() { - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select distinct p " + - "from Post p " + - "left join fetch p.comments " + - "where p.title = :title", Post.class) - .setParameter("title", "High-Performance Java Persistence") - .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false) - .getResultList(); - - LOGGER.info("Fetched {} post entities: {}", posts.size(), posts); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - - @Override - public String toString() { - return "Post{" + - "id=" + id + - ", title='" + title + '\'' + - '}'; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/EagerFetchingManyToOneFindEntityTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/EagerFetchingManyToOneFindEntityTest.java deleted file mode 100644 index 4f0d0de9f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/EagerFetchingManyToOneFindEntityTest.java +++ /dev/null @@ -1,197 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import com.vladmihalcea.book.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.Collections; -import java.util.List; - -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -public class EagerFetchingManyToOneFindEntityTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class, - }; - } - - - @Override - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle(String.format("Post nr. %d", 1)); - PostComment comment = new PostComment(); - comment.setId(1L); - comment.setPost(post); - comment.setReview("Excellent!"); - entityManager.persist(post); - entityManager.persist(comment); - }); - } - - @Test - public void testFind() { - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - assertNotNull(comment); - }); - } - - @Test - public void testFindWithQuery() { - doInJPA(entityManager -> { - Long commentId = 1L; - PostComment comment = entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "where pc.id = :id", PostComment.class) - .setParameter("id", commentId) - .getSingleResult(); - assertNotNull(comment); - }); - } - - @Test - public void testFindWithQueryAndFetch() { - doInJPA(entityManager -> { - Long commentId = 1L; - PostComment comment = entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "left join fetch pc.post p " + - "where pc.id = :id", PostComment.class) - .setParameter("id", commentId) - .getSingleResult(); - assertNotNull(comment); - }); - } - - @Test - public void testFindWithNamedEntityGraph() { - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L, - Collections.singletonMap( - "javax.persistence.fetchgraph", - entityManager.getEntityGraph("PostComment.post") - ) - ); - LOGGER.info("Fetch entity graph"); - assertNotNull(comment); - }); - } - - @Test - public void testNPlusOneDetection() { - try { - String review = "Excellent!"; - - doInJPA(entityManager -> { - LOGGER.info("Detect N+1"); - SQLStatementCountValidator.reset(); - List comments = entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "join fetch pc.post " + - "where pc.review = :review", PostComment.class) - .setParameter("review", review) - .getResultList(); - SQLStatementCountValidator.assertSelectCount(1); - }); - } catch (Exception expected) { - LOGGER.error("Exception", expected); - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Post() { - } - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @NamedEntityGraph(name = "PostComment.post", attributeNodes = {}) - public static class PostComment { - - @Id - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public PostComment() { - } - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/EagerFetchingOneToManyBagsFindEntityTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/EagerFetchingOneToManyBagsFindEntityTest.java deleted file mode 100644 index ec8fbeb68..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/EagerFetchingOneToManyBagsFindEntityTest.java +++ /dev/null @@ -1,177 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -public class EagerFetchingOneToManyBagsFindEntityTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class, - Tag.class - }; - } - - - @Override - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle(String.format("Post nr. %d", 1)); - PostComment comment = new PostComment(); - comment.setId(1L); - comment.setReview("Excellent!"); - entityManager.persist(post); - entityManager.persist(comment); - post.comments.add(comment); - }); - } - - @Test - public void testGet() { - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertNotNull(post); - }); - } - - @Test - public void testFindWithQuery() { - doInJPA(entityManager -> { - Long postId = 1L; - Post post = entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.tags " + - "join fetch p.comments " + - "where p.id = :id", Post.class) - .setParameter("id", postId) - .getSingleResult(); - assertNotNull(post); - }); - } - - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - @OneToMany(fetch = FetchType.EAGER) - private List comments = new ArrayList<>(); - - @ManyToMany(fetch = FetchType.EAGER) - @JoinTable(name = "post_tag", - joinColumns = @JoinColumn(name = "post_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id") - ) - private List tags = new ArrayList<>(); - - public Post() { - } - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getTags() { - return tags; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - private Long id; - - private String review; - - public PostComment() { - } - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } - - @Entity(name = "Tag") - @Table(name = "tag") - public static class Tag { - - @Id - private Long id; - - private String name; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/EagerFetchingOneToManyFindEntityTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/EagerFetchingOneToManyFindEntityTest.java deleted file mode 100644 index 4c8fe6caa..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/EagerFetchingOneToManyFindEntityTest.java +++ /dev/null @@ -1,222 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -public class EagerFetchingOneToManyFindEntityTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class, - Tag.class - }; - } - - - @Override - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle(String.format("Post nr. %d", 1)); - - for (long i = 0; i < 20; i++) { - PostComment comment = new PostComment(); - comment.setId(i); - post.addComment(comment); - comment.setReview("Excellent!"); - - entityManager.persist(comment); - } - - for (long i = 0; i < 10; i++) { - Tag tag = new Tag(); - tag.setId(i); - tag.setName("My tag"); - - entityManager.persist(tag); - post.getTags().add(tag); - } - - entityManager.persist(post); - }); - } - - @Test - public void testGet() { - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertNotNull(post); - }); - } - - @Test - public void testFindWithQuery() { - doInJPA(entityManager -> { - Long postId = 1L; - Post post = entityManager.createQuery( - "select p from Post p where p.id = :id", Post.class) - .setParameter("id", postId) - .getSingleResult(); - assertNotNull(post); - }); - } - - @Test - public void testFindWithJoinFetchQuery() { - doInJPA(entityManager -> { - Long postId = 1L; - List posts = entityManager.createQuery( - "select p " + - "from Post p " + - "left join fetch p.comments " + - "left join fetch p.tags", Post.class) - .getResultList(); - assertEquals(200, posts.size()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - @OneToMany(mappedBy = "post", fetch = FetchType.EAGER) - private Set comments = new HashSet<>(); - - @ManyToMany(fetch = FetchType.EAGER) - @JoinTable(name = "post_tag", - joinColumns = @JoinColumn(name = "post_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id") - ) - private Set tags = new HashSet<>(); - - public Post() { - } - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public Set getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - - public Set getTags() { - return tags; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public PostComment() { - } - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } - - @Entity(name = "Tag") - @Table(name = "tag") - public static class Tag { - - @Id - private Long id; - - private String name; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/HibernateProxyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/HibernateProxyTest.java deleted file mode 100644 index 18b90b413..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/HibernateProxyTest.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import java.util.Objects; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToOne; -import javax.persistence.Table; - -import org.hibernate.Hibernate; - -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class HibernateProxyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class, - }; - } - - @Test - public void test() { - Post _post = doInJPA(entityManager -> { - Post post = new Post(); - post.setId( 1L ); - post.setTitle( "High-Performance Java Persistence" ); - entityManager.persist(post); - return post; - }); - - doInJPA(entityManager -> { - LOGGER.info( "Saving a PostComment" ); - - Post post = entityManager.getReference(Post.class, 1L); - - PostComment comment = new PostComment(); - comment.setId( 1L ); - comment.setPost( post ); - comment.setReview( "A must read!" ); - entityManager.persist( comment ); - }); - - doInJPA(entityManager -> { - LOGGER.info( "Loading a PostComment" ); - - PostComment comment = entityManager.find( - PostComment.class, - 1L - ); - - LOGGER.info( "Loading the Post Proxy" ); - - assertEquals( - "High-Performance Java Persistence", - comment.getPost().getTitle() - ); - }); - - doInJPA(entityManager -> { - LOGGER.info( "Equality check" ); - Post post = entityManager.getReference(Post.class, 1L); - LOGGER.info( "Post entity class: {}", post.getClass().getName() ); - - assertFalse(_post.equals(post)); - - assertTrue(_post.equals( Hibernate.unproxy( post))); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Post)) return false; - return id != null && id.equals(((Post) o).id); - } - - @Override - public int hashCode() { - return 31; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyAttributeTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyAttributeTest.java deleted file mode 100644 index 03ae96157..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyAttributeTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import com.vladmihalcea.book.hpjp.hibernate.forum.Attachment; -import com.vladmihalcea.book.hpjp.hibernate.forum.MediaType; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import org.junit.Test; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicReference; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.fail; - -/** - * @author Vlad Mihalcea - */ -public class LazyAttributeTest extends AbstractMySQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Attachment.class, - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - //properties.setProperty(AvailableSettings.USE_STREAMS_FOR_BINARY, Boolean.FALSE.toString()); - return properties; - } - - @Test - public void test() throws URISyntaxException { - final Path bookFilePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource("ehcache.xml").toURI()); - final Path videoFilePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource("spy.properties").toURI()); - - AtomicReference bookIdHolder = new AtomicReference<>(); - AtomicReference videoIdHolder = new AtomicReference<>(); - - doInJPA(entityManager -> { - try { - Attachment book = new Attachment(); - book.setName("High-Performance Java Persistence"); - book.setMediaType(MediaType.PDF); - book.setContent(Files.readAllBytes(bookFilePath)); - entityManager.persist(book); - - Attachment video = new Attachment(); - video.setName("High-Performance Hibernate"); - video.setMediaType(MediaType.MPEG_VIDEO); - video.setContent(Files.readAllBytes(videoFilePath)); - entityManager.persist(video); - - bookIdHolder.set(book.getId()); - videoIdHolder.set(video.getId()); - } catch (IOException e) { - fail(e.getMessage()); - } - }); - - doInJPA(entityManager -> { - try { - Long bookId = bookIdHolder.get(); - Long videoId = videoIdHolder.get(); - - Attachment book = entityManager.find(Attachment.class, bookId); - LOGGER.debug("Fetched book: {}", book.getName()); - assertArrayEquals(Files.readAllBytes(bookFilePath), book.getContent()); - - Attachment video = entityManager.find(Attachment.class, videoId); - LOGGER.debug("Fetched video: {}", video.getName()); - assertArrayEquals(Files.readAllBytes(videoFilePath), video.getContent()); - } catch (IOException e) { - fail(e.getMessage()); - } - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyFetchingManyToOneFindEntityTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyFetchingManyToOneFindEntityTest.java deleted file mode 100644 index fe1989549..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyFetchingManyToOneFindEntityTest.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import com.vladmihalcea.book.hpjp.hibernate.forum.*; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.LazyInitializationException; -import org.junit.Test; - -import javax.persistence.EntityGraph; -import javax.persistence.EntityManager; -import javax.persistence.EntityTransaction; -import java.util.Collections; -import java.util.List; - -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -public class LazyFetchingManyToOneFindEntityTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class, - PostDetails.class, - Tag.class - }; - } - - @Test - public void testFind() { - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle(String.format("Post nr. %d", 1)); - PostComment comment = new PostComment(); - comment.setId(1L); - comment.setPost(post); - comment.setReview("Excellent!"); - entityManager.persist(post); - entityManager.persist(comment); - }); - - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - LOGGER.info("Loaded comment entity"); - LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); - assertNotNull(comment); - }); - - doInJPA(entityManager -> { - LOGGER.info("Using custom entity graph"); - - EntityGraph postEntityGraph = entityManager.createEntityGraph( - PostComment.class); - postEntityGraph.addAttributeNodes(PostComment_.post); - - PostComment comment = entityManager.find(PostComment.class, 1L, - Collections.singletonMap("javax.persistence.fetchgraph", postEntityGraph) - ); - LOGGER.info("Fetch entity graph"); - assertNotNull(comment); - }); - - doInJPA(entityManager -> { - LOGGER.info("Using JPQL"); - - PostComment comment = entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "join fetch pc.post p " + - "where pc.id = :id", PostComment.class) - .setParameter("id", 1L) - .getSingleResult(); - assertNotNull(comment); - }); - } - - @Test - public void testNPlusOne() { - - String review = "Excellent!"; - - doInJPA(entityManager -> { - - for (long i = 1; i < 4; i++) { - Post post = new Post(); - post.setId(i); - post.setTitle(String.format("Post nr. %d", i)); - entityManager.persist(post); - - PostComment comment = new PostComment(); - comment.setId(i); - comment.setPost(post); - comment.setReview(review); - entityManager.persist(comment); - } - }); - - doInJPA(entityManager -> { - LOGGER.info("N+1 query problem"); - List comments = entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "where pc.review = :review", PostComment.class) - .setParameter("review", review) - .getResultList(); - LOGGER.info("Loaded {} comments", comments.size()); - for(PostComment comment : comments) { - LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); - } - }); - - doInJPA(entityManager -> { - LOGGER.info("N+1 query problem fixed"); - List comments = entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "join fetch pc.post p " + - "where pc.review = :review", PostComment.class) - .setParameter("review", review) - .getResultList(); - LOGGER.info("Loaded {} comments", comments.size()); - for(PostComment comment : comments) { - LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); - } - }); - } - - @Test(expected = LazyInitializationException.class) - public void testSessionIsClosed() { - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle(String.format("Post nr. %d", 1)); - PostComment comment = new PostComment(); - comment.setId(1L); - comment.setPost(post); - comment.setReview("Excellent!"); - entityManager.persist(post); - entityManager.persist(comment); - }); - - PostComment comment = null; - - EntityManager entityManager = null; - EntityTransaction transaction = null; - try { - entityManager = entityManagerFactory().createEntityManager(); - transaction = entityManager.getTransaction(); - transaction.begin(); - - comment = entityManager.find(PostComment.class, 1L); - - transaction.commit(); - } catch (Throwable e) { - if ( transaction != null && transaction.isActive()) - transaction.rollback(); - throw e; - } finally { - if (entityManager != null) { - entityManager.close(); - } - } - - LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyFetchingOneToManyFindEntityTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyFetchingOneToManyFindEntityTest.java deleted file mode 100644 index 29da275a0..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyFetchingOneToManyFindEntityTest.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class LazyFetchingOneToManyFindEntityTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class, - }; - } - - - @Override - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("high-performance-java-persistence"); - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("Excellent!"); - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Good!"); - post.addComment(comment1); - post.addComment(comment2); - entityManager.persist(post); - }); - } - - @Test - public void testFetchAndPaginate() { - doInJPA(entityManager -> { - String titlePattern = "high-performance%"; - int maxResults = 5; - List posts = entityManager.createQuery( - "select p " + - "from Post p " + - "left join fetch p.comments " + - "where p.title like :title " + - "order by p.id", Post.class) - .setParameter("title", titlePattern) - .setMaxResults(maxResults) - .getResultList(); - assertEquals(1, posts.size()); - assertEquals(2, posts.get(0).comments.size()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) - private List comments = new ArrayList<>(); - - public Post() { - } - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public PostComment() { - } - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyInitializationExceptionFixWithDTOTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyInitializationExceptionFixWithDTOTest.java deleted file mode 100644 index ca4f69004..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyInitializationExceptionFixWithDTOTest.java +++ /dev/null @@ -1,143 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public class LazyInitializationExceptionFixWithDTOTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class, - }; - } - - @Test - public void testNPlusOne() { - - String review = "Excellent!"; - - doInJPA(entityManager -> { - - for (long i = 1; i < 4; i++) { - Post post = new Post(); - post.setId(i); - post.setTitle(String.format("Post nr. %d", i)); - entityManager.persist(post); - - PostComment comment = new PostComment(); - comment.setId(i); - comment.setPost(post); - comment.setReview(review); - entityManager.persist(comment); - } - }); - - List comments = doInJPA(entityManager -> { - return entityManager.createQuery( - "select new " + - " com.vladmihalcea.book.hpjp.hibernate.fetching.PostCommentDTO(" + - " pc.id, pc.review, p.title" + - " ) " + - "from PostComment pc " + - "join pc.post p " + - "where pc.review = :review", PostCommentDTO.class) - .setParameter("review", review) - .getResultList(); - }); - - for(PostCommentDTO comment : comments) { - LOGGER.info("The post title is '{}'", comment.getTitle()); - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Post() { - } - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @NamedEntityGraph(name = "PostComment.post", attributeNodes = {}) - public static class PostComment { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public PostComment() { - } - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyInitializationExceptionTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyInitializationExceptionTest.java deleted file mode 100644 index ed8126635..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyInitializationExceptionTest.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.LazyInitializationException; -import org.junit.Test; - -import javax.persistence.*; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class LazyInitializationExceptionTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class, - }; - } - - @Test - public void testNPlusOne() { - - String review = "Excellent!"; - - doInJPA(entityManager -> { - - for (long i = 1; i < 4; i++) { - Post post = new Post(); - post.setId(i); - post.setTitle(String.format("Post nr. %d", i)); - entityManager.persist(post); - - PostComment comment = new PostComment(); - comment.setId(i); - comment.setPost(post); - comment.setReview(review); - entityManager.persist(comment); - } - }); - - List comments = null; - - EntityManager entityManager = null; - EntityTransaction transaction = null; - try { - entityManager = entityManagerFactory().createEntityManager(); - transaction = entityManager.getTransaction(); - transaction.begin(); - - comments = entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "where pc.review = :review", PostComment.class) - .setParameter("review", review) - .getResultList(); - - transaction.commit(); - } catch (Throwable e) { - if ( transaction != null && transaction.isActive()) - transaction.rollback(); - throw e; - } finally { - if (entityManager != null) { - entityManager.close(); - } - } - try { - for(PostComment comment : comments) { - LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); - } - } catch (LazyInitializationException expected) { - assertEquals("could not initialize proxy - no Session", expected.getMessage()); - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Post() { - } - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @NamedEntityGraph(name = "PostComment.post", attributeNodes = {}) - public static class PostComment { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public PostComment() { - } - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NPlusOneLazyFetchingManyToOneFindEntityTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NPlusOneLazyFetchingManyToOneFindEntityTest.java deleted file mode 100644 index ec4eb6fd4..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NPlusOneLazyFetchingManyToOneFindEntityTest.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public class NPlusOneLazyFetchingManyToOneFindEntityTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class - }; - } - - @Test - public void testNPlusOne() { - - String review = "Excellent!"; - - doInJPA(entityManager -> { - - for (long i = 1; i < 4; i++) { - Post post = new Post(); - post.setId(i); - post.setTitle(String.format("Post nr. %d", i)); - entityManager.persist(post); - - PostComment comment = new PostComment(); - comment.setId(i); - comment.setPost(post); - comment.setReview(review); - entityManager.persist(comment); - } - }); - - doInJPA(entityManager -> { - LOGGER.info("N+1 query problem"); - List comments = entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "where pc.review = :review", PostComment.class) - .setParameter("review", review) - .getResultList(); - LOGGER.info("Loaded {} comments", comments.size()); - for(PostComment comment : comments) { - LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); - } - }); - - doInJPA(entityManager -> { - LOGGER.info("N+1 query problem fixed"); - List comments = entityManager.createQuery( - "select pc " + - "from PostComment pc " + - "join fetch pc.post p " + - "where pc.review = :review", PostComment.class) - .setParameter("review", review) - .getResultList(); - LOGGER.info("Loaded {} comments", comments.size()); - for(PostComment comment : comments) { - LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); - } - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Post() { - } - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @NamedEntityGraph(name = "PostComment.post", attributeNodes = {}) - public static class PostComment { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public PostComment() { - } - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NamedQueryPerformanceTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NamedQueryPerformanceTest.java deleted file mode 100644 index 26b8734dc..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NamedQueryPerformanceTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import org.hibernate.Session; - -import javax.persistence.EntityManager; - -/** - * @author Vlad Mihalcea - */ -public class NamedQueryPerformanceTest extends PlanCacheSizePerformanceTest { - - public static final String QUERY_NAME_1 = "findPostCommentSummary"; - public static final String QUERY_NAME_2 = "findPostComments"; - - public NamedQueryPerformanceTest(int planCacheMaxSize) { - super(planCacheMaxSize); - } - - @Override - public void init() { - super.init(); - doInJPA(entityManager -> { - entityManagerFactory().addNamedQuery(QUERY_NAME_1, createEntityQuery1(entityManager)); - entityManagerFactory().addNamedQuery(QUERY_NAME_2, createEntityQuery2(entityManager)); - }); - } - - @Override - protected Object getEntityQuery1(EntityManager entityManager) { - Session session = entityManager.unwrap(Session.class); - return session.getNamedQuery(QUERY_NAME_1); - } - - @Override - protected Object getEntityQuery2(EntityManager entityManager) { - Session session = entityManager.unwrap(Session.class); - return session.getNamedQuery(QUERY_NAME_2); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NaturalIdTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NaturalIdTest.java deleted file mode 100644 index b5493b226..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NaturalIdTest.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.Session; -import org.hibernate.annotations.NaturalId; -import org.junit.Test; - -import javax.persistence.*; -import java.util.List; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -public class NaturalIdTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class - }; - } - - - @Override - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setTitle(String.format("Post nr. %d", 1)); - post.setSlug("high-performance-java-persistence"); - entityManager.persist(post); - }); - } - - @Test - public void testFindByNaturalId() { - doInJPA(entityManager -> { - String slug = "high-performance-java-persistence"; - Session session = entityManager.unwrap(Session.class); - Post post = session.bySimpleNaturalId(Post.class).load(slug); - assertNotNull(post); - }); - } - - @Test - public void testFindWithQuery() { - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select p " + - "from Post p " + - "where p.slug is not null", Post.class) - .getResultList(); - assertFalse(posts.isEmpty()); - }); - } - - @Test - public void testGetReferenceByNaturalId() { - doInJPA(entityManager -> { - String slug = "high-performance-java-persistence"; - Session session = entityManager.unwrap(Session.class); - LOGGER.info("Loading a post by natural identifier"); - Post post = session.bySimpleNaturalId(Post.class).getReference(slug); - LOGGER.info("Proxy is loaded"); - LOGGER.info("Post title is {}", post.getTitle()); - assertNotNull(post); - }); - } - - @Entity(name = "Post") @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - @NaturalId - @Column(nullable = false, unique = true) - private String slug; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getSlug() { - return slug; - } - - public void setSlug(String slug) { - this.slug = slug; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/PlanCacheSizePerformanceTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/PlanCacheSizePerformanceTest.java deleted file mode 100644 index aca6a2883..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/PlanCacheSizePerformanceTest.java +++ /dev/null @@ -1,398 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Slf4jReporter; -import com.codahale.metrics.Timer; -import com.codahale.metrics.UniformReservoir; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.SQLQuery; -import org.hibernate.jpa.QueryHints; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import javax.persistence.*; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.stream.LongStream; - -/** - * @author Vlad Mihalcea - */ -@RunWith(Parameterized.class) -public class PlanCacheSizePerformanceTest extends AbstractTest { - - private MetricRegistry metricRegistry = new MetricRegistry(); - - private Timer timer = new Timer(new UniformReservoir(10000)); - - private Slf4jReporter logReporter = Slf4jReporter - .forRegistry(metricRegistry) - .outputTo(LOGGER) - .convertDurationsTo(TimeUnit.MICROSECONDS) - .build(); - - private final int planCacheMaxSize; - - public PlanCacheSizePerformanceTest(int planCacheMaxSize) { - this.planCacheMaxSize = planCacheMaxSize; - } - - @Parameterized.Parameters - public static Collection rdbmsDataSourceProvider() { - List planCacheMaxSizes = new ArrayList<>(); - planCacheMaxSizes.add(new Integer[] {1}); - planCacheMaxSizes.add(new Integer[] {100}); - return planCacheMaxSizes; - } - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostDetails.class, - PostComment.class, - Tag.class - }; - } - - - @Override - public void init() { - metricRegistry.register(getClass().getSimpleName(), timer); - super.init(); - int commentsSize = 5; - doInJPA(entityManager -> { - LongStream.range(0, 50).forEach(i -> { - Post post = new Post(i); - post.setTitle(String.format("Post nr. %d", i)); - - LongStream.range(0, commentsSize).forEach(j -> { - PostComment comment = new PostComment(); - comment.setId((i * commentsSize) + j); - comment.setReview(String.format("Good review nr. %d", comment.getId())); - post.addComment(comment); - - }); - entityManager.persist(post); - }); - }); - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.query.plan_cache_max_size", planCacheMaxSize); - properties.put("hibernate.query.plan_parameter_metadata_max_size", planCacheMaxSize); - return properties; - } - - @Test - public void testEntityQueries() { - //warming up - LOGGER.info("Warming up"); - doInJPA(entityManager -> { - for (int i = 0; i < 10000; i++) { - getEntityQuery1(entityManager); - getEntityQuery2(entityManager); - } - }); - LOGGER.info("Create entity queries for plan cache size {}", planCacheMaxSize); - int iterations = 2500; - doInJPA(entityManager -> { - for (int i = 0; i < iterations; i++) { - long startNanos = System.nanoTime(); - getEntityQuery1(entityManager); - timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - startNanos = System.nanoTime(); - getEntityQuery2(entityManager); - timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - } - }); - logReporter.report(); - } - - @Test - public void testNativeQueries() { - //warming up - LOGGER.info("Warming up"); - doInJPA(entityManager -> { - for (int i = 0; i < 10000; i++) { - getNativeQuery1(entityManager); - getNativeQuery2(entityManager); - } - }); - LOGGER.info("Create native queries for plan cache size {}", planCacheMaxSize); - int iterations = 2500; - doInJPA(entityManager -> { - for (int i = 0; i < iterations; i++) { - long startNanos = System.nanoTime(); - getNativeQuery1(entityManager); - timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - startNanos = System.nanoTime(); - getNativeQuery2(entityManager); - timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - } - }); - logReporter.report(); - } - - protected Object getEntityQuery1(EntityManager entityManager) { - return createEntityQuery1(entityManager); - } - - protected Object getEntityQuery2(EntityManager entityManager) { - return createEntityQuery2(entityManager); - } - - protected Object getNativeQuery1(EntityManager entityManager) { - return createNativeQuery1(entityManager); - } - - protected Object getNativeQuery2(EntityManager entityManager) { - return createNativeQuery2(entityManager); - } - - protected Query createEntityQuery1(EntityManager entityManager) { - return entityManager.createQuery( - "select new " + - " com.vladmihalcea.book.hpjp.hibernate.fetching.PostCommentSummary( " + - " p.id, p.title, c.review ) " + - "from PostComment c " + - "join c.post p") - .setFirstResult(10) - .setMaxResults(20) - .setHint(QueryHints.HINT_FETCH_SIZE, 20); - } - - protected Query createEntityQuery2(EntityManager entityManager) { - return entityManager.createQuery( - "select c " + - "from PostComment c " + - "join fetch c.post p " + - "where p.title like :title"); - } - - protected Query createNativeQuery1(EntityManager entityManager) { - return entityManager.createNativeQuery( - "select p.id, p.title, c.review * " + - "from post_comment c " + - "join post p on p.id = c.post_id ") - .setFirstResult(10) - .setMaxResults(20) - .setHint(QueryHints.HINT_FETCH_SIZE, 20); - } - - protected org.hibernate.Query createNativeQuery2(EntityManager entityManager) { - return entityManager.createNativeQuery( - "select c.*, p.* " + - "from post_comment c " + - "join post p on p.id = c.post_id " + - "where p.title like :title") - .unwrap(SQLQuery.class) - .addEntity(PostComment.class) - .addEntity(Post.class); - } - - @Entity(name = "Post") - @Table(name = "post") - @NamedNativeQuery( - name = "findPostCommentsByPostTitle", - query = "select c.review " + - "from post_comment c " + - "where c.id > :id " - ) - public static class Post { - - @Id - private Long id; - - private String title; - - public Post() { - } - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true, fetch = FetchType.LAZY) - private PostDetails details; - - @ManyToMany - @JoinTable(name = "post_tag", - joinColumns = @JoinColumn(name = "post_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id") - ) - private List tags = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public PostDetails getDetails() { - return details; - } - - public List getTags() { - return tags; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - - public void addDetails(PostDetails details) { - this.details = details; - details.setPost(this); - } - - public void removeDetails() { - this.details.setPost(null); - this.details = null; - } - } - - @Entity(name = "PostDetails") - @Table(name = "post_details") - public static class PostDetails { - - @Id - private Long id; - - @Column(name = "created_on") - private Date createdOn; - - @Column(name = "created_by") - private String createdBy; - - public PostDetails() { - createdOn = new Date(); - } - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") - @MapsId - private Post post; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public PostComment() { - } - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } - - @Entity(name = "Tag") - @Table(name = "tag") - public static class Tag { - - @Id - private Long id; - - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/PostCommentDTO.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/PostCommentDTO.java deleted file mode 100644 index 4c06ef9ba..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/PostCommentDTO.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -/** - * @author Vlad Mihalcea - */ -public class PostCommentDTO { - - private final Long id; - - private final String review; - - private final String title; - - public PostCommentDTO(Long id, String review, String title) { - this.id = id; - this.review = review; - this.title = title; - } - - public Long getId() { - return id; - } - - public String getReview() { - return review; - } - - public String getTitle() { - return title; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/PostCommentSummary.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/PostCommentSummary.java deleted file mode 100644 index 83c4b338a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/PostCommentSummary.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; - -/** - * @author Vlad Mihalcea - */ -public class PostCommentSummary { - - private Number id; - private String title; - private String review; - - public PostCommentSummary(Number id, String title, String review) { - this.id = id; - this.title = title; - this.review = review; - } - - public PostCommentSummary() {} - - public Number getId() { - return id; - } - - public String getTitle() { - return title; - } - - public String getReview() { - return review; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/file/PostgreSQLQueryToFileTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/file/PostgreSQLQueryToFileTest.java deleted file mode 100644 index 7079cce26..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/file/PostgreSQLQueryToFileTest.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching.file; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.junit.Test; - -import javax.persistence.*; -import java.net.URISyntaxException; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * @author Vlad Mihalcea - */ -public class PostgreSQLQueryToFileTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Properties properties() { - Properties properties = super.properties(); - //properties.setProperty(AvailableSettings.USE_STREAMS_FOR_BINARY, Boolean.FALSE.toString()); - return properties; - } - - @Override - protected Class[] entities() { - return new Class[]{ - Post.class, - PostComment.class, - }; - } - - - @Override - public void init() { - super.init(); - doInJPA(entityManager -> { - for (long id = 1; id <= 100; id++) { - Post post = new Post(); - post.setId(id); - post.setTitle("High-Performance Java Persistence"); - - post.addComment(new PostComment("Excellent!")); - post.addComment(new PostComment("Great!")); - - entityManager.persist(post); - } - }); - } - - @Test - public void testCopy() throws URISyntaxException { - doInJPA(entityManager -> { - entityManager.createNativeQuery( - String.format( - "copy (" + - " select * " + - " from post p " + - " inner join post_comment pc on pc.post_id = p.id " + - ") " + - "to '%s' " + - "with CSV DELIMITER ','", - Paths.get(System.getenv("PGSQL_DATA"), "post_and_comments.csv").toString() - ) - ) - .executeUpdate(); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - - @Override - public String toString() { - return "Post{" + - "id=" + id + - ", title='" + title + '\'' + - '}'; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/AlwaysFlushTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/AlwaysFlushTest.java deleted file mode 100644 index 7b7613956..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/AlwaysFlushTest.java +++ /dev/null @@ -1,232 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.FlushMode; -import org.hibernate.Session; -import org.hibernate.transform.Transformers; -import org.jboss.logging.Logger; -import org.junit.Test; - -import javax.persistence.*; -import java.math.BigInteger; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class AlwaysFlushTest extends AbstractTest { - - private static final Logger log = Logger.getLogger(AlwaysFlushTest.class); - - @Override - protected Class[] entities() { - return new Class[] { - Board.class, - Post.class, - }; - } - - @Test - public void testFlushSQL() { - doInJPA(entityManager -> { - entityManager.createNativeQuery("delete from Post").executeUpdate(); - entityManager.createNativeQuery("delete from Board").executeUpdate(); - }); - doInJPA(entityManager -> { - log.info("testFlushSQL"); - - Board board1 = new Board(); - board1.setName("JPA"); - Board board2 = new Board(); - board2.setName("Hibernate"); - - entityManager.persist(board1); - entityManager.persist(board2); - - Post post1 = new Post("JPA 1"); - post1.setVersion(1); - post1.setBoard(board1); - entityManager.persist(post1); - - Post post2 = new Post("Hibernate 1"); - post2.setVersion(2); - post2.setBoard(board2); - entityManager.persist(post2); - - Post post3 = new Post("Hibernate 3"); - post3.setVersion(1); - post3.setBoard(board2); - entityManager.persist(post3); - - Session session = entityManager.unwrap(Session.class); - List result = session.createSQLQuery( - "SELECT " + - " b.name as forum, " + - " COUNT (p) as count " + - "FROM post p " + - "JOIN board b on b.id = p.board_id " + - "GROUP BY forum") - .setFlushMode(FlushMode.ALWAYS) - .setResultTransformer( Transformers.aliasToBean(ForumCount.class)) - .list(); - - assertEquals(result.size(), 2); - }); - } - - @Test - public void testSynchronizeSQL() { - doInJPA(entityManager -> { - entityManager.createNativeQuery("delete from Post").executeUpdate(); - entityManager.createNativeQuery("delete from Board").executeUpdate(); - }); - doInJPA(entityManager -> { - log.info("testFlushSQL"); - - Board board1 = new Board(); - board1.setName("JPA"); - Board board2 = new Board(); - board2.setName("Hibernate"); - - entityManager.persist(board1); - entityManager.persist(board2); - - Post post1 = new Post("JPA 1"); - post1.setVersion(1); - post1.setBoard(board1); - entityManager.persist(post1); - - Post post2 = new Post("Hibernate 1"); - post2.setVersion(2); - post2.setBoard(board2); - entityManager.persist(post2); - - Post post3 = new Post("Hibernate 3"); - post3.setVersion(1); - post3.setBoard(board2); - entityManager.persist(post3); - - Session session = entityManager.unwrap(Session.class); - List result = session.createSQLQuery( - "SELECT " + - " b.name as forum, " + - " COUNT (p) as count " + - "FROM post p " + - "JOIN board b on b.id = p.board_id " + - "GROUP BY forum") - .addSynchronizedEntityClass(Board.class) - .addSynchronizedEntityClass(Post.class) - .setResultTransformer( Transformers.aliasToBean(ForumCount.class)) - .list(); - - assertEquals(result.size(), 2); - }); - } - - public static class ForumCount { - - private String forum; - - private BigInteger count; - - public String getForum() { - return forum; - } - - public void setForum(String forum) { - this.forum = forum; - } - - public BigInteger getCount() { - return count; - } - - public void setCount(BigInteger count) { - this.count = count; - } - } - - @Entity(name = "Board") - @Table(name = "board") - public static class Board { - - @Id - @GeneratedValue - private Long id; - - private String name; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - @ManyToOne - private Board board; - - @Version - private int version; - - public Post() {} - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public Board getBoard() { - return board; - } - - public void setBoard(Board board) { - this.board = board; - } - - public int getVersion() { - return version; - } - - public void setVersion(int version) { - this.version = version; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/BatchProcessingTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/BatchProcessingTest.java deleted file mode 100644 index 7670834ff..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/BatchProcessingTest.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; - -import java.util.Properties; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; - -/** - * @author Vlad Mihalcea - */ -public class BatchProcessingTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.jdbc.batch_size", "25"); - properties.put("hibernate.order_inserts", "true"); - properties.put("hibernate.order_updates", "true"); - properties.put("hibernate.jdbc.batch_versioned_data", "true"); - return properties; - } - - @Test - public void testBatchProcessing() { - int entityCount = 50; - int batchSize = 25; - - EntityManager entityManager = entityManagerFactory().createEntityManager();; - - try { - entityManager.getTransaction().begin(); - - for ( int i = 0; i < entityCount; ++i ) { - if ( i > 0 && i % batchSize == 0 ) { - flush( entityManager ); - } - - Post post = new Post( String.format( "Post %d", i + 1 ) ); - entityManager.persist( post ); - } - - entityManager.getTransaction().commit(); - } catch (RuntimeException e) { - if ( entityManager.getTransaction().isActive()) { - entityManager.getTransaction().rollback(); - } - throw e; - } finally { - entityManager.close(); - } - } - - private void flush(EntityManager entityManager) { - entityManager.flush(); - entityManager.clear(); - - entityManager.getTransaction().commit(); - entityManager.getTransaction().begin(); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - - public Post() { - } - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/HibernateAutoFlushTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/HibernateAutoFlushTest.java deleted file mode 100644 index d1f8d5df7..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/HibernateAutoFlushTest.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import org.jboss.logging.Logger; -import org.junit.Test; - -import javax.persistence.EntityManager; -import javax.persistence.EntityTransaction; - -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.Post; -import static org.junit.Assert.assertTrue; - -/** - * @author Vlad Mihalcea - */ -public class HibernateAutoFlushTest extends AbstractTest { - - private static final Logger log = Logger.getLogger(HibernateAutoFlushTest.class); - - private BlogEntityProvider entityProvider = new BlogEntityProvider(); - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Override - protected boolean nativeHibernateSessionFactoryBootstrap() { - return true; - } - - @Test - public void testFlushAutoCommit() { - EntityManager entityManager = null; - EntityTransaction txn = null; - try { - entityManager = entityManagerFactory().createEntityManager(); - txn = entityManager.getTransaction(); - txn.begin(); - - Post post = new Post("Hibernate"); - post.setId(1L); - entityManager.persist(post); - log.info("Entity is in persisted state"); - - txn.commit(); - } catch (RuntimeException e) { - if (txn != null && txn.isActive()) txn.rollback(); - throw e; - } finally { - if (entityManager != null) { - entityManager.close(); - } - } - } - - @Test - public void testFlushAutoJPQL() { - doInJPA(entityManager -> { - log.info("testFlushAutoJPQL"); - Post post = new Post("Hibernate"); - post.setId(1L); - entityManager.persist(post); - entityManager.createQuery("select p from Tag p").getResultList(); - entityManager.createQuery("select p from Post p").getResultList(); - }); - } - - @Test - public void testFlushAutoJPQLOverlap() { - doInJPA(entityManager -> { - log.info("testFlushAutoJPQL"); - Post post = new Post("Hibernate"); - post.setId(1L); - entityManager.persist(post); - entityManager.createQuery("select p from PostDetails p").getResultList(); - entityManager.createQuery("select p from Post p").getResultList(); - }); - } - - @Test - public void testFlushAutoSQL() { - doInJPA(entityManager -> { - entityManager.createNativeQuery("delete from Post").executeUpdate(); - }); - doInJPA(entityManager -> { - log.info("testFlushAutoSQL"); - - assertTrue(((Number) entityManager - .createNativeQuery("select count(*) from Post") - .getSingleResult()).intValue() == 0); - - Post post = new Post("Hibernate"); - post.setId(1L); - entityManager.persist(post); - - int count = ((Number) entityManager - .createNativeQuery("select count(*) from Post") - .getSingleResult()).intValue(); - - assertTrue( count == 0 ); - }); - } - - @Test - public void testFlushAutoSQLNativeSessionWithoutSynchronization() { - doInHibernate(session -> { - log.info("testFlushAutoSQLNativeSession"); - - assertTrue(((Number) session - .createQuery("select count(*) from Post") - .getSingleResult()).intValue() == 0); - - Post post = new Post("Hibernate"); - post.setId(1L); - session.persist(post); - - int count = ((Number) session - .createNativeQuery("select count(*) from Post") - .uniqueResult()).intValue(); - - assertTrue( count == 0 ); - }); - } - - @Test - public void testFlushAutoSQLNativeSessionWithSynchronization() { - doInHibernate(session -> { - log.info("testFlushAutoSQLNativeSession"); - - assertTrue(((Number) session - .createQuery("select count(*) from Post") - .getSingleResult()).intValue() == 0); - - Post post = new Post("Hibernate"); - post.setId(1L); - session.persist(post); - - int count = ((Number) session - .createNativeQuery("select count(*) from Post") - .addSynchronizedEntityClass(Post.class) - .uniqueResult()).intValue(); - - assertTrue( count == 1 ); - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/IdentityOrderTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/IdentityOrderTest.java deleted file mode 100644 index 7df8d7e3d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/IdentityOrderTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; - -/** - * @author Vlad Mihalcea - */ -public class IdentityOrderTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - private Long postId; - - - @Test - public void tesOperationOrder() { - - EntityManager entityManager = null; - EntityTransaction txn = null; - try { - entityManager = entityManagerFactory().createEntityManager(); - Post post = new Post(); - post.setTitle("High-Performance Java Persistence"); - post.setSlug("high-performance-java-persistence"); - - entityManager.persist(post); - entityManager.getTransaction().begin(); - entityManager.flush(); - entityManager.getTransaction().commit(); - } catch (Throwable e) { - if ( txn != null && txn.isActive()) txn.rollback(); - throw e; - } finally { - if (entityManager != null) { - entityManager.close(); - } - } - } - - @Test - public void tesOperationOrderWithManualFlush() { - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, postId); - entityManager.remove(post); - entityManager.flush(); - - Post newPost = new Post(); - newPost.setTitle("High-Performance Java Persistence Book"); - newPost.setSlug("high-performance-java-persistence"); - entityManager.persist(newPost); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String title; - - private String slug; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getSlug() { - return slug; - } - - public void setSlug(String slug) { - this.slug = slug; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/JpaAutoFlushTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/JpaAutoFlushTest.java deleted file mode 100644 index 45af75acc..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/JpaAutoFlushTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; - -import javax.persistence.EntityManager; -import javax.persistence.EntityTransaction; - -import org.junit.Test; - -import org.jboss.logging.Logger; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; - -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.Post; -import static org.junit.Assert.assertTrue; - -/** - * @author Vlad Mihalcea - */ -public class JpaAutoFlushTest extends AbstractTest { - - private static final Logger log = Logger.getLogger(JpaAutoFlushTest.class); - - private BlogEntityProvider entityProvider = new BlogEntityProvider(); - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Override - protected boolean nativeHibernateSessionFactoryBootstrap() { - return false; - } - - @Test - public void testFlushAutoCommit() { - EntityManager entityManager = null; - EntityTransaction txn = null; - try { - entityManager = entityManagerFactory().createEntityManager(); - txn = entityManager.getTransaction(); - txn.begin(); - - Post post = new Post("Hibernate"); - post.setId(1L); - entityManager.persist(post); - log.info("Entity is in persisted state"); - - txn.commit(); - } catch (RuntimeException e) { - if (txn != null && txn.isActive()) txn.rollback(); - throw e; - } finally { - if (entityManager != null) { - entityManager.close(); - } - } - } - - @Test - public void testFlushAutoJPQL() { - doInJPA(entityManager -> { - log.info("testFlushAutoJPQL"); - Post post = new Post("Hibernate"); - post.setId(1L); - entityManager.persist(post); - entityManager.createQuery("select p from Tag p").getResultList(); - entityManager.createQuery("select p from Post p").getResultList(); - }); - } - - @Test - public void testFlushAutoJPQLOverlap() { - doInJPA(entityManager -> { - log.info("testFlushAutoJPQL"); - Post post = new Post("Hibernate"); - post.setId(1L); - entityManager.persist(post); - entityManager.createQuery("select p from PostDetails p").getResultList(); - entityManager.createQuery("select p from Post p").getResultList(); - }); - } - - @Test - public void testFlushAutoSQL() { - doInJPA(entityManager -> { - entityManager.createNativeQuery("delete from Post").executeUpdate(); - }); - doInJPA(entityManager -> { - log.info("testFlushAutoSQL"); - - assertTrue(((Number) entityManager - .createNativeQuery("select count(*) from Post") - .getSingleResult()).intValue() == 0); - - Post post = new Post("Hibernate"); - post.setId(1L); - entityManager.persist(post); - - int count = ((Number) entityManager - .createNativeQuery("select count(*) from Post") - .getSingleResult()).intValue(); - - assertTrue( count == 1 ); - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/OrderTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/OrderTest.java deleted file mode 100644 index e7a34c79c..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/OrderTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.NaturalId; -import org.junit.Test; - -import javax.persistence.*; - -/** - * @author Vlad Mihalcea - */ -public class OrderTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - private Long postId; - - @Override - public void init() { - super.init(); - postId = doInJPA(entityManager -> { - Post post = new Post(); - post.setTitle("High-Performance Java Persistence"); - post.setSlug("high-performance-java-persistence"); - - entityManager.persist(post); - entityManager.flush(); - return post.getId(); - }); - } - - @Test - public void tesOperationOrder() { - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, postId); - entityManager.remove(post); - - Post newPost = new Post(); - newPost.setTitle("High-Performance Java Persistence Book"); - newPost.setSlug("high-performance-java-persistence"); - entityManager.persist(newPost); - }); - } - - @Test - public void tesOperationOrderWithManualFlush() { - doInJPA(entityManager -> { - Post post = entityManager.find(Post.class, postId); - entityManager.remove(post); - entityManager.flush(); - - Post newPost = new Post(); - newPost.setTitle("High-Performance Java Persistence Book"); - newPost.setSlug("high-performance-java-persistence"); - entityManager.persist(newPost); - }); - } - - @Entity(name = "Post") - @Table(name = "post", - uniqueConstraints = @UniqueConstraint(name = "slug_uq", columnNames = "slug")) - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - @NaturalId - private String slug; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getSlug() { - return slug; - } - - public void setSlug(String slug) { - this.slug = slug; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/ReadOnlyQueryTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/ReadOnlyQueryTest.java deleted file mode 100644 index c06c5303f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/ReadOnlyQueryTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.Session; -import org.hibernate.jpa.QueryHints; -import org.junit.Test; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Table; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public class ReadOnlyQueryTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Override - public void init() { - super.init(); - doInJPA(entityManager -> { - Post post = new Post(); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - }); - } - - @Test - public void testReadOnly() { - doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select p from Post p", Post.class) - .setHint(QueryHints.HINT_READONLY, true) - .getResultList(); - }); - } - - @Test - public void testDefaultReadOnly() { - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - boolean isDefaultReadOnly = session.isDefaultReadOnly(); - session.setDefaultReadOnly(true); - List posts = entityManager.createQuery( - "select p from Post p", Post.class) - .getResultList(); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/SessionAlwaysFlushTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/SessionAlwaysFlushTest.java deleted file mode 100644 index a809c935d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/SessionAlwaysFlushTest.java +++ /dev/null @@ -1,236 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.FlushMode; -import org.hibernate.Session; -import org.hibernate.transform.Transformers; -import org.jboss.logging.Logger; -import org.junit.Test; - -import javax.persistence.*; -import java.math.BigInteger; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class SessionAlwaysFlushTest extends AbstractPostgreSQLIntegrationTest { - - private static final Logger log = Logger.getLogger(AlwaysFlushTest.class); - - @Override - protected Class[] entities() { - return new Class[] { - Board.class, - Post.class, - }; - } - - @Override - protected boolean nativeHibernateSessionFactoryBootstrap() { - return true; - } - - @Test - public void testFlushSQL() { - doInJPA(entityManager -> { - entityManager.createNativeQuery("delete from Post").executeUpdate(); - entityManager.createNativeQuery("delete from Board").executeUpdate(); - }); - doInJPA(entityManager -> { - log.info("testFlushSQL"); - - Board board1 = new Board(); - board1.setName("JPA"); - Board board2 = new Board(); - board2.setName("Hibernate"); - - entityManager.persist(board1); - entityManager.persist(board2); - - Post post1 = new Post("JPA 1"); - post1.setVersion(1); - post1.setBoard(board1); - entityManager.persist(post1); - - Post post2 = new Post("Hibernate 1"); - post2.setVersion(2); - post2.setBoard(board2); - entityManager.persist(post2); - - Post post3 = new Post("Hibernate 3"); - post3.setVersion(1); - post3.setBoard(board2); - entityManager.persist(post3); - - Session session = entityManager.unwrap(Session.class); - List result = session.createSQLQuery( - "SELECT " + - " b.name as forum, " + - " COUNT (p) as count " + - "FROM post p " + - "JOIN board b on b.id = p.board_id " + - "GROUP BY forum") - .setFlushMode(FlushMode.ALWAYS) - .setResultTransformer( Transformers.aliasToBean(ForumCount.class)) - .list(); - - assertEquals(result.size(), 2); - }); - } - - @Test - public void testSynchronizeSQL() { - doInHibernate(session -> { - session.createNativeQuery("delete from Post").executeUpdate(); - session.createNativeQuery("delete from Board").executeUpdate(); - }); - doInHibernate(session -> { - log.info("testFlushSQL"); - - Board board1 = new Board(); - board1.setName("JPA"); - Board board2 = new Board(); - board2.setName("Hibernate"); - - session.persist(board1); - session.persist(board2); - - Post post1 = new Post("JPA 1"); - post1.setVersion(1); - post1.setBoard(board1); - session.persist(post1); - - Post post2 = new Post("Hibernate 1"); - post2.setVersion(2); - post2.setBoard(board2); - session.persist(post2); - - Post post3 = new Post("Hibernate 3"); - post3.setVersion(1); - post3.setBoard(board2); - session.persist(post3); - - List result = session.createSQLQuery( - "SELECT " + - " b.name as forum, " + - " COUNT (p) as count " + - "FROM post p " + - "JOIN board b on b.id = p.board_id " + - "GROUP BY forum") - //.addSynchronizedEntityClass(Board.class) - //.addSynchronizedEntityClass(Post.class) - .setResultTransformer( Transformers.aliasToBean(ForumCount.class)) - .list(); - - assertEquals(result.size(), 2); - }); - } - - public static class ForumCount { - - private String forum; - - private BigInteger count; - - public String getForum() { - return forum; - } - - public void setForum(String forum) { - this.forum = forum; - } - - public BigInteger getCount() { - return count; - } - - public void setCount(BigInteger count) { - this.count = count; - } - } - - @Entity(name = "Board") - @Table(name = "board") - public static class Board { - - @Id - @GeneratedValue - private Long id; - - private String name; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - @ManyToOne - private Board board; - - @Version - private int version; - - public Post() {} - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public Board getBoard() { - return board; - } - - public void setBoard(Board board) { - this.board = board; - } - - public int getVersion() { - return version; - } - - public void setVersion(int version) { - this.version = version; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AbstractPooledSequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AbstractPooledSequenceIdentifierTest.java deleted file mode 100644 index 1a43eb1b5..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AbstractPooledSequenceIdentifierTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.Session; - -import java.sql.Statement; -import java.util.List; -import java.util.Properties; - -import static org.junit.Assert.assertEquals; - -public abstract class AbstractPooledSequenceIdentifierTest extends AbstractTest { - - protected abstract Object newEntityInstance(); - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.id.new_generator_mappings", "true"); - return properties; - } - - protected void insertSequences() { - LOGGER.debug("testSequenceIdentifierGenerator"); - doInJPA(entityManager -> { - for (int i = 0; i < 5; i++) { - entityManager.persist(newEntityInstance()); - entityManager.flush(); - } - entityManager.flush(); - assertEquals(5, ((Number) entityManager.createNativeQuery("SELECT COUNT(*) FROM Post").getSingleResult()).intValue()); - - entityManager.unwrap(Session.class).doWork(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate("INSERT INTO Post VALUES NEXT VALUE FOR sequence"); - } - }); - - assertEquals(6, ((Number) entityManager.createNativeQuery("SELECT COUNT(*) FROM Post").getSingleResult()).intValue()); - List ids = entityManager.createNativeQuery("SELECT id FROM Post").getResultList(); - for (Number id : ids) { - LOGGER.debug("Found id: {}", id); - } - for (int i = 0; i < 3; i++) { - entityManager.persist(newEntityInstance()); - entityManager.flush(); - } - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AssignedIdentityGenerator.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AssignedIdentityGenerator.java deleted file mode 100644 index 518455c9d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AssignedIdentityGenerator.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.id.IdentityGenerator; - -import java.io.Serializable; - -/** - * AssignedIdentityGenerator - Assigned IdentityGenerator - * - * @author Vlad Mihalcea - */ -public class AssignedIdentityGenerator extends IdentityGenerator { - - @Override - public Serializable generate(SharedSessionContractImplementor session, Object obj) { - if(obj instanceof Identifiable) { - Identifiable identifiable = (Identifiable) obj; - Serializable id = identifiable.getId(); - if(id != null) { - return id; - } - } - return super.generate(session, obj); - } - - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AssignedIdentityGeneratorTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AssignedIdentityGeneratorTest.java deleted file mode 100644 index 830b6c7b3..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AssignedIdentityGeneratorTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.Session; -import org.hibernate.annotations.GenericGenerator; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.Statement; - -public class AssignedIdentityGeneratorTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - @Test - public void test() { - LOGGER.debug("test"); - - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - session.doWork(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate("ALTER TABLE post ALTER COLUMN id bigint generated by default as identity (start with 1)"); - } - }); - }); - - doInJPA(entityManager -> { - entityManager.persist(new Post()); - entityManager.persist(new Post(-1L)); - entityManager.persist(new Post()); - entityManager.persist(new Post(-2L)); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post implements Identifiable { - - @Id - @GenericGenerator( - name = "assigned-identity", - strategy = "com.vladmihalcea.book.hpjp.hibernate.identifier.AssignedIdentityGenerator" - ) - @GeneratedValue(generator = "assigned-identity", strategy = GenerationType.IDENTITY) - private Long id; - - @Version - private Integer version; - - public Post() { - } - - public Post(Long id) { - this.id = id; - } - - @Override - public Long getId() { - return id; - } - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AssignedSequenceStyleGenerator.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AssignedSequenceStyleGenerator.java deleted file mode 100644 index 3a7759b22..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AssignedSequenceStyleGenerator.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.id.enhanced.SequenceStyleGenerator; - -import java.io.Serializable; - -/** - * AssignedSequenceStyleGenerator - Assigned SequenceStyleGenerator - * - * @author Vlad Mihalcea - */ -public class AssignedSequenceStyleGenerator extends SequenceStyleGenerator { - - @Override - public Serializable generate(SharedSessionContractImplementor session, Object obj) { - if(obj instanceof Identifiable) { - Identifiable identifiable = (Identifiable) obj; - Serializable id = identifiable.getId(); - if(id != null) { - return id; - } - } - return super.generate(session, obj); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AssignedSequenceStyleGeneratorTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AssignedSequenceStyleGeneratorTest.java deleted file mode 100644 index 7d69fe20e..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AssignedSequenceStyleGeneratorTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.GenericGenerator; -import org.junit.Test; - -import javax.persistence.*; - -public class AssignedSequenceStyleGeneratorTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - @Test - public void test() { - LOGGER.debug("test"); - doInJPA(entityManager -> { - entityManager.persist(new Post()); - entityManager.persist(new Post(-1L)); - entityManager.persist(new Post()); - entityManager.persist(new Post(-2L)); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post implements Identifiable { - - @Id - @GenericGenerator( - name = "assigned-sequence", - strategy = "com.vladmihalcea.book.hpjp.hibernate.identifier.AssignedSequenceStyleGenerator", - parameters = @org.hibernate.annotations.Parameter(name = "sequence_name", value = "post_sequence") - ) - @GeneratedValue(generator = "assigned-sequence", strategy = GenerationType.SEQUENCE) - private Long id; - - @Version - private Integer version; - - public Post() { - } - - public Post(Long id) { - this.id = id; - } - - @Override - public Long getId() { - return id; - } - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AutoIdentifierMySQLTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AutoIdentifierMySQLTest.java deleted file mode 100644 index 5cfe0b1b8..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/AutoIdentifierMySQLTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import org.junit.Test; - -import javax.persistence.*; - -public class AutoIdentifierMySQLTest extends AbstractMySQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - @Override - protected boolean nativeHibernateSessionFactoryBootstrap() { - return false; - } - - @Test - public void test() { - doInJPA(entityManager -> { - for ( int i = 1; i <= 3; i++ ) { - entityManager.persist( - new Post( - String.format( - "High-Performance Java Persistence, Part %d", i - ) - ) - ); - } - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - - private String title; - - public Post() {} - - public Post(String title) { - this.title = title; - } - } -} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/EntityIdentifierCockroachDBTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/EntityIdentifierCockroachDBTest.java deleted file mode 100644 index 9277eb796..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/EntityIdentifierCockroachDBTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import java.time.LocalDate; -import java.time.ZoneId; -import java.util.Date; -import java.util.List; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; - -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.AbstractCockroachDBIntegrationTest; - -import static org.junit.Assert.assertEquals; - -public class EntityIdentifierCockroachDBTest extends AbstractCockroachDBIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - @Override - protected boolean nativeHibernateSessionFactoryBootstrap() { - return false; - } - - @Test - public void test() { - doInJPA( entityManager -> { - LocalDate startDate = LocalDate.of( 2016, 11, 2 ); - for ( int offset = 0; offset < 10; offset++ ) { - Post post = new Post(); - post.setTitle( - String.format( - "High-Performance Java Persistence, Review %d", - offset - ) - ); - post.setCreatedOn( - Date.from( startDate - .plusDays( offset ) - .atStartOfDay( ZoneId.of( "UTC" ) ) - .toInstant() - ) - ); - entityManager.persist( post ); - } - } ); - - doInJPA( entityManager -> { - - List posts = entityManager.createQuery( - "select p " + - "from Post p " + - "order by p.createdOn", Post.class ) - .setMaxResults( 5 ) - .getResultList(); - - assertEquals( 5, posts.size() ); - } ); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue( - strategy = GenerationType.IDENTITY - ) - private Long id; - - @Column - @Temporal(TemporalType.DATE) - private Date createdOn; - - private String title; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } -} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/EntityIdentifierTimestampCockroachDBTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/EntityIdentifierTimestampCockroachDBTest.java deleted file mode 100644 index 02d983b13..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/EntityIdentifierTimestampCockroachDBTest.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import java.sql.PreparedStatement; -import java.sql.Timestamp; -import java.time.LocalDate; -import java.time.ZoneId; -import java.util.Date; -import java.util.List; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; - -import org.hibernate.Session; - -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.AbstractCockroachDBIntegrationTest; - -import static org.junit.Assert.assertEquals; - -public class EntityIdentifierTimestampCockroachDBTest extends AbstractCockroachDBIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - @Override - protected boolean nativeHibernateSessionFactoryBootstrap() { - return false; - } - - @Test - public void test() { - doInJPA( entityManager -> { - entityManager.unwrap( Session.class ).doWork( connection -> { - try(PreparedStatement preparedStatement = connection.prepareStatement( - "INSERT INTO post (title, createdOn) " + - "VALUES (?, ?)") - ) { - int index = 0; - preparedStatement.setString( - ++index, - "High-Performance Java Persistence" - ); - preparedStatement.setTimestamp( - ++index, - new Timestamp( System.currentTimeMillis() ) - ); - int updateCount = preparedStatement.executeUpdate(); - - assertEquals( 1, updateCount ); - } - } ); - } ); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(columnDefinition = "timestamptz") - @Temporal(TemporalType.TIMESTAMP) - private Date createdOn; - - private String title; - - public Post() { - } - - public Post(String title) { - this.title = title; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } -} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/HiloIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/HiloIdentifierTest.java deleted file mode 100644 index b4d9513ee..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/HiloIdentifierTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.GenericGenerator; -import org.hibernate.annotations.Parameter; -import org.junit.Test; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; - -public class HiloIdentifierTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Test - public void testHiloIdentifierGenerator() { - doInJPA(entityManager -> { - for(int i = 0; i < 4; i++) { - Post post = new Post(); - entityManager.persist(post); - } - }); - } - - @Entity(name = "Post") - public static class Post { - - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "hilo") - @GenericGenerator( - name = "hilo", - strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator", - parameters = { - @Parameter(name = "sequence_name", value = "sequence"), - @Parameter(name = "initial_value", value = "1"), - @Parameter(name = "increment_size", value = "3"), - @Parameter(name = "optimizer", value = "hilo") - } - ) - private Long id; - } - - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/NativeIdentifierMySQLTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/NativeIdentifierMySQLTest.java deleted file mode 100644 index 323e5dfe6..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/NativeIdentifierMySQLTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import org.hibernate.annotations.GenericGenerator; -import org.junit.Test; - -import javax.persistence.*; - -public class NativeIdentifierMySQLTest extends AbstractMySQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - @Override - protected boolean nativeHibernateSessionFactoryBootstrap() { - return false; - } - - @Test - public void test() { - doInJPA(entityManager -> { - for ( int i = 1; i <= 3; i++ ) { - entityManager.persist( - new Post( - String.format( - "High-Performance Java Persistence, Part %d", i - ) - ) - ); - } - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue(strategy= GenerationType.AUTO, generator="native") - @GenericGenerator(name = "native", strategy = "native") - private Long id; - - private String title; - - public Post() {} - - public Post(String title) { - this.title = title; - } - } -} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/OracleRowIdTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/OracleRowIdTest.java deleted file mode 100644 index d747c6368..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/OracleRowIdTest.java +++ /dev/null @@ -1,186 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; -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.JoinColumn; -import javax.persistence.JoinTable; -import javax.persistence.ManyToMany; -import javax.persistence.ManyToOne; -import javax.persistence.MapsId; -import javax.persistence.OneToMany; -import javax.persistence.OneToOne; -import javax.persistence.Table; - -import org.hibernate.Session; -import org.hibernate.annotations.RowId; - -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractSQLServerIntegrationTest; - -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -public class OracleRowIdTest extends AbstractOracleXEIntegrationTest { - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class - }; - } - - @Test - public void test() { - doInJPA( entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - entityManager.persist(post); - - PostComment comment1 = new PostComment(); - comment1.setReview("Great!"); - post.addComment(comment1); - - PostComment comment2 = new PostComment(); - comment2.setReview("To read"); - post.addComment(comment2); - - PostComment comment3 = new PostComment(); - comment3.setReview("Lorem Ipsum"); - post.addComment(comment3); - } ); - - Post _post = doInJPA( entityManager -> { - return entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.comments " + - "where p.id = :id", Post.class) - .setParameter( "id", 1L ) - .getSingleResult(); - } ); - - List_comments = _post.getComments(); - - _post.getComments().get( 0 ).setReview( "Must read!" ); - _post.removeComment( _comments.get( 2 ) ); - - doInJPA( entityManager -> { - entityManager.merge( _post ); - } ); - } - - @Entity(name = "Post") - @Table(name = "post") - @RowId( "ROWID" ) - public static class Post { - - @Id - private Long id; - - private String title; - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - - public void removeComment(PostComment comment) { - comments.remove(comment); - comment.setPost(null); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @RowId( "ROWID" ) - public static class PostComment { - - @Id - @GeneratedValue - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof PostComment)) return false; - return id != null && id.equals(((PostComment) o).id); - } - - @Override - public int hashCode() { - return 31; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/PooledLoSequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/PooledLoSequenceIdentifierTest.java deleted file mode 100644 index e418119a1..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/PooledLoSequenceIdentifierTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import org.hibernate.annotations.GenericGenerator; -import org.hibernate.annotations.Parameter; -import org.junit.Test; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; - -public class PooledLoSequenceIdentifierTest extends AbstractPooledSequenceIdentifierTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Override - protected Object newEntityInstance() { - return new Post(); - } - - @Test - public void testOptimizer() { - insertSequences(); - } - - @Entity(name = "Post") - public static class Post { - - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pooled-lo") - @GenericGenerator( - name = "pooled-lo", - strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator", - parameters = { - @Parameter(name = "sequence_name", value = "sequence"), - @Parameter(name = "initial_value", value = "1"), - @Parameter(name = "increment_size", value = "3"), - @Parameter(name = "optimizer", value = "pooled-lo") - } - ) - private Long id; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/PooledSequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/PooledSequenceIdentifierTest.java deleted file mode 100644 index 8e5de25f4..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/PooledSequenceIdentifierTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import org.hibernate.annotations.GenericGenerator; -import org.hibernate.annotations.Parameter; -import org.junit.Test; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; - -public class PooledSequenceIdentifierTest extends AbstractPooledSequenceIdentifierTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - protected Object newEntityInstance() { - return new Post(); - } - - @Test - public void testOptimizer() { - insertSequences(); - } - - @Entity(name = "Post") - public static class Post { - - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pooled") - @GenericGenerator( - name = "pooled", - strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator", - parameters = { - @Parameter(name = "sequence_name", value = "sequence"), - @Parameter(name = "initial_value", value = "1"), - @Parameter(name = "increment_size", value = "3"), - @Parameter(name = "optimizer", value = "pooled") - } - ) - private Long id; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/StringSequenceIdentifier.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/StringSequenceIdentifier.java deleted file mode 100644 index 4f4d7c1c0..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/StringSequenceIdentifier.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import org.hibernate.MappingException; -import org.hibernate.Session; -import org.hibernate.dialect.Dialect; -import org.hibernate.engine.config.spi.ConfigurationService; -import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.id.Configurable; -import org.hibernate.id.IdentifierGenerator; -import org.hibernate.id.enhanced.SequenceStyleGenerator; -import org.hibernate.internal.util.config.ConfigurationHelper; -import org.hibernate.service.ServiceRegistry; -import org.hibernate.type.Type; - -import java.io.Serializable; -import java.util.Properties; - - -/** - * @author Vlad Mihalcea - */ -public class StringSequenceIdentifier - implements IdentifierGenerator, Configurable { - - public static final String SEQUENCE_PREFIX = "sequence_prefix"; - - private String sequencePrefix; - - private String sequenceCallSyntax; - - @Override - public void configure( - Type type, Properties params, ServiceRegistry serviceRegistry) - throws MappingException { - final JdbcEnvironment jdbcEnvironment = - serviceRegistry.getService(JdbcEnvironment.class); - final Dialect dialect = jdbcEnvironment.getDialect(); - - final ConfigurationService configurationService = - serviceRegistry.getService(ConfigurationService.class); - String globalEntityIdentifierPrefix = - configurationService.getSetting( "entity.identifier.prefix", String.class, "SEQ_" ); - - sequencePrefix = ConfigurationHelper.getString( - SEQUENCE_PREFIX, - params, - globalEntityIdentifierPrefix); - - final String sequencePerEntitySuffix = ConfigurationHelper.getString( - SequenceStyleGenerator.CONFIG_SEQUENCE_PER_ENTITY_SUFFIX, - params, - SequenceStyleGenerator.DEF_SEQUENCE_SUFFIX); - - final String defaultSequenceName = ConfigurationHelper.getBoolean( - SequenceStyleGenerator.CONFIG_PREFER_SEQUENCE_PER_ENTITY, - params, - false) - ? params.getProperty(JPA_ENTITY_NAME) + sequencePerEntitySuffix - : SequenceStyleGenerator.DEF_SEQUENCE_NAME; - - sequenceCallSyntax = dialect.getSequenceNextValString( - ConfigurationHelper.getString( - SequenceStyleGenerator.SEQUENCE_PARAM, - params, - defaultSequenceName)); - } - - @Override - public Serializable generate(SharedSessionContractImplementor session, Object obj) { - if (obj instanceof Identifiable) { - Identifiable identifiable = (Identifiable) obj; - Serializable id = identifiable.getId(); - if (id != null) { - return id; - } - } - long seqValue = ((Number) Session.class.cast(session) - .createSQLQuery(sequenceCallSyntax) - .uniqueResult()).longValue(); - - return sequencePrefix + String.format("%011d%s", 0 ,seqValue); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/StringSequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/StringSequenceIdentifierTest.java deleted file mode 100644 index aed16bf07..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/StringSequenceIdentifierTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; - -import java.util.Properties; - -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; - -import org.hibernate.annotations.GenericGenerator; -import org.junit.Test; - -import javax.persistence.*; - -public class StringSequenceIdentifierTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - Board.class, - Event.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.setProperty( "entity.identifier.prefix", "ID_" ); - return properties; - } - - @Test - public void test() { - LOGGER.debug("test"); - doInJPA(entityManager -> { - entityManager.persist(new Post()); - entityManager.persist(new Post("ABC")); - entityManager.persist(new Post()); - entityManager.persist(new Post("DEF")); - entityManager.persist(new Post()); - entityManager.persist(new Post()); - }); - doInJPA(entityManager -> { - entityManager.persist(new Board()); - entityManager.persist(new Board("ABC")); - entityManager.persist(new Board()); - entityManager.persist(new Board("DEF")); - entityManager.persist(new Board()); - entityManager.persist(new Board()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post implements Identifiable { - - @Id - @GenericGenerator( - name = "assigned-sequence", - strategy = "com.vladmihalcea.book.hpjp.hibernate.identifier.StringSequenceIdentifier", - parameters = { - @org.hibernate.annotations.Parameter( - name = "sequence_name", value = "hibernate_sequence"), - @org.hibernate.annotations.Parameter( - name = "sequence_prefix", value = "CTC_"), - } - ) - @GeneratedValue(generator = "assigned-sequence", strategy = GenerationType.SEQUENCE) - private String id; - - @Version - private Integer version; - - public Post() { - } - - public Post(String id) { - this.id = id; - } - - @Override - public String getId() { - return id; - } - } - - @Entity(name = "Board") - public static class Board { - - @Id - @GenericGenerator( - name = "assigned-sequence", - strategy = "com.vladmihalcea.book.hpjp.hibernate.identifier.StringSequenceIdentifier", - parameters = { - @org.hibernate.annotations.Parameter( - name = "sequence_name", value = "hibernate_sequence"), - } - ) - @GeneratedValue(generator = "assigned-sequence", strategy = GenerationType.SEQUENCE) - private String id; - - @Version - private Integer version; - - public Board() { - } - - public Board(String id) { - this.id = id; - } - } - - @Entity(name = "Event") - public static class Event { - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE) - private String id; - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/AbstractBatchIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/AbstractBatchIdentifierTest.java deleted file mode 100644 index 240e32871..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/AbstractBatchIdentifierTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; - -import java.util.Properties; - -public abstract class AbstractBatchIdentifierTest extends AbstractTest { - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.order_inserts", "true"); - properties.put("hibernate.order_updates", "true"); - properties.put("hibernate.jdbc.batch_size", "2"); - return properties; - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/AssignedTableGenerator.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/AssignedTableGenerator.java deleted file mode 100644 index 9c5387999..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/AssignedTableGenerator.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch; - -import com.vladmihalcea.book.hpjp.hibernate.identifier.Identifiable; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.id.enhanced.TableGenerator; - -import java.io.Serializable; - -/** - * AssignedTableGenerator - Assigned TableGenerator - * - * @author Vlad Mihalcea - */ -public class AssignedTableGenerator extends TableGenerator { - - @Override - public Serializable generate(SharedSessionContractImplementor session, Object obj) { - if(obj instanceof Identifiable) { - Identifiable identifiable = (Identifiable) obj; - Serializable id = identifiable.getId(); - if(id != null) { - return id; - } - } - return super.generate(session, obj); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/AutoIdentifierWithSequenceGeneratorTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/AutoIdentifierWithSequenceGeneratorTest.java deleted file mode 100644 index 06f8e9445..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/AutoIdentifierWithSequenceGeneratorTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; - -public class AutoIdentifierWithSequenceGeneratorTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - @Test - public void test() { - int batchSize = 10; - doInJPA(entityManager -> { - for (int i = 0; i < batchSize; i++) { - entityManager.persist(new Post()); - } - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO, generator = "custom-sequence") - @SequenceGenerator(name = "custom-sequence", initialValue = 10) - private Long id; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/IdentityIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/IdentityIdentifierTest.java deleted file mode 100644 index 509dd0154..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/IdentityIdentifierTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch; - -import org.junit.Test; - -import javax.persistence.*; - -public class IdentityIdentifierTest extends AbstractBatchIdentifierTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - @Test - public void testIdentityIdentifierGenerator() { - LOGGER.debug("testIdentityIdentifierGenerator"); - int batchSize = 2; - doInJPA(entityManager -> { - for (int i = 0; i < batchSize; i++) { - entityManager.persist(new Post()); - } - LOGGER.debug("Flush is triggered at commit-time"); - }); - } - - @Test - public void testIdentityIdentifierGeneratorOutsideTransaction() { - LOGGER.debug("testIdentityIdentifierGeneratorOutsideTransaction"); - EntityManager entityManager = null; - EntityTransaction txn = null; - try { - entityManager = entityManagerFactory().createEntityManager(); - for (int i = 0; i < 5; i++) { - entityManager.persist(new Post()); - } - txn = entityManager.getTransaction(); - txn.begin(); - txn.commit(); - } catch (RuntimeException e) { - if ( txn != null && txn.isActive()) txn.rollback(); - throw e; - } finally { - if (entityManager != null) { - entityManager.close(); - } - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/SequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/SequenceIdentifierTest.java deleted file mode 100644 index a21832162..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/SequenceIdentifierTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch; - -import org.junit.Test; - -import javax.persistence.*; - -public class SequenceIdentifierTest extends AbstractBatchIdentifierTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - @Test - public void testSequenceCall() { - LOGGER.debug("testSequenceGenerator"); - int batchSize = 2; - doInJPA(entityManager -> { - for (int i = 0; i < batchSize; i++) { - entityManager.persist(new Post()); - } - LOGGER.debug("Flush is triggered at commit-time"); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue(strategy=GenerationType.SEQUENCE) - private Long id; - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/TableIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/TableIdentifierTest.java deleted file mode 100644 index 3afcccc38..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/TableIdentifierTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch; - -import org.junit.Test; - -import javax.persistence.*; - -public class TableIdentifierTest extends AbstractBatchIdentifierTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - @Test - public void testTableIdentifierGenerator() { - LOGGER.debug("testTableIdentifierGenerator"); - int batchSize = 2; - doInJPA(entityManager -> { - for (int i = 0; i < batchSize; i++) { - entityManager.persist(new Post()); - } - LOGGER.debug("Flush is triggered at commit-time"); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue(strategy=GenerationType.TABLE) - private Long id; - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/providers/PostEntityProvider.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/providers/PostEntityProvider.java deleted file mode 100644 index d127755e8..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/providers/PostEntityProvider.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch.concurrent.providers; - -import com.vladmihalcea.book.hpjp.util.EntityProvider; - -/** - * @author Vlad Mihalcea - */ -public abstract class PostEntityProvider implements EntityProvider { - - private final Class clazz; - - protected PostEntityProvider(Class clazz) { - this.clazz = clazz; - } - - public abstract T newPost(); - - @Override - public Class[] entities() { - return new Class[] { - clazz - }; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/jta/JtaTableIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/jta/JtaTableIdentifierTest.java deleted file mode 100644 index e400fceff..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/jta/JtaTableIdentifierTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch.jta; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; - -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = JtaTableIdentifierTestConfiguration.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class JtaTableIdentifierTest { - - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - @PersistenceContext - private EntityManager entityManager; - - @Autowired - private TransactionTemplate transactionTemplate; - - @Test - public void testTableIdentifierGenerator() { - LOGGER.debug("testIdentityIdentifierGenerator"); - transactionTemplate.execute((TransactionCallback) transactionStatus -> { - for (int i = 0; i < 5; i++) { - entityManager.persist(new Post()); - } - LOGGER.debug("Flush is triggered at commit-time"); - return null; - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/jta/JtaTableIdentifierTestConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/jta/JtaTableIdentifierTestConfiguration.java deleted file mode 100644 index 9b4264d34..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/jta/JtaTableIdentifierTestConfiguration.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch.jta; - -import com.vladmihalcea.book.hpjp.util.spring.config.jta.PostgreSQLJtaTransactionManagerConfiguration; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class JtaTableIdentifierTestConfiguration extends PostgreSQLJtaTransactionManagerConfiguration { - - @Override - protected Class configurationClass() { - return Post.class; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/jta/Post.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/jta/Post.java deleted file mode 100644 index 975bc6ac6..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/jta/Post.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch.jta; - -import org.hibernate.annotations.GenericGenerator; - -import javax.persistence.*; - -/** - * @author Vlad Mihalcea - */ -@Entity(name = "Post") -@Table(name = "post") -public class Post { - - @Id - @GenericGenerator(name = "table", strategy = "enhanced-table", parameters = { - @org.hibernate.annotations.Parameter(name = "table_name", value = "sequence_table") - }) - @GeneratedValue(generator = "table", strategy = GenerationType.TABLE) - private Long id; -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/global/MySQLIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/global/MySQLIdentifierTest.java deleted file mode 100644 index f72e5b1a3..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/global/MySQLIdentifierTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.global; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import org.junit.Test; - -public class MySQLIdentifierTest extends AbstractMySQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - @Override - protected String[] resources() { - return new String[] { - "mappings/identifier/global/mysql-orm.xml" - }; - } - - @Test - public void test() { - doInJPA(entityManager -> { - for (int i = 0; i < 5; i++) { - Post post = new Post(); - post.setTitle(String.format("Post nr %d", i + 1)); - entityManager.persist(post); - } - }); - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/global/Post.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/global/Post.java deleted file mode 100644 index 783f3c9a1..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/global/Post.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.global; - -import javax.persistence.*; - -/** - * @author Vlad Mihalcea - */ -@Entity(name = "Post") -@Table(name = "post") -public class Post { - - @Id - @GeneratedValue(generator = "sequence", strategy = GenerationType.SEQUENCE) - @SequenceGenerator(name = "sequence", allocationSize = 10) - private Long id; - - private String title; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/global/PostgreSQLIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/global/PostgreSQLIdentifierTest.java deleted file mode 100644 index 45bb85f73..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/global/PostgreSQLIdentifierTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.global; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.junit.Test; - -public class PostgreSQLIdentifierTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - }; - } - - @Test - public void test() { - doInJPA(entityManager -> { - for (int i = 0; i < 5; i++) { - Post post = new Post(); - post.setTitle(String.format("Post nr %d", i + 1)); - entityManager.persist(post); - } - }); - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/globalsequence/package-info.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/globalsequence/package-info.java deleted file mode 100644 index 99ae68a6d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/globalsequence/package-info.java +++ /dev/null @@ -1,14 +0,0 @@ -@GenericGenerator( - name = "pooled", - strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator", - parameters = { - @Parameter(name = "sequence_name", value = "sequence"), - @Parameter(name = "initial_value", value = "1"), - @Parameter(name = "increment_size", value = "5"), - @Parameter(name = "optimizer", value = "pooled-lo"), - } -) -package com.vladmihalcea.book.hpjp.hibernate.identifier.globalsequence; - -import org.hibernate.annotations.GenericGenerator; -import org.hibernate.annotations.Parameter; \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/PostEntityProvider.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/PostEntityProvider.java deleted file mode 100644 index bcd7f8f2d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/PostEntityProvider.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.optimizer.providers; - -import com.vladmihalcea.book.hpjp.util.EntityProvider; - -/** - * @author Vlad Mihalcea - */ -public abstract class PostEntityProvider implements EntityProvider { - - private final Class clazz; - - protected PostEntityProvider(Class clazz) { - this.clazz = clazz; - } - - public abstract T newPost(); - - @Override - public Class[] entities() { - return new Class[] { - clazz - }; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/uuid/AssignedUUIDIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/uuid/AssignedUUIDIdentifierTest.java deleted file mode 100644 index fbf6f3ca6..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/uuid/AssignedUUIDIdentifierTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.uuid; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; -import java.util.UUID; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; - -public class AssignedUUIDIdentifierTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Test - public void testAssignedIdentifierGenerator() { - LOGGER.debug("testAssignedIdentifierGenerator"); - doInJPA(entityManager -> { - Post post = new Post(); - LOGGER.debug("persist Post"); - entityManager.persist(post); - entityManager.flush(); - assertSame(post, entityManager - .createQuery("select p from Post p where p.id = :uuid", Post.class) - .setParameter("uuid", post.id) - .getSingleResult()); - byte[] uuid = (byte[]) entityManager.createNativeQuery("select id from Post").getSingleResult(); - assertNotNull(uuid); - LOGGER.debug("merge Post"); - entityManager.merge(new Post()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @Column(columnDefinition = "BINARY(16)") - private UUID id = UUID.randomUUID(); - - public Post() {} - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/uuid/UUID2IdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/uuid/UUID2IdentifierTest.java deleted file mode 100644 index 9ad719d89..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/uuid/UUID2IdentifierTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.uuid; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.GenericGenerator; -import org.junit.Test; - -import javax.persistence.*; -import java.util.UUID; - -public class UUID2IdentifierTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Test - public void testUUID2IdentifierGenerator() { - LOGGER.debug("testUUID2IdentifierGenerator"); - doInJPA(entityManager -> { - entityManager.persist(new Post()); - entityManager.flush(); - entityManager.merge(new Post()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue(generator = "uuid2") - @GenericGenerator(name = "uuid2", strategy = "uuid2") - @Column(columnDefinition = "BINARY(16)") - private UUID id; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/uuid/UUIDIdentifierTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/uuid/UUIDIdentifierTest.java deleted file mode 100644 index 025ed363f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/uuid/UUIDIdentifierTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.uuid; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.GenericGenerator; -import org.junit.Test; - -import javax.persistence.*; - -public class UUIDIdentifierTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Test - public void testUUIDIdentifierGenerator() { - LOGGER.debug("testUUIDIdentifierGenerator"); - doInJPA(entityManager -> { - entityManager.persist(new Post()); - entityManager.flush(); - entityManager.merge(new Post()); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue(generator = "uuid") - @GenericGenerator(name = "uuid", strategy = "uuid") - @Column(columnDefinition = "CHAR(32)") - private String id; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/JoinTableBulkDeleteTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/JoinTableBulkDeleteTest.java deleted file mode 100644 index 215ca70dd..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/JoinTableBulkDeleteTest.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class JoinTableBulkDeleteTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Board.class, - Topic.class, - Post.class, - Announcement.class, - }; - } - - @Test - public void test() { - Topic topic = doInJPA(entityManager -> { - Board board = new Board(); - board.setName("Hibernate"); - - entityManager.persist(board); - - Post post = new Post(); - post.setOwner("John Doe"); - post.setTitle("Inheritance"); - post.setContent("Best practices"); - post.setBoard(board); - - entityManager.persist(post); - - Announcement announcement = new Announcement(); - announcement.setOwner("John Doe"); - announcement.setTitle("Release x.y.z.Final"); - announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); - announcement.setBoard(board); - - entityManager.persist(announcement); - - return post; - }); - - doInJPA(entityManager -> { - int updateCount = entityManager - .createQuery("delete from Topic") - .executeUpdate(); - assertEquals(2, updateCount); - }); - } - - @Entity(name = "Board") - @Table(name = "board") - public static class Board { - - @Id - @GeneratedValue - private Long id; - - private String name; - - //Only useful for the sake of seeing the queries being generated. - @OneToMany(mappedBy = "board") - private List topics = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getTopics() { - return topics; - } - } - - @Entity(name = "Topic") - @Table(name = "topic") - @Inheritance(strategy = InheritanceType.JOINED) - public static class Topic { - - @Id - @GeneratedValue - private Long id; - - private String title; - - private String owner; - - @Temporal(TemporalType.TIMESTAMP) - private Date createdOn = new Date(); - - @ManyToOne(fetch = FetchType.LAZY) - private Board board; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getOwner() { - return owner; - } - - public void setOwner(String owner) { - this.owner = owner; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public Board getBoard() { - return board; - } - - public void setBoard(Board board) { - this.board = board; - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post extends Topic { - - private String content; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - } - - @Entity(name = "Announcement") - @Table(name = "announcement") - public static class Announcement extends Topic { - - @Temporal(TemporalType.TIMESTAMP) - private Date validUntil; - - public Date getValidUntil() { - return validUntil; - } - - public void setValidUntil(Date validUntil) { - this.validUntil = validUntil; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/JoinTableDiscriminatorTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/JoinTableDiscriminatorTest.java deleted file mode 100644 index 8dc94c3e2..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/JoinTableDiscriminatorTest.java +++ /dev/null @@ -1,323 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class JoinTableDiscriminatorTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Board.class, - Topic.class, - Post.class, - Announcement.class, - TopicStatistics.class - }; - } - - @Test - public void test() { - Topic topic = doInJPA(entityManager -> { - Board board = new Board(); - board.setName("Hibernate"); - - entityManager.persist(board); - - Post post = new Post(); - post.setOwner("John Doe"); - post.setTitle("Inheritance"); - post.setContent("Best practices"); - post.setBoard(board); - - entityManager.persist(post); - - Announcement announcement = new Announcement(); - announcement.setOwner("John Doe"); - announcement.setTitle("Release x.y.z.Final"); - announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); - announcement.setBoard(board); - - entityManager.persist(announcement); - - TopicStatistics postStatistics = new TopicStatistics(post); - postStatistics.incrementViews(); - entityManager.persist(postStatistics); - - TopicStatistics announcementStatistics = new TopicStatistics(announcement); - announcementStatistics.incrementViews(); - entityManager.persist(announcementStatistics); - - return post; - }); - - doInJPA(entityManager -> { - Board board = topic.getBoard(); - LOGGER.info("Fetch Topics"); - List topics = entityManager - .createQuery("select t from Topic t where t.board = :board", Topic.class) - .setParameter("board", board) - .getResultList(); - }); - - doInJPA(entityManager -> { - LOGGER.info("Fetch Board topics"); - entityManager.find(Board.class, topic.getBoard().getId()).getTopics().size(); - }); - - doInJPA(entityManager -> { - LOGGER.info("Fetch Board topics eagerly"); - Long id = topic.getBoard().getId(); - Board board = entityManager.createQuery( - "select b from Board b join fetch b.topics where b.id = :id", Board.class) - .setParameter("id", id) - .getSingleResult(); - }); - - doInJPA(entityManager -> { - Long topicId = topic.getId(); - LOGGER.info("Fetch statistics"); - TopicStatistics statistics = entityManager - .createQuery("select s from TopicStatistics s join fetch s.topic t where t.id = :topicId", TopicStatistics.class) - .setParameter("topicId", topicId) - .getSingleResult(); - }); - } - - @Test - public void testQueryUsingAll() { - doInJPA(entityManager -> { - Board board1 = new Board(); - board1.setName("Hibernate"); - - entityManager.persist(board1); - - Post post1 = new Post(); - post1.setOwner("John Doe"); - post1.setTitle("Inheritance"); - post1.setContent("Best practices"); - post1.setBoard(board1); - - entityManager.persist(post1); - - Announcement announcement1 = new Announcement(); - announcement1.setOwner("John Doe"); - announcement1.setTitle("Release x.y.z.Final"); - announcement1.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); - announcement1.setBoard(board1); - - entityManager.persist(announcement1); - - Board board2 = new Board(); - board2.setName("JPA"); - - entityManager.persist(board2); - - Post post2 = new Post(); - post2.setOwner("John Doe"); - post2.setTitle("Inheritance"); - post2.setContent("Best practices"); - post2.setBoard(board2); - - entityManager.persist(post2); - - Post post3 = new Post(); - post3.setOwner("John Doe"); - post3.setTitle("Inheritance"); - post3.setContent("More best practices"); - post3.setBoard(board2); - - entityManager.persist(post3); - }); - - doInJPA(entityManager -> { - List postOnlyBoards = entityManager - .createQuery( - "select distinct b " + - "from Board b " + - "where Post = all (" + - " select type(t) from Topic t where t.board = b" + - ")", Board.class) - .getResultList(); - assertEquals(1, postOnlyBoards.size()); - assertEquals("JPA", postOnlyBoards.get(0).getName()); - }); - } - - @Entity(name = "Board") - @Table(name = "board") - public static class Board { - - @Id - @GeneratedValue - private Long id; - - private String name; - - //Only useful for the sake of seeing the queries being generated. - @OneToMany(mappedBy = "board") - private List topics = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getTopics() { - return topics; - } - } - - @Entity(name = "Topic") - @Table(name = "topic") - @Inheritance(strategy = InheritanceType.JOINED) - @DiscriminatorColumn(name="class_type") - public static class Topic { - - @Id - @GeneratedValue - private Long id; - - private String title; - - private String owner; - - @Temporal(TemporalType.TIMESTAMP) - private Date createdOn = new Date(); - - @ManyToOne(fetch = FetchType.LAZY) - private Board board; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getOwner() { - return owner; - } - - public void setOwner(String owner) { - this.owner = owner; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public Board getBoard() { - return board; - } - - public void setBoard(Board board) { - this.board = board; - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post extends Topic { - - private String content; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - } - - @Entity(name = "Announcement") - @Table(name = "announcement") - public static class Announcement extends Topic { - - @Temporal(TemporalType.TIMESTAMP) - private Date validUntil; - - public Date getValidUntil() { - return validUntil; - } - - public void setValidUntil(Date validUntil) { - this.validUntil = validUntil; - } - } - - @Entity(name = "TopicStatistics") - @Table(name = "topic_statistics") - public static class TopicStatistics { - - @Id - @GeneratedValue - private Long id; - - @OneToOne - @JoinColumn(name = "id") - @MapsId - private Topic topic; - - private long views; - - public TopicStatistics() {} - - public TopicStatistics(Topic topic) { - this.topic = topic; - } - - public Long getId() { - return id; - } - - public Topic getTopic() { - return topic; - } - - public long getViews() { - return views; - } - - public void incrementViews() { - this.views++; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/JoinTableTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/JoinTableTest.java deleted file mode 100644 index 0ab0cc94f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/JoinTableTest.java +++ /dev/null @@ -1,349 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class JoinTableTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Board.class, - Topic.class, - Post.class, - Announcement.class, - TopicStatistics.class - }; - } - - @Test - public void test() { - Topic topic = doInJPA(entityManager -> { - Board board = new Board(); - board.setName("Hibernate"); - - entityManager.persist(board); - - Post post = new Post(); - post.setOwner("John Doe"); - post.setTitle("Inheritance"); - post.setContent("Best practices"); - post.setBoard(board); - - entityManager.persist(post); - - Announcement announcement = new Announcement(); - announcement.setOwner("John Doe"); - announcement.setTitle("Release x.y.z.Final"); - announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); - announcement.setBoard(board); - - entityManager.persist(announcement); - - TopicStatistics postStatistics = new TopicStatistics(post); - postStatistics.incrementViews(); - entityManager.persist(postStatistics); - - TopicStatistics announcementStatistics = new TopicStatistics(announcement); - announcementStatistics.incrementViews(); - entityManager.persist(announcementStatistics); - - return post; - }); - - doInJPA(entityManager -> { - Board board = topic.getBoard(); - LOGGER.info("Fetch Topics"); - List topics = entityManager - .createQuery("select t from Topic t where t.board = :board", Topic.class) - .setParameter("board", board) - .getResultList(); - }); - - doInJPA(entityManager -> { - Board board = topic.getBoard(); - LOGGER.info("Fetch Topic projection"); - List titles = entityManager - .createQuery("select t.title from Topic t where t.board = :board", String.class) - .setParameter("board", board) - .getResultList(); - assertEquals(2, titles.size()); - }); - - doInJPA(entityManager -> { - LOGGER.info("Fetch just one Topic"); - Topic _topic = entityManager.find(Topic.class, topic.getId()); - }); - - doInJPA(entityManager -> { - LOGGER.info("Fetch Board topics"); - entityManager.find(Board.class, topic.getBoard().getId()).getTopics().size(); - }); - - doInJPA(entityManager -> { - LOGGER.info("Fetch Board topics eagerly"); - Long id = topic.getBoard().getId(); - Board board = entityManager.createQuery( - "select b from Board b join fetch b.topics where b.id = :id", Board.class) - .setParameter("id", id) - .getSingleResult(); - }); - - doInJPA(entityManager -> { - Long topicId = topic.getId(); - LOGGER.info("Fetch statistics"); - TopicStatistics statistics = entityManager - .createQuery("select s from TopicStatistics s join fetch s.topic t where t.id = :topicId", TopicStatistics.class) - .setParameter("topicId", topicId) - .getSingleResult(); - }); - - TopicStatistics statistics = doInJPA(entityManager -> { - Long topicId = topic.getId(); - LOGGER.info("Fetch one TopicStatistic"); - return entityManager.find(TopicStatistics.class, topicId); - }); - - try { - statistics.getTopic().getCreatedOn(); - } - catch (Exception expected) { - LOGGER.info( "Topic was not fetched" ); - } - } - - @Test - public void testQueryUsingAll() { - doInJPA(entityManager -> { - Board board1 = new Board(); - board1.setName("Hibernate"); - - entityManager.persist(board1); - - Post post1 = new Post(); - post1.setOwner("John Doe"); - post1.setTitle("Inheritance"); - post1.setContent("Best practices"); - post1.setBoard(board1); - - entityManager.persist(post1); - - Announcement announcement1 = new Announcement(); - announcement1.setOwner("John Doe"); - announcement1.setTitle("Release x.y.z.Final"); - announcement1.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); - announcement1.setBoard(board1); - - entityManager.persist(announcement1); - - Board board2 = new Board(); - board2.setName("JPA"); - - entityManager.persist(board2); - - Post post2 = new Post(); - post2.setOwner("John Doe"); - post2.setTitle("Inheritance"); - post2.setContent("Best practices"); - post2.setBoard(board2); - - entityManager.persist(post2); - - Post post3 = new Post(); - post3.setOwner("John Doe"); - post3.setTitle("Inheritance"); - post3.setContent("More best practices"); - post3.setBoard(board2); - - entityManager.persist(post3); - }); - - doInJPA(entityManager -> { - List postOnlyBoards = entityManager - .createQuery( - "select distinct b " + - "from Board b " + - "where Post = all (" + - " select type(t) from Topic t where t.board = b" + - ")", Board.class) - .getResultList(); - assertEquals(1, postOnlyBoards.size()); - assertEquals("JPA", postOnlyBoards.get(0).getName()); - }); - } - - @Entity(name = "Board") - @Table(name = "board") - public static class Board { - - @Id - @GeneratedValue - private Long id; - - private String name; - - //Only useful for the sake of seeing the queries being generated. - @OneToMany(mappedBy = "board") - private List topics = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getTopics() { - return topics; - } - } - - @Entity(name = "Topic") - @Table(name = "topic") - @Inheritance(strategy = InheritanceType.JOINED) - public static class Topic { - - @Id - @GeneratedValue - private Long id; - - private String title; - - private String owner; - - @Temporal(TemporalType.TIMESTAMP) - private Date createdOn = new Date(); - - @ManyToOne(fetch = FetchType.LAZY) - private Board board; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getOwner() { - return owner; - } - - public void setOwner(String owner) { - this.owner = owner; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public Board getBoard() { - return board; - } - - public void setBoard(Board board) { - this.board = board; - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post extends Topic { - - private String content; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - } - - @Entity(name = "Announcement") - @Table(name = "announcement") - public static class Announcement extends Topic { - - @Temporal(TemporalType.TIMESTAMP) - private Date validUntil; - - public Date getValidUntil() { - return validUntil; - } - - public void setValidUntil(Date validUntil) { - this.validUntil = validUntil; - } - } - - @Entity(name = "TopicStatistics") - @Table(name = "topic_statistics") - public static class TopicStatistics { - - @Id - private Long id; - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") - @MapsId - private Topic topic; - - private long views; - - public TopicStatistics() {} - - public TopicStatistics(Topic topic) { - this.topic = topic; - } - - public Long getId() { - return id; - } - - public Topic getTopic() { - return topic; - } - - public long getViews() { - return views; - } - - public void incrementViews() { - this.views++; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/MappedSuperclassTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/MappedSuperclassTest.java deleted file mode 100644 index 13fc431bc..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/MappedSuperclassTest.java +++ /dev/null @@ -1,254 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.Date; - -/** - * @author Vlad Mihalcea - */ -public class MappedSuperclassTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Board.class, - Post.class, - Announcement.class, - PostStatistics.class, - AnnouncementStatistics.class - }; - } - - @Test - public void test() { - Topic topic = doInJPA(entityManager -> { - Board board = new Board(); - board.setId(1L); - board.setName("Hibernate"); - - entityManager.persist(board); - - Post post = new Post(); - post.setId(1L); - post.setOwner("John Doe"); - post.setTitle("Inheritance"); - post.setContent("Best practices"); - post.setBoard(board); - - entityManager.persist(post); - - Announcement announcement = new Announcement(); - announcement.setId(2L); - announcement.setOwner("John Doe"); - announcement.setTitle("Release x.y.z.Final"); - announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); - announcement.setBoard(board); - - entityManager.persist(announcement); - - TopicStatistics postStatistics = new PostStatistics(post); - postStatistics.incrementViews(); - entityManager.persist(postStatistics); - - TopicStatistics announcementStatistics = new AnnouncementStatistics(announcement); - announcementStatistics.incrementViews(); - entityManager.persist(announcementStatistics); - - return post; - }); - - doInJPA(entityManager -> { - Long postId = topic.getId(); - LOGGER.info("Fetch statistics"); - PostStatistics statistics = entityManager - .createQuery("select s from PostStatistics s join fetch s.topic t where t.id = :postId", PostStatistics.class) - .setParameter("postId", postId) - .getSingleResult(); - }); - } - - @Entity(name = "Board") - @Table(name = "board") - public static class Board { - - @Id - private Long id; - - private String name; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - @MappedSuperclass - public static abstract class Topic { - - @Id - private Long id; - - private String title; - - private String owner; - - @Temporal(TemporalType.TIMESTAMP) - private Date createdOn = new Date(); - - @ManyToOne - private Board board; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getOwner() { - return owner; - } - - public void setOwner(String owner) { - this.owner = owner; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public Board getBoard() { - return board; - } - - public void setBoard(Board board) { - this.board = board; - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post extends Topic { - - private String content; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - } - - @Entity(name = "Announcement") - @Table(name = "announcement") - public static class Announcement extends Topic { - - @Temporal(TemporalType.TIMESTAMP) - private Date validUntil; - - public Date getValidUntil() { - return validUntil; - } - - public void setValidUntil(Date validUntil) { - this.validUntil = validUntil; - } - } - - @MappedSuperclass - public abstract static class TopicStatistics { - - @Id - private Long id; - - private long views; - - public Long getId() { - return id; - } - - public abstract T getTopic(); - - public long getViews() { - return views; - } - - public void incrementViews() { - this.views++; - } - } - - @Entity(name = "PostStatistics") - @Table(name = "post_statistics") - public static class PostStatistics extends TopicStatistics { - - @OneToOne - @JoinColumn(name = "id") - @MapsId - private Post topic; - - public PostStatistics() {} - - public PostStatistics(Post topic) { - this.topic = topic; - } - - @Override - public Post getTopic() { - return topic; - } - } - - @Entity(name = "AnnouncementStatistics") - @Table(name = "announcement_statistics") - public static class AnnouncementStatistics extends TopicStatistics { - - @OneToOne - @JoinColumn(name = "id") - @MapsId - private Announcement topic; - - public AnnouncementStatistics() {} - - public AnnouncementStatistics(Announcement topic) { - this.topic = topic; - } - - @Override - public Announcement getTopic() { - return topic; - } - } -} - diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/MySQLSingleTableTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/MySQLSingleTableTest.java deleted file mode 100644 index 5bb663605..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/MySQLSingleTableTest.java +++ /dev/null @@ -1,325 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.Session; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * @author Vlad Mihalcea - */ -public class MySQLSingleTableTest extends AbstractMySQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Board.class, - Topic.class, - Post.class, - Announcement.class, - TopicStatistics.class - }; - } - - @Test - public void test() { - Topic topic = doInJPA(entityManager -> { - - entityManager.unwrap(Session.class).doWork(connection -> { - try(Statement st = connection.createStatement()) { - st.executeUpdate( - "CREATE " + - "TRIGGER post_content_check BEFORE INSERT " + - "ON Topic " + - "FOR EACH ROW " + - "BEGIN " + - " IF NEW.DTYPE = 'Post' " + - " THEN " + - " IF NEW.content IS NULL " + - " THEN " + - " signal sqlstate '45000' " + - " set message_text = 'Post content cannot be NULL'; " + - " END IF; " + - " END IF; " + - "END;" - ); - st.executeUpdate( - "CREATE " + - "TRIGGER announcement_validUntil_check BEFORE INSERT " + - "ON Topic " + - "FOR EACH ROW " + - "BEGIN " + - " IF NEW.DTYPE = 'Announcement' " + - " THEN " + - " IF NEW.validUntil IS NULL " + - " THEN " + - " signal sqlstate '45000' " + - " set message_text = 'Announcement validUntil cannot be NULL'; " + - " END IF; " + - " END IF; " + - "END;" - ); - } - }); - - Board board = new Board(); - board.setName("Hibernate"); - - entityManager.persist(board); - - Post post = new Post(); - post.setOwner("John Doe"); - post.setTitle("Inheritance"); - post.setContent("Best practices"); - post.setBoard(board); - - entityManager.persist(post); - - Announcement announcement = new Announcement(); - announcement.setOwner("John Doe"); - announcement.setTitle("Release x.y.z.Final"); - announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); - announcement.setBoard(board); - - entityManager.persist(announcement); - - TopicStatistics postStatistics = new TopicStatistics(post); - postStatistics.incrementViews(); - entityManager.persist(postStatistics); - - TopicStatistics announcementStatistics = new TopicStatistics(announcement); - announcementStatistics.incrementViews(); - entityManager.persist(announcementStatistics); - - return post; - }); - - doInJPA(entityManager -> { - Board board = topic.getBoard(); - LOGGER.info("Fetch Topics"); - List topics = entityManager - .createQuery("select t from Topic t where t.board = :board", Topic.class) - .setParameter("board", board) - .getResultList(); - }); - - doInJPA(entityManager -> { - LOGGER.info("Fetch Board topics"); - entityManager.find(Board.class, topic.getBoard().getId()).getTopics().size(); - }); - - doInJPA(entityManager -> { - LOGGER.info("Fetch Board topics eagerly"); - Long id = topic.getBoard().getId(); - Board board = entityManager.createQuery( - "select b from Board b join fetch b.topics where b.id = :id", Board.class) - .setParameter("id", id) - .getSingleResult(); - }); - - doInJPA(entityManager -> { - Long topicId = topic.getId(); - LOGGER.info("Fetch statistics"); - TopicStatistics statistics = entityManager - .createQuery("select s from TopicStatistics s join fetch s.topic t where t.id = :topicId", TopicStatistics.class) - .setParameter("topicId", topicId) - .getSingleResult(); - }); - - try { - doInJPA(entityManager -> { - Post post = new Post(); - post.setCreatedOn(new Date()); - entityManager.persist(post); - }); - fail("content_check should fail"); - } catch (Exception expected) { - assertEquals(PersistenceException.class, expected.getCause().getClass()); - } - - try { - doInJPA(entityManager -> { - Announcement announcement = new Announcement(); - entityManager.persist(announcement); - }); - fail("content_check should fail"); - } catch (Exception expected) { - assertEquals(PersistenceException.class, expected.getCause().getClass()); - } - } - - @Entity(name = "Board") - @Table(name = "board") - public static class Board { - - @Id - @GeneratedValue - private Long id; - - private String name; - - //Only useful for the sake of seeing the queries being generated. - @OneToMany(mappedBy = "board") - private List topics = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getTopics() { - return topics; - } - } - - @Entity(name = "Topic") - @Table(name = "topic") - @Inheritance(strategy = InheritanceType.SINGLE_TABLE) - public static class Topic { - - @Id - @GeneratedValue - private Long id; - - private String title; - - private String owner; - - @Temporal(TemporalType.TIMESTAMP) - private Date createdOn = new Date(); - - @ManyToOne(fetch = FetchType.LAZY) - private Board board; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getOwner() { - return owner; - } - - public void setOwner(String owner) { - this.owner = owner; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public Board getBoard() { - return board; - } - - public void setBoard(Board board) { - this.board = board; - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post extends Topic { - - private String content; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - } - - @Entity(name = "Announcement") - @Table(name = "announcement") - public static class Announcement extends Topic { - - @Temporal(TemporalType.TIMESTAMP) - private Date validUntil; - - public Date getValidUntil() { - return validUntil; - } - - public void setValidUntil(Date validUntil) { - this.validUntil = validUntil; - } - } - - @Entity(name = "TopicStatistics") - @Table(name = "topic_statistics") - public static class TopicStatistics { - - @Id - @GeneratedValue - private Long id; - - @OneToOne - @JoinColumn(name = "id") - @MapsId - private Topic topic; - - private long views; - - public TopicStatistics() {} - - public TopicStatistics(Topic topic) { - this.topic = topic; - } - - public Long getId() { - return id; - } - - public Topic getTopic() { - return topic; - } - - public long getViews() { - return views; - } - - public void incrementViews() { - this.views++; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/SingleTableTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/SingleTableTest.java deleted file mode 100644 index 7a6fbed9a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/SingleTableTest.java +++ /dev/null @@ -1,335 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance; - -import com.vladmihalcea.book.hpjp.util.*; -import org.hibernate.Session; -import org.hibernate.jdbc.Work; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.Connection; -import java.sql.SQLException; -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.springframework.test.util.AssertionErrors.assertTrue; - -/** - * @author Vlad Mihalcea - */ -public class SingleTableTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Board.class, - Topic.class, - Post.class, - Announcement.class, - TopicStatistics.class - }; - } - - @Test - public void test() { - Topic topic = doInJPA(entityManager -> { - - entityManager.unwrap(Session.class).doWork(connection -> { - try(Statement st = connection.createStatement()) { - st.executeUpdate( - "ALTER TABLE Topic " + - "ADD CONSTRAINT post_content_check CHECK " + - "( " + - " CASE " + - " WHEN DTYPE = 'Post' THEN " + - " CASE " + - " WHEN content IS NOT NULL " + - " THEN 1 " + - " ELSE 0 " + - " END " + - " ELSE 1 " + - " END = 1 " + - ")" - ); - st.executeUpdate( - "ALTER TABLE Topic " + - "ADD CONSTRAINT announcement_validUntil_check CHECK " + - "( " + - " CASE " + - " WHEN DTYPE = 'Announcement' THEN " + - " CASE " + - " WHEN validUntil IS NOT NULL " + - " THEN 1 " + - " ELSE 0 " + - " END " + - " ELSE 1 " + - " END = 1 " + - ")" - ); - } - }); - - Board board = new Board(); - board.setName("Hibernate"); - - entityManager.persist(board); - - Post post = new Post(); - post.setOwner("John Doe"); - post.setTitle("Inheritance"); - post.setContent("Best practices"); - post.setBoard(board); - - entityManager.persist(post); - - Announcement announcement = new Announcement(); - announcement.setOwner("John Doe"); - announcement.setTitle("Release x.y.z.Final"); - announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); - announcement.setBoard(board); - - entityManager.persist(announcement); - - TopicStatistics postStatistics = new TopicStatistics(post); - postStatistics.incrementViews(); - entityManager.persist(postStatistics); - - TopicStatistics announcementStatistics = new TopicStatistics(announcement); - announcementStatistics.incrementViews(); - entityManager.persist(announcementStatistics); - - return post; - }); - - doInJPA(entityManager -> { - Board board = topic.getBoard(); - LOGGER.info("Fetch Topics"); - List topics = entityManager - .createQuery("select t from Topic t where t.board = :board", Topic.class) - .setParameter("board", board) - .getResultList(); - }); - - doInJPA(entityManager -> { - LOGGER.info("Fetch Board topics"); - entityManager.find(Board.class, topic.getBoard().getId()).getTopics().size(); - }); - - doInJPA(entityManager -> { - LOGGER.info("Fetch Board topics eagerly"); - Long id = topic.getBoard().getId(); - Board board = entityManager.createQuery( - "select b from Board b join fetch b.topics where b.id = :id", Board.class) - .setParameter("id", id) - .getSingleResult(); - }); - - doInJPA(entityManager -> { - Long topicId = topic.getId(); - LOGGER.info("Fetch statistics"); - TopicStatistics statistics = entityManager - .createQuery("select s from TopicStatistics s join fetch s.topic t where t.id = :topicId", TopicStatistics.class) - .setParameter("topicId", topicId) - .getSingleResult(); - }); - - try { - doInJPA(entityManager -> { - entityManager.persist(new Post()); - }); - fail("content_check should fail"); - } catch (Exception expected) { - assertEquals(PersistenceException.class, expected.getCause().getClass()); - } - - try { - doInJPA(entityManager -> { - entityManager.persist(new Announcement()); - }); - fail("announcement_validUntil_check should fail"); - } catch (Exception expected) { - assertEquals(PersistenceException.class, expected.getCause().getClass()); - } - - doInJPA(entityManager -> { - Board board = topic.getBoard(); - LOGGER.info("Fetch Posts"); - List posts = entityManager - .createQuery( - "select p " + - "from Post p " + - "where p.board = :board", Post.class) - .setParameter("board", board) - .getResultList(); - }); - } - - @Entity(name = "Board") - @Table(name = "board") - public static class Board { - - @Id - @GeneratedValue - private Long id; - - private String name; - - //Only useful for the sake of seeing the queries being generated. - @OneToMany(mappedBy = "board") - private List topics = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getTopics() { - return topics; - } - } - - @Entity(name = "Topic") - @Table(name = "topic") - @Inheritance(strategy = InheritanceType.SINGLE_TABLE) - public static class Topic { - - @Id - @GeneratedValue - private Long id; - - private String title; - - private String owner; - - @Temporal(TemporalType.TIMESTAMP) - private Date createdOn = new Date(); - - @ManyToOne(fetch = FetchType.LAZY) - private Board board; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getOwner() { - return owner; - } - - public void setOwner(String owner) { - this.owner = owner; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public Board getBoard() { - return board; - } - - public void setBoard(Board board) { - this.board = board; - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post extends Topic { - - private String content; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - } - - @Entity(name = "Announcement") - @Table(name = "announcement") - public static class Announcement extends Topic { - - @Temporal(TemporalType.TIMESTAMP) - private Date validUntil; - - public Date getValidUntil() { - return validUntil; - } - - public void setValidUntil(Date validUntil) { - this.validUntil = validUntil; - } - } - - @Entity(name = "TopicStatistics") - @Table(name = "topic_statistics") - public static class TopicStatistics { - - @Id - @GeneratedValue - private Long id; - - @OneToOne - @JoinColumn(name = "id") - @MapsId - private Topic topic; - - private long views; - - public TopicStatistics() {} - - public TopicStatistics(Topic topic) { - this.topic = topic; - } - - public Long getId() { - return id; - } - - public Topic getTopic() { - return topic; - } - - public long getViews() { - return views; - } - - public void incrementViews() { - this.views++; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/TablePerClassTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/TablePerClassTest.java deleted file mode 100644 index f20563988..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/TablePerClassTest.java +++ /dev/null @@ -1,260 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public class TablePerClassTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Board.class, - Topic.class, - Post.class, - Announcement.class, - TopicStatistics.class - }; - } - - @Test - public void test() { - Topic topic = doInJPA(entityManager -> { - Board board = new Board(); - board.setName("Hibernate"); - - entityManager.persist(board); - - Post post = new Post(); - post.setOwner("John Doe"); - post.setTitle("Inheritance"); - post.setContent("Best practices"); - post.setBoard(board); - - entityManager.persist(post); - - Announcement announcement = new Announcement(); - announcement.setOwner("John Doe"); - announcement.setTitle("Release x.y.z.Final"); - announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); - announcement.setBoard(board); - - entityManager.persist(announcement); - - TopicStatistics postStatistics = new TopicStatistics(post); - postStatistics.incrementViews(); - entityManager.persist(postStatistics); - - TopicStatistics announcementStatistics = new TopicStatistics(announcement); - announcementStatistics.incrementViews(); - entityManager.persist(announcementStatistics); - - return post; - }); - - doInJPA(entityManager -> { - Board board = topic.getBoard(); - LOGGER.info("Fetch Topics"); - List topics = entityManager - .createQuery("select t from Topic t where t.board = :board", Topic.class) - .setParameter("board", board) - .getResultList(); - }); - - doInJPA(entityManager -> { - LOGGER.info("Fetch Board topics"); - entityManager.find(Board.class, topic.getBoard().getId()).getTopics().size(); - }); - - doInJPA(entityManager -> { - LOGGER.info("Fetch Board topics eagerly"); - Long id = topic.getBoard().getId(); - Board board = entityManager.createQuery( - "select b from Board b join fetch b.topics where b.id = :id", Board.class) - .setParameter("id", id) - .getSingleResult(); - }); - - doInJPA(entityManager -> { - Long topicId = topic.getId(); - LOGGER.info("Fetch statistics"); - TopicStatistics statistics = entityManager - .createQuery("select s from TopicStatistics s join fetch s.topic t where t.id = :topicId", TopicStatistics.class) - .setParameter("topicId", topicId) - .getSingleResult(); - }); - } - - @Entity(name = "Board") - @Table(name = "board") - public static class Board { - - @Id - @GeneratedValue - private Long id; - - private String name; - - //Only useful for the sake of seeing the queries being generated. - @OneToMany(mappedBy = "board") - private List topics = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getTopics() { - return topics; - } - } - - @Entity(name = "Topic") - @Table(name = "topic") - @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) - public static class Topic { - - @Id - @GeneratedValue - private Long id; - - private String title; - - private String owner; - - @Temporal(TemporalType.TIMESTAMP) - private Date createdOn = new Date(); - - @ManyToOne(fetch = FetchType.LAZY) - private Board board; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getOwner() { - return owner; - } - - public void setOwner(String owner) { - this.owner = owner; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public Board getBoard() { - return board; - } - - public void setBoard(Board board) { - this.board = board; - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post extends Topic { - - private String content; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - } - - @Entity(name = "Announcement") - @Table(name = "announcement") - public static class Announcement extends Topic { - - @Temporal(TemporalType.TIMESTAMP) - private Date validUntil; - - public Date getValidUntil() { - return validUntil; - } - - public void setValidUntil(Date validUntil) { - this.validUntil = validUntil; - } - } - - @Entity(name = "TopicStatistics") - @Table(name = "topic_statistics") - public static class TopicStatistics { - - @Id - @GeneratedValue - private Long id; - - @OneToOne - @JoinColumn(name = "id") - @MapsId - private Topic topic; - - private long views; - - public TopicStatistics() {} - - public TopicStatistics(Topic topic) { - this.topic = topic; - } - - public Long getId() { - return id; - } - - public Topic getTopic() { - return topic; - } - - public long getViews() { - return views; - } - - public void incrementViews() { - this.views++; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/Announcement.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/Announcement.java deleted file mode 100644 index adb6be773..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/Announcement.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.discriminator; - -import java.util.Date; -import javax.persistence.DiscriminatorValue; -import javax.persistence.Entity; -import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "announcement") -@DiscriminatorValue("2") -public class Announcement extends Topic { - - @Temporal(TemporalType.TIMESTAMP) - private Date validUntil; - - public Date getValidUntil() { - return validUntil; - } - - public void setValidUntil(Date validUntil) { - this.validUntil = validUntil; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/Board.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/Board.java deleted file mode 100644 index e4b11b3fd..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/Board.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.discriminator; - -import java.util.ArrayList; -import java.util.List; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.OneToMany; -import javax.persistence.Table; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "board") -public class Board { - - @Id - @GeneratedValue - private Long id; - - private String name; - - //Only useful for the sake of seeing the queries being generated. - @OneToMany(mappedBy = "board") - private List topics = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getTopics() { - return topics; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/IntegerDiscriminatorDefaultTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/IntegerDiscriminatorDefaultTest.java deleted file mode 100644 index 923c2bf85..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/IntegerDiscriminatorDefaultTest.java +++ /dev/null @@ -1,315 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.discriminator; - -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import javax.persistence.DiscriminatorColumn; -import javax.persistence.DiscriminatorType; -import javax.persistence.DiscriminatorValue; -import javax.persistence.Entity; -import javax.persistence.EntityManager; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Inheritance; -import javax.persistence.InheritanceType; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.MapsId; -import javax.persistence.OneToMany; -import javax.persistence.OneToOne; -import javax.persistence.PersistenceException; -import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; - -import org.hibernate.Session; - -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * @author Vlad Mihalcea - */ -public class IntegerDiscriminatorDefaultTest extends AbstractMySQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Board.class, - Topic.class, - Post.class, - Announcement.class, - TopicStatistics.class - }; - } - - @Test - public void test() { - Topic _topic = doInJPA(entityManager -> { - - addConsistecyTriggers( entityManager ); - - Board board = new Board(); - board.setName("Hibernate"); - - entityManager.persist(board); - - Post post = new Post(); - post.setOwner("John Doe"); - post.setTitle("Inheritance"); - post.setContent("Best practices"); - post.setBoard(board); - - entityManager.persist(post); - - Announcement announcement = new Announcement(); - announcement.setOwner("John Doe"); - announcement.setTitle("Release x.y.z.Final"); - announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); - announcement.setBoard(board); - - entityManager.persist(announcement); - - TopicStatistics postStatistics = new TopicStatistics(post); - postStatistics.incrementViews(); - entityManager.persist(postStatistics); - - TopicStatistics announcementStatistics = new TopicStatistics(announcement); - announcementStatistics.incrementViews(); - entityManager.persist(announcementStatistics); - - return post; - }); - - doInJPA(entityManager -> { - Board board = _topic.getBoard(); - LOGGER.info("Fetch Topics"); - List topics = entityManager - .createQuery("select t from Topic t where t.board = :board", Topic.class) - .setParameter("board", board) - .getResultList(); - - for ( Topic topic: topics ) { - LOGGER.info( "Found topic: {}", topic.getClass().getName() ); - } - }); - } - - private void addConsistecyTriggers(EntityManager entityManager) { - entityManager.unwrap(Session.class).doWork( connection -> { - try(Statement st = connection.createStatement()) { - st.executeUpdate( - "CREATE " + - "TRIGGER post_content_check BEFORE INSERT " + - "ON Topic " + - "FOR EACH ROW " + - "BEGIN " + - " IF NEW.topic_type_id = 1 " + - " THEN " + - " IF NEW.content IS NULL " + - " THEN " + - " signal sqlstate '45000' " + - " set message_text = 'Post content cannot be NULL'; " + - " END IF; " + - " END IF; " + - "END;" - ); - st.executeUpdate( - "CREATE " + - "TRIGGER announcement_validUntil_check BEFORE INSERT " + - "ON Topic " + - "FOR EACH ROW " + - "BEGIN " + - " IF NEW.topic_type_id = 2 " + - " THEN " + - " IF NEW.validUntil IS NULL " + - " THEN " + - " signal sqlstate '45000' " + - " set message_text = 'Announcement validUntil cannot be NULL'; " + - " END IF; " + - " END IF; " + - "END;" - ); - } - }); - } - @Entity(name = "Board") - @Table(name = "board") - public static class Board { - - @Id - @GeneratedValue - private Long id; - - private String name; - - //Only useful for the sake of seeing the queries being generated. - @OneToMany(mappedBy = "board") - private List topics = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getTopics() { - return topics; - } - } - - @Entity(name = "Topic") - @Table(name = "topic") - @Inheritance(strategy = InheritanceType.SINGLE_TABLE) - @DiscriminatorColumn( - discriminatorType = DiscriminatorType.INTEGER, - name = "topic_type_id", - columnDefinition = "TINYINT(1)" - ) - public static class Topic { - - @Id - @GeneratedValue - private Long id; - - private String title; - - private String owner; - - @Temporal(TemporalType.TIMESTAMP) - private Date createdOn = new Date(); - - @ManyToOne(fetch = FetchType.LAZY) - private Board board; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getOwner() { - return owner; - } - - public void setOwner(String owner) { - this.owner = owner; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public Board getBoard() { - return board; - } - - public void setBoard(Board board) { - this.board = board; - } - } - - @Entity(name = "Post") - @Table(name = "post") - @DiscriminatorValue("1") - public static class Post extends Topic { - - private String content; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - } - - @Entity(name = "Announcement") - @Table(name = "announcement") - @DiscriminatorValue("2") - public static class Announcement extends Topic { - - @Temporal(TemporalType.TIMESTAMP) - private Date validUntil; - - public Date getValidUntil() { - return validUntil; - } - - public void setValidUntil(Date validUntil) { - this.validUntil = validUntil; - } - } - - @Entity(name = "TopicStatistics") - @Table(name = "topic_statistics") - public static class TopicStatistics { - - @Id - @GeneratedValue - private Long id; - - @OneToOne - @JoinColumn(name = "id") - @MapsId - private Topic topic; - - private long views; - - public TopicStatistics() {} - - public TopicStatistics(Topic topic) { - this.topic = topic; - } - - public Long getId() { - return id; - } - - public Topic getTopic() { - return topic; - } - - public long getViews() { - return views; - } - - public void incrementViews() { - this.views++; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/IntegerDiscriminatorWithDescriptionTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/IntegerDiscriminatorWithDescriptionTest.java deleted file mode 100644 index 30c7d781b..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/IntegerDiscriminatorWithDescriptionTest.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.discriminator; - -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.List; -import javax.persistence.DiscriminatorValue; -import javax.persistence.EntityManager; - -import org.hibernate.Session; - -import org.junit.Assert; -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class IntegerDiscriminatorWithDescriptionTest - extends AbstractMySQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[]{ - Board.class, - TopicType.class, - Topic.class, - Post.class, - Announcement.class, - TopicStatistics.class - }; - } - - @Test - public void test() { - Topic _topic = doInJPA(entityManager -> { - - addConsistecyTriggers( entityManager ); - - for ( Class entityClass : entities() ) { - if ( Topic.class.isAssignableFrom( entityClass ) && !Topic.class.equals( entityClass ) ) { - DiscriminatorValue discriminatorValue = (DiscriminatorValue) entityClass.getAnnotation( DiscriminatorValue.class ); - Byte id = Byte.valueOf( discriminatorValue.value() ); - - TopicType topicType = new TopicType(); - topicType.setId( id ); - topicType.setName( entityClass.getName() ); - topicType.setDescription( String.format( "%s is a subclass of the Topic base class", entityClass.getSimpleName()) ); - - entityManager.persist( topicType ); - } - } - - Board board = new Board(); - board.setName("Hibernate"); - - entityManager.persist(board); - - Post post = new Post(); - post.setOwner("John Doe"); - post.setTitle("Inheritance"); - post.setContent("Best practices"); - post.setBoard(board); - - entityManager.persist(post); - - Announcement announcement = new Announcement(); - announcement.setOwner("John Doe"); - announcement.setTitle("Release x.y.z.Final"); - announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); - announcement.setBoard(board); - - entityManager.persist(announcement); - - TopicStatistics postStatistics = new TopicStatistics(post); - postStatistics.incrementViews(); - entityManager.persist(postStatistics); - - TopicStatistics announcementStatistics = new TopicStatistics(announcement); - announcementStatistics.incrementViews(); - entityManager.persist(announcementStatistics); - - return post; - }); - - doInJPA(entityManager -> { - Board board = _topic.getBoard(); - LOGGER.info("Fetch Topics"); - List topics = entityManager - .createQuery("select t from Topic t where t.board = :board", Topic.class) - .setParameter("board", board) - .getResultList(); - - for ( Topic topic: topics ) { - LOGGER.info( "Found topic: {}", topic.getType() ); - } - - List results = entityManager - .createNativeQuery( - "SELECT " + - " tt.name, " + - " t.id, " + - " t.createdOn, " + - " t.owner, " + - " t.title, " + - " t.content, " + - " t.validUntil, " + - " t.board_id " + - "FROM topic t " + - "INNER JOIN topic_type tt ON t.topic_type_id = tt.id " - ) - .getResultList(); - - assertEquals( 2, results.size()); - }); - - doInJPA(entityManager -> { - Board board = _topic.getBoard(); - LOGGER.info("Fetch Posts"); - List posts = entityManager - .createQuery( - "select p " + - "from Post p " + - "where p.board = :board", Post.class) - .setParameter("board", board) - .getResultList(); - - for ( Topic post: posts ) { - LOGGER.info( "Found post: {}", post.getType() ); - } - }); - } - - private void addConsistecyTriggers(EntityManager entityManager) { - entityManager.unwrap(Session.class).doWork( connection -> { - try(Statement st = connection.createStatement()) { - st.executeUpdate( - "CREATE " + - "TRIGGER post_content_check BEFORE INSERT " + - "ON Topic " + - "FOR EACH ROW " + - "BEGIN " + - " IF NEW.topic_type_id = 1 " + - " THEN " + - " IF NEW.content IS NULL " + - " THEN " + - " signal sqlstate '45000' " + - " set message_text = 'Post content cannot be NULL'; " + - " END IF; " + - " END IF; " + - "END;" - ); - st.executeUpdate( - "CREATE " + - "TRIGGER announcement_validUntil_check BEFORE INSERT " + - "ON Topic " + - "FOR EACH ROW " + - "BEGIN " + - " IF NEW.topic_type_id = 2 " + - " THEN " + - " IF NEW.validUntil IS NULL " + - " THEN " + - " signal sqlstate '45000' " + - " set message_text = 'Announcement validUntil cannot be NULL'; " + - " END IF; " + - " END IF; " + - "END;" - ); - } - }); - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/Post.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/Post.java deleted file mode 100644 index 4406b9e75..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/Post.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.discriminator; - -import javax.persistence.DiscriminatorValue; -import javax.persistence.Entity; -import javax.persistence.Table; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "post") -@DiscriminatorValue("1") -public class Post extends Topic { - - private String content; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/Topic.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/Topic.java deleted file mode 100644 index bfea1b7be..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/Topic.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.discriminator; - -import java.util.Date; -import javax.persistence.DiscriminatorColumn; -import javax.persistence.DiscriminatorType; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Inheritance; -import javax.persistence.InheritanceType; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "topic") -@Inheritance(strategy = InheritanceType.SINGLE_TABLE) -@DiscriminatorColumn( - discriminatorType = DiscriminatorType.INTEGER, - name = "topic_type_id", - columnDefinition = "TINYINT(1)" -) -public class Topic { - - @Id - @GeneratedValue - private Long id; - - private String title; - - private String owner; - - @Temporal(TemporalType.TIMESTAMP) - private Date createdOn = new Date(); - - @ManyToOne(fetch = FetchType.LAZY) - private Board board; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn( - name = "topic_type_id", - insertable = false, - updatable = false - ) - private TopicType type; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getOwner() { - return owner; - } - - public void setOwner(String owner) { - this.owner = owner; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public Board getBoard() { - return board; - } - - public void setBoard(Board board) { - this.board = board; - } - - TopicType getType() { - return type; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/TopicStatistics.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/TopicStatistics.java deleted file mode 100644 index d055c4723..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/TopicStatistics.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.discriminator; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.MapsId; -import javax.persistence.OneToOne; -import javax.persistence.Table; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "topic_statistics") -public class TopicStatistics { - - @Id - @GeneratedValue - private Long id; - - @OneToOne - @JoinColumn(name = "id") - @MapsId - private Topic topic; - - private long views; - - public TopicStatistics() { - } - - public TopicStatistics(Topic topic) { - this.topic = topic; - } - - public Long getId() { - return id; - } - - public Topic getTopic() { - return topic; - } - - public long getViews() { - return views; - } - - public void incrementViews() { - this.views++; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/BehaviorDrivenInheritanceTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/BehaviorDrivenInheritanceTest.java deleted file mode 100644 index 57c71926f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/BehaviorDrivenInheritanceTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.model.EmailNotification; -import com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.model.SmsNotification; -import com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.config.BehaviorDrivenInheritanceConfiguration; -import com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.service.NotificationService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.TransactionException; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -/** - * @author Vlad Mihalcea - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = BehaviorDrivenInheritanceConfiguration.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class BehaviorDrivenInheritanceTest { - - protected final Logger LOGGER = LoggerFactory.getLogger( getClass() ); - - @Autowired - private TransactionTemplate transactionTemplate; - - @PersistenceContext - private EntityManager entityManager; - - @Autowired - private NotificationService notificationService; - - @Test - public void test() { - try { - transactionTemplate.execute( (TransactionCallback) transactionStatus -> { - SmsNotification sms = new SmsNotification(); - sms.setPhoneNumber( "012-345-67890" ); - sms.setFirstName( "Vlad" ); - sms.setLastName( "Mihalcea" ); - entityManager.persist( sms ); - - EmailNotification email = new EmailNotification(); - email.setEmailAddress( "vlad@acme.com" ); - email.setFirstName( "Vlad" ); - email.setLastName( "Mihalcea" ); - - entityManager.persist( email ); - return null; - } ); - } - catch (TransactionException e) { - LOGGER.error( "Failure", e ); - } - - notificationService.sendCampaign( "Black Friday", "High-Performance Java Persistence is 40% OFF" ); - - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/config/BehaviorDrivenInheritanceConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/config/BehaviorDrivenInheritanceConfiguration.java deleted file mode 100644 index 5bf6c324a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/config/BehaviorDrivenInheritanceConfiguration.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.config; - -import java.util.Properties; -import javax.persistence.EntityManagerFactory; -import javax.sql.DataSource; - -import org.hibernate.jpa.HibernatePersistenceProvider; - -import com.vladmihalcea.book.hpjp.util.DataSourceProxyType; -import com.vladmihalcea.book.hpjp.util.logging.InlineQueryLogEntryCreator; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import net.ttddyy.dsproxy.listener.SLF4JQueryLoggingListener; -import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.EnableAspectJAutoProxy; -import org.springframework.context.annotation.PropertySource; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.JpaVendorAdapter; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.transaction.support.TransactionTemplate; - -/** - * @author Vlad Mihalcea - */ -@Configuration -@PropertySource({ "/META-INF/jdbc-mysql.properties" }) -@ComponentScan(basePackages = "com.vladmihalcea.book.hpjp.hibernate.inheritance.spring") -@EnableTransactionManagement -@EnableAspectJAutoProxy -public class BehaviorDrivenInheritanceConfiguration { - - public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); - - @Value("${jdbc.dataSourceClassName}") - private String dataSourceClassName; - - @Value("${jdbc.url}") - private String jdbcUrl; - - @Value("${jdbc.username}") - private String jdbcUser; - - @Value("${jdbc.password}") - private String jdbcPassword; - - @Value("${hibernate.dialect}") - private String hibernateDialect; - - @Bean - public static PropertySourcesPlaceholderConfigurer properties() { - return new PropertySourcesPlaceholderConfigurer(); - } - - @Bean(destroyMethod = "close") - public DataSource actualDataSource() { - Properties driverProperties = new Properties(); - driverProperties.setProperty( "url", jdbcUrl ); - driverProperties.setProperty( "user", jdbcUser ); - driverProperties.setProperty( "password", jdbcPassword ); - - Properties properties = new Properties(); - properties.put( "dataSourceClassName", dataSourceClassName ); - properties.put( "dataSourceProperties", driverProperties ); - properties.setProperty( "maximumPoolSize", String.valueOf( 3 ) ); - return new HikariDataSource( new HikariConfig( properties ) ); - } - - @Bean - public DataSource dataSource() { - SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); - loggingListener.setQueryLogEntryCreator( new InlineQueryLogEntryCreator() ); - return ProxyDataSourceBuilder - .create( actualDataSource() ) - .name( DATA_SOURCE_PROXY_NAME ) - .listener( loggingListener ) - .build(); - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactory() { - LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - localContainerEntityManagerFactoryBean.setPersistenceUnitName( getClass().getSimpleName() ); - localContainerEntityManagerFactoryBean.setPersistenceProvider( new HibernatePersistenceProvider() ); - localContainerEntityManagerFactoryBean.setDataSource( dataSource() ); - localContainerEntityManagerFactoryBean.setPackagesToScan( packagesToScan() ); - - JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); - localContainerEntityManagerFactoryBean.setJpaVendorAdapter( vendorAdapter ); - localContainerEntityManagerFactoryBean.setJpaProperties( additionalProperties() ); - return localContainerEntityManagerFactoryBean; - } - - @Bean - public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { - JpaTransactionManager transactionManager = new JpaTransactionManager(); - transactionManager.setEntityManagerFactory( entityManagerFactory ); - return transactionManager; - } - - @Bean - public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { - return new TransactionTemplate( transactionManager( entityManagerFactory ) ); - } - - protected Properties additionalProperties() { - Properties properties = new Properties(); - properties.setProperty( "hibernate.dialect", hibernateDialect ); - properties.setProperty( "hibernate.hbm2ddl.auto", "create-drop" ); - return properties; - } - - protected String[] packagesToScan() { - return new String[] { - "com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.model" - }; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/dao/GenericDAO.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/dao/GenericDAO.java deleted file mode 100644 index e01b49fe0..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/dao/GenericDAO.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.dao; - -import java.io.Serializable; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public interface GenericDAO { - - T findById(ID id); - - List findAll(); - - T persist(T entity); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/dao/GenericDAOImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/dao/GenericDAOImpl.java deleted file mode 100644 index 55d823c2f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/dao/GenericDAOImpl.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.dao; - -import java.io.Serializable; -import java.util.List; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; - -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -/** - * @author Vlad Mihalcea - */ -@Repository -@Transactional -public abstract class GenericDAOImpl implements GenericDAO { - - @PersistenceContext - private EntityManager entityManager; - - private final Class entityClass; - - protected EntityManager getEntityManager() { - return entityManager; - } - - protected GenericDAOImpl(Class entityClass) { - this.entityClass = entityClass; - } - - @Override - public T findById(ID id) { - return entityManager.find( entityClass, id ); - } - - @Override - public List findAll() { - CriteriaBuilder builder = entityManager.getCriteriaBuilder(); - CriteriaQuery criteria = builder.createQuery( entityClass ); - criteria.from( entityClass ); - - return entityManager.createQuery( criteria ).getResultList(); - } - - @Override - public T persist(T entity) { - entityManager.persist( entity ); - return entity; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/dao/NotificationDAO.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/dao/NotificationDAO.java deleted file mode 100644 index 1debee5bf..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/dao/NotificationDAO.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.dao; - -import com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.model.Notification; - -/** - * @author Vlad Mihalcea - */ -public interface NotificationDAO extends GenericDAO { - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/dao/NotificationDAOImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/dao/NotificationDAOImpl.java deleted file mode 100644 index 04674a0b8..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/dao/NotificationDAOImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.dao; - -import com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.model.Notification; -import org.springframework.stereotype.Repository; - -/** - * @author Vlad Mihalcea - */ -@Repository -public class NotificationDAOImpl extends GenericDAOImpl implements NotificationDAO { - - protected NotificationDAOImpl() { - super( Notification.class ); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/model/EmailNotification.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/model/EmailNotification.java deleted file mode 100644 index 13c68d9ab..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/model/EmailNotification.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.model; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Table; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "email_notification") -public class EmailNotification extends Notification { - - @Column(name = "email_address", nullable = false) - private String emailAddress; - - public String getEmailAddress() { - return emailAddress; - } - - public void setEmailAddress(String emailAddress) { - this.emailAddress = emailAddress; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/model/Notification.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/model/Notification.java deleted file mode 100644 index 571f929c0..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/model/Notification.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.model; - -import java.util.Date; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Inheritance; -import javax.persistence.InheritanceType; -import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; - -import org.hibernate.annotations.CreationTimestamp; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "notification") -@Inheritance(strategy = InheritanceType.JOINED) -public class Notification { - - @Id - @GeneratedValue - private Long id; - - @Column(name = "first_name") - private String firstName; - - @Column(name = "last_name") - private String lastName; - - @Temporal( TemporalType.TIMESTAMP ) - @CreationTimestamp - @Column(name = "created_on") - private Date createdOn; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/model/SmsNotification.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/model/SmsNotification.java deleted file mode 100644 index 023fca286..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/model/SmsNotification.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.model; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Table; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "sms_notification") -public class SmsNotification extends Notification { - - @Column(name = "phone_number", nullable = false) - private String phoneNumber; - - public String getPhoneNumber() { - return phoneNumber; - } - - public void setPhoneNumber(String phoneNumber) { - this.phoneNumber = phoneNumber; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/NotificationService.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/NotificationService.java deleted file mode 100644 index 68b2d1c30..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/NotificationService.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.service; - -/** - * @author Vlad Mihalcea - */ -public interface NotificationService { - - void sendCampaign(String name, String message); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/NotificationServiceImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/NotificationServiceImpl.java deleted file mode 100644 index 5b6f2fb36..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/NotificationServiceImpl.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.service; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.PostConstruct; -import javax.transaction.Transactional; - -import com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.model.Notification; -import com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.dao.NotificationDAO; -import com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.service.sender.NotificationSender; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -/** - * @author Vlad Mihalcea - */ -@Service -public class NotificationServiceImpl - implements NotificationService { - - @Autowired - private NotificationDAO notificationDAO; - - @Autowired - private List notificationSenders; - - private Map, NotificationSender> - notificationSenderMap = new HashMap<>(); - - @PostConstruct - @SuppressWarnings( "unchecked" ) - public void init() { - for ( NotificationSender notificationSender : notificationSenders ) { - notificationSenderMap.put( - notificationSender.appliesTo(), - notificationSender - ); - } - } - - @Override - @Transactional - @SuppressWarnings( "unchecked" ) - public void sendCampaign(String name, String message) { - List notifications = notificationDAO.findAll(); - - for ( Notification notification : notifications ) { - notificationSenderMap - .get( notification.getClass() ) - .send( notification ); - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/sender/EmailNotificationSender.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/sender/EmailNotificationSender.java deleted file mode 100644 index 0ca31cfc6..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/sender/EmailNotificationSender.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.service.sender; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.model.EmailNotification; -import com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.model.SmsNotification; -import org.springframework.stereotype.Component; - -/** - * @author Vlad Mihalcea - */ -@Component -public class EmailNotificationSender - implements NotificationSender { - - protected final Logger LOGGER = LoggerFactory.getLogger( getClass() ); - - @Override - public Class appliesTo() { - return EmailNotification.class; - } - - @Override - public void send(EmailNotification notification) { - LOGGER.info( - "Send Email to {} {} via address: {}", - notification.getFirstName(), - notification.getLastName(), - notification.getEmailAddress() - ); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/sender/NotificationSender.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/sender/NotificationSender.java deleted file mode 100644 index 3bb905b96..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/sender/NotificationSender.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.service.sender; - -import com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.model.Notification; - -/** - * @author Vlad Mihalcea - */ -public interface NotificationSender { - - Class appliesTo(); - - void send(N notification); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/sender/SmsNotificationSender.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/sender/SmsNotificationSender.java deleted file mode 100644 index 658cb9a64..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/spring/service/sender/SmsNotificationSender.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.service.sender; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.vladmihalcea.book.hpjp.hibernate.inheritance.spring.model.SmsNotification; -import org.springframework.stereotype.Component; - -/** - * @author Vlad Mihalcea - */ -@Component -public class SmsNotificationSender implements NotificationSender { - - protected final Logger LOGGER = LoggerFactory.getLogger( getClass() ); - - @Override - public Class appliesTo() { - return SmsNotification.class; - } - - @Override - public void send(SmsNotification notification) { - LOGGER.info( "Send SMS to {} {} via phone number: {}", - notification.getFirstName(), - notification.getLastName(), - notification.getPhoneNumber() - ); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/jmx/JmxTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/jmx/JmxTest.java deleted file mode 100644 index 8960a7a0b..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/jmx/JmxTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.jmx; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.cfg.AvailableSettings; -import org.junit.Test; - -import javax.persistence.Entity; -import javax.persistence.Id; -import java.util.Properties; - -/** - * @author Vlad Mihalcea - */ -public class JmxTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Person.class, - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put(AvailableSettings.JMX_ENABLED, Boolean.TRUE.toString()); - return properties; - } - - @Test - public void test() { - doInJPA(entityManager -> { - Person vlad = new Person(); - vlad.id = 1L; - entityManager.persist(vlad); - }); - } - - @Entity(name = "Person") - public static class Person { - - @Id - private Long id; - - private String firstName; - - private String lastName; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/listener/Updatable.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/listener/Updatable.java deleted file mode 100644 index 6dc961a7e..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/listener/Updatable.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.listener; - -import java.util.Date; - -/** - * @author Vlad Mihalcea - */ -public interface Updatable { - - void setTimestamp(Date timestamp); - - Date getTimestamp(); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/listener/UpdatableListener.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/listener/UpdatableListener.java deleted file mode 100644 index acfb769f2..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/listener/UpdatableListener.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.listener; - -import javax.persistence.PrePersist; -import javax.persistence.PreUpdate; -import java.util.Date; - -/** - * @author Vlad Mihalcea - */ -public class UpdatableListener { - - @PrePersist - @PreUpdate - private void setCurrentTimestamp(Object entity) { - if(entity instanceof Updatable) { - Updatable updatable = (Updatable) entity; - updatable.setTimestamp(new Date()); - } - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/DataSourceProxyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/DataSourceProxyTest.java deleted file mode 100644 index 720f198df..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/DataSourceProxyTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.logging; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.DataSourceProxyType; -import org.junit.Test; - -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; - -/** - * @author Vlad Mihalcea - */ -public class DataSourceProxyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Override - protected DataSourceProxyType dataSourceProxyType() { - return DataSourceProxyType.DATA_SOURCE_PROXY; - } - - @Test - public void test() { - doInJPA(entityManager -> { - Post post = new Post(); - post.id = 1L; - post.title = "Post it"; - entityManager.persist(post); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/P6spyTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/P6spyTest.java deleted file mode 100644 index 32b65f72f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/P6spyTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.logging; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.DataSourceProxyType; -import org.junit.Test; - -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; - -/** - * @author Vlad Mihalcea - */ -public class P6spyTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Override - protected DataSourceProxyType dataSourceProxyType() { - return DataSourceProxyType.P6SPY; - } - - @Test - public void test() { - doInJPA(entityManager -> { - Post post = new Post(); - post.id = 1L; - post.title = "Post it"; - entityManager.persist(post); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/validator/SQLStatementCountValidatorTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/validator/SQLStatementCountValidatorTest.java deleted file mode 100644 index edce0f9dc..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/validator/SQLStatementCountValidatorTest.java +++ /dev/null @@ -1,175 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.logging.validator; - -import com.vladmihalcea.book.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; -import com.vladmihalcea.book.hpjp.hibernate.logging.validator.sql.exception.SQLStatementCountMismatchException; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -public class SQLStatementCountValidatorTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - }; - } - - @Test - public void testValidate() { - doInJPA(entityManager -> { - Post post1 = new Post(1L); - post1.setTitle("Post one"); - - Post post2 = new Post(2L); - post2.setTitle("Post two"); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("Good"); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Excellent"); - - post1.addComment(comment1); - post2.addComment(comment2); - entityManager.persist(post1); - entityManager.persist(post2); - }); - - doInJPA(entityManager -> { - LOGGER.info("Detect N+1"); - try { - SQLStatementCountValidator.reset(); - List postComments = entityManager.createQuery( - "select pc " + - "from PostComment pc", PostComment.class) - .getResultList(); - - for(PostComment postComment : postComments) { - assertNotNull(postComment.getPost().getTitle()); - } - SQLStatementCountValidator.assertSelectCount(1); - } catch (SQLStatementCountMismatchException e) { - assertEquals(3, e.getRecorded()); - } - }); - - doInJPA(entityManager -> { - LOGGER.info("Join fetch to prevent N+1"); - SQLStatementCountValidator.reset(); - List postComments = entityManager - .createQuery("select pc from PostComment pc join fetch pc.post", PostComment.class) - .getResultList(); - - for(PostComment postComment : postComments) { - assertNotNull(postComment.getPost()); - } - - SQLStatementCountValidator.assertSelectCount(1); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Post() {} - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/validator/sql/exception/SQLStatementCountMismatchException.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/validator/sql/exception/SQLStatementCountMismatchException.java deleted file mode 100644 index cca68a516..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/validator/sql/exception/SQLStatementCountMismatchException.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.logging.validator.sql.exception; - -/** - * SQLStatementCountMismatchException - Thrown whenever there is a mismatch between expected statements count and - * the ones being executed. - * - * @author Vlad Mihalcea - */ -public class SQLStatementCountMismatchException extends RuntimeException { - - private final int expected; - private final int recorded; - - public SQLStatementCountMismatchException(int expected, int recorded) { - super(String.format("Expected %d statement(s) but recorded %d instead!", - expected, recorded)); - this.expected = expected; - this.recorded = recorded; - } - - public int getExpected() { - return expected; - } - - public int getRecorded() { - return recorded; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/DefaultUpdateTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/DefaultUpdateTest.java deleted file mode 100644 index dc2092784..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/DefaultUpdateTest.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping; - -import java.sql.Timestamp; -import java.time.format.DateTimeFormatter; -import java.util.Properties; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.persistence.Transient; - -import org.hibernate.annotations.DynamicUpdate; -import org.hibernate.cfg.AvailableSettings; - -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; - -/** - * @author Vlad Mihalcea - */ -public class DefaultUpdateTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.jdbc.batch_size", "5"); - properties.put("hibernate.order_inserts", "true"); - properties.put("hibernate.order_updates", "true"); - properties.put("hibernate.jdbc.batch_versioned_data", "true"); - return properties; - } - - @Test - public void test() { - - doInJPA(entityManager -> { - Post post1 = new Post(); - post1.setId(1L); - post1.setTitle("High-Performance Java Persistence"); - entityManager.persist(post1); - - Post post2 = new Post(); - post2.setId(2L); - post2.setTitle("Spring Boot Buch"); - entityManager.persist(post2); - }); - doInJPA(entityManager -> { - Post post1 = entityManager.find(Post.class, 1L); - post1.setTitle("High-Performance Java Persistence 2nd Edition"); - - Post post2 = entityManager.find(Post.class, 2L); - post2.setScore(12); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - private long score; - - @Column(name = "created_on", nullable = false, updatable = false) - private Timestamp createdOn; - - @Transient - private String creationTimestamp; - - public Post() { - this.createdOn = new Timestamp(System.currentTimeMillis()); - } - - public String getCreationTimestamp() { - if(creationTimestamp == null) { - creationTimestamp = DateTimeFormatter.ISO_DATE_TIME.format( - createdOn.toLocalDateTime() - ); - } - return creationTimestamp; - } - - @Override - public String toString() { - return String.format( - "Post{\n" + - " id=%d\n" + - " title='%s'\n" + - " score=%d\n" + - " creationTimestamp='%s'\n" + - '}', id, title, score, getCreationTimestamp() - ); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public long getScore() { - return score; - } - - public void setScore(long score) { - this.score = score; - } - - public Timestamp getCreatedOn() { - return createdOn; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/DynamicUpdateTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/DynamicUpdateTest.java deleted file mode 100644 index aa2d94596..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/DynamicUpdateTest.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.DynamicUpdate; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.Timestamp; -import java.time.format.DateTimeFormatter; -import java.util.Properties; - -/** - * @author Vlad Mihalcea - */ -public class DynamicUpdateTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.jdbc.batch_size", "5"); - properties.put("hibernate.order_inserts", "true"); - properties.put("hibernate.order_updates", "true"); - properties.put("hibernate.jdbc.batch_versioned_data", "true"); - return properties; - } - - @Test - public void test() { - - doInJPA(entityManager -> { - Post post1 = new Post(); - post1.setId(1L); - post1.setTitle("High-Performance Java Persistence"); - entityManager.persist(post1); - - Post post2 = new Post(); - post2.setId(2L); - post2.setTitle("Spring Boot Buch"); - entityManager.persist(post2); - }); - doInJPA(entityManager -> { - Post post1 = entityManager.find(Post.class, 1L); - post1.setTitle("High-Performance Java Persistence 2nd Edition"); - - Post post2 = entityManager.find(Post.class, 2L); - post2.setScore(12); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - @DynamicUpdate - public static class Post { - - @Id - private Long id; - - private String title; - - private long score; - - @Column(name = "created_on", nullable = false, updatable = false) - private Timestamp createdOn; - - @Transient - private String creationTimestamp; - - public Post() { - this.createdOn = new Timestamp(System.currentTimeMillis()); - } - - public String getCreationTimestamp() { - if(creationTimestamp == null) { - creationTimestamp = DateTimeFormatter.ISO_DATE_TIME.format( - createdOn.toLocalDateTime() - ); - } - return creationTimestamp; - } - - @Override - public String toString() { - return String.format( - "Post{\n" + - " id=%d\n" + - " title='%s'\n" + - " score=%d\n" + - " creationTimestamp='%s'\n" + - '}', id, title, score, getCreationTimestamp() - ); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public long getScore() { - return score; - } - - public void setScore(long score) { - this.score = score; - } - - public Timestamp getCreatedOn() { - return createdOn; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/LatestChildJoinFormulaTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/LatestChildJoinFormulaTest.java deleted file mode 100644 index 8afcb8980..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/LatestChildJoinFormulaTest.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.annotations.JoinFormula; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.Date; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -/** - * @author Vlad Mihalcea - */ -public class LatestChildJoinFormulaTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class - }; - } - - @Test - public void test() { - - doInJPA( entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - assertNull(post.getLatestComment()); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setPost(post); - comment1.setCreatedOn(Timestamp.valueOf( - LocalDateTime.of(2016, 11, 2, 12, 33, 14) - )); - comment1.setReview("Woohoo!"); - entityManager.persist(comment1); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setPost(post); - comment2.setCreatedOn(Timestamp.valueOf( - LocalDateTime.of(2016, 11, 2, 15, 45, 58) - )); - comment2.setReview("Finally!"); - entityManager.persist(comment2); - - PostComment comment3 = new PostComment(); - comment3.setId(3L); - comment3.setPost(post); - comment3.setCreatedOn(Timestamp.valueOf( - LocalDateTime.of(2017, 2, 16, 16, 10, 21) - )); - comment3.setReview("Awesome!"); - entityManager.persist(comment3); - } ); - - doInJPA( entityManager -> { - Post post = entityManager.find(Post.class, 1L); - PostComment latestComment = post.getLatestComment(); - - assertEquals("Awesome!", latestComment.getReview()); - } ); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinFormula("(" + - "SELECT pc.id " + - "FROM post_comment pc " + - "WHERE pc.post_id = id " + - "ORDER BY pc.created_on DESC " + - "LIMIT 1" + - ")") - private PostComment latestComment; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public PostComment getLatestComment() { - return latestComment; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - @Column(name = "created_on") - @Temporal(TemporalType.TIMESTAMP) - private Date createdOn; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/calculated/JpaCalculatedPostLoadTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/calculated/JpaCalculatedPostLoadTest.java deleted file mode 100644 index 8a9b8ebac..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/calculated/JpaCalculatedPostLoadTest.java +++ /dev/null @@ -1,160 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping.calculated; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import java.math.BigDecimal; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class JpaCalculatedPostLoadTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Account.class, - User.class - }; - } - - @Test - public void test() { - doInJPA(entityManager -> { - User user = new User(); - user.setId(1L); - user.setFirstName("John"); - user.setFirstName("Doe"); - - entityManager.persist(user); - - Account account = new Account( - 1L, - user, - "ABC123", - 12345L, - 6.7, - Timestamp.valueOf( - LocalDateTime.now().minusMonths(3) - ) - ); - - entityManager.persist(account); - }); - doInJPA(entityManager -> { - Account account = entityManager.find(Account.class, 1L); - - assertEquals(123.45D, account.getDollars(), 0.001); - assertEquals(207L, account.getInterestCents()); - assertEquals(2.07D, account.getInterestDollars(), 0.001); - }); - } - - @Entity(name = "Account") - @Table(name = "account") - public static class Account { - - @Id - private Long id; - - @ManyToOne - private User owner; - - private String iban; - - private long cents; - - private double interestRate; - - private Timestamp createdOn; - - @Transient - private double dollars; - - @Transient - private long interestCents; - - @Transient - private double interestDollars; - - public Account() { - } - - public Account(Long id, User owner, String iban, long cents, double interestRate, Timestamp createdOn) { - this.id = id; - this.owner = owner; - this.iban = iban; - this.cents = cents; - this.interestRate = interestRate; - this.createdOn = createdOn; - } - - @PostLoad - private void postLoad() { - this.dollars = cents / 100D; - - long months = createdOn.toLocalDateTime().until(LocalDateTime.now(), ChronoUnit.MONTHS); - double interestUnrounded = ( ( interestRate / 100D ) * cents * months ) / 12; - this.interestCents = BigDecimal.valueOf(interestUnrounded).setScale(0, BigDecimal.ROUND_HALF_EVEN).longValue(); - - this.interestDollars = interestCents / 100D; - } - - @Transient - public double getDollars() { - return dollars; - } - - @Transient - public long getInterestCents() { - return interestCents; - } - - @Transient - public double getInterestDollars() { - return interestDollars; - } - } - - @Entity(name = "User") - @Table(name = "`user`") - public static class User { - - @Id - private Long id; - - private String firstName; - - private String lastName; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/encrypt/EncryptTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/encrypt/EncryptTest.java deleted file mode 100644 index b4004970a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/encrypt/EncryptTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping.encrypt; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.annotations.ColumnTransformer; -import org.junit.Test; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class EncryptTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Vault.class - }; - } - - @Test - public void test() { - doInJPA( entityManager -> { - Vault user = new Vault(); - user.setId(1L); - user.setStorage("my_secret_key"); - - entityManager.persist(user); - - } ); - doInJPA( entityManager -> { - String encryptedStorage = (String) entityManager.createNativeQuery( - "select encode(storage, 'base64') " + - "from Vault " + - "where id = :id") - .setParameter("id", 1L) - .getSingleResult(); - - LOGGER.info("Encoded storage: {}", encryptedStorage); - } ); - doInJPA( entityManager -> { - Vault vault = entityManager.find( Vault.class, 1L ); - assertEquals("my_secret_key", vault.getStorage()); - - vault.setStorage("another_secret_key"); - } ); - - doInJPA( entityManager -> { - Vault vault = entityManager.find( Vault.class, 1L ); - assertEquals("another_secret_key", vault.getStorage()); - } ); - } - - @Entity(name = "Vault") - public static class Vault { - - @Id - private Long id; - - @ColumnTransformer( - read = "pgp_sym_decrypt(" + - " storage, " + - " current_setting('encrypt.key')" + - ")", - write = "pgp_sym_encrypt( " + - " ?, " + - " current_setting('encrypt.key')" + - ") " - ) - @Column(columnDefinition = "bytea") - private String storage; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getStorage() { - return storage; - } - - public void setStorage(String storage) { - this.storage = storage; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/softdelete/SoftDeleteTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/softdelete/SoftDeleteTest.java deleted file mode 100644 index 725615667..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/softdelete/SoftDeleteTest.java +++ /dev/null @@ -1,457 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping.softdelete; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import org.hibernate.annotations.Loader; -import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.*; - -/** - * @author Vlad Mihalcea - */ -public class SoftDeleteTest extends AbstractMySQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostDetails.class, - PostComment.class, - Tag.class - }; - } - - @Override - public void init() { - super.init(); - - doInJPA( entityManager -> { - Tag javaTag = new Tag(); - javaTag.setId("Java"); - entityManager.persist(javaTag); - - Tag jpaTag = new Tag(); - jpaTag.setId("JPA"); - entityManager.persist(jpaTag); - - Tag hibernateTag = new Tag(); - hibernateTag.setId("Hibernate"); - entityManager.persist(hibernateTag); - - Tag miscTag = new Tag(); - miscTag.setId("Misc"); - entityManager.persist(miscTag); - } ); - } - - @Test - public void testRemoveTag() { - doInJPA( entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - entityManager.persist(post); - - post.addTag(entityManager.getReference(Tag.class, "Java")); - post.addTag(entityManager.getReference(Tag.class, "Hibernate")); - post.addTag(entityManager.getReference(Tag.class, "Misc")); - } ); - - doInJPA( entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(3, post.getTags().size()); - } ); - - doInJPA( entityManager -> { - Tag miscTag = entityManager.getReference(Tag.class, "Misc"); - entityManager.remove(miscTag); - } ); - - doInJPA( entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getTags().size()); - } ); - - doInJPA( entityManager -> { - //That would not work without @Loader(namedQuery = "findTagById") - assertNull(entityManager.find(Tag.class, "Misc")); - } ); - - doInJPA( entityManager -> { - List tags = entityManager.createQuery("select t from Tag t", Tag.class).getResultList(); - //That would not work without @Where(clause = "deleted = false") - assertEquals(3, tags.size()); - } ); - } - - @Test - public void testRemovePostDetails() { - doInJPA( entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - PostDetails postDetails = new PostDetails(); - postDetails.setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2016, 11, 2, 12, 0, 0))); - post.addDetails(postDetails); - - entityManager.persist(post); - - post.addTag(entityManager.getReference(Tag.class, "Java")); - post.addTag(entityManager.getReference(Tag.class, "Hibernate")); - post.addTag(entityManager.getReference(Tag.class, "Misc")); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("Great!"); - post.addComment(comment1); - - PostComment comment2= new PostComment(); - comment2.setId(2L); - comment2.setReview("To read"); - post.addComment(comment2); - } ); - - doInJPA( entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertNotNull(post.getDetails()); - post.removeDetails(); - } ); - - doInJPA( entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertNull(post.getDetails()); - } ); - - doInJPA( entityManager -> { - assertNull(entityManager.find(PostDetails.class, 1L)); - } ); - } - - @Test - public void testRemovePostComment() { - doInJPA( entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - PostDetails postDetails = new PostDetails(); - postDetails.setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2016, 11, 2, 12, 0, 0))); - post.addDetails(postDetails); - - entityManager.persist(post); - - post.addTag(entityManager.getReference(Tag.class, "Java")); - post.addTag(entityManager.getReference(Tag.class, "Hibernate")); - post.addTag(entityManager.getReference(Tag.class, "Misc")); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("Great!"); - post.addComment(comment1); - - PostComment comment2= new PostComment(); - comment2.setId(2L); - comment2.setReview("To read"); - post.addComment(comment2); - } ); - - doInJPA( entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(2, post.getComments().size()); - assertNotNull(entityManager.find(PostComment.class, 2L)); - post.removeComment(post.getComments().get(1)); - } ); - - doInJPA( entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(1, post.getComments().size()); - assertNull(entityManager.find(PostComment.class, 2L)); - } ); - } - - @Test - public void testRemoveAndFindPostComment() { - doInJPA( entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("Great!"); - post.addComment(comment1); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Excellent!"); - post.addComment(comment2); - } ); - doInJPA( entityManager -> { - Post post = entityManager.find(Post.class, 1L); - post.removeComment(post.getComments().get(0)); - } ); - doInJPA( entityManager -> { - Post post = entityManager.find(Post.class, 1L); - assertEquals(1, post.getComments().size()); - } ); - } - - @Entity(name = "Post") - @Table(name = "post") - @SQLDelete(sql = - "UPDATE post " + - "SET deleted = true " + - "WHERE id = ?") - @Loader(namedQuery = "findPostById") - @NamedQuery(name = "findPostById", query = - "SELECT p " + - "FROM Post p " + - "WHERE " + - " p.id = ? AND " + - " p.deleted = false") - @Where(clause = "deleted = false") - public static class Post extends BaseEntity { - - @Id - private Long id; - - private String title; - - @OneToMany( - mappedBy = "post", - cascade = CascadeType.ALL, - orphanRemoval = true - ) - private List comments = new ArrayList<>(); - - @OneToOne( - mappedBy = "post", - cascade = CascadeType.ALL, - orphanRemoval = true, - fetch = FetchType.LAZY - ) - private PostDetails details; - - @ManyToMany - @JoinTable( - name = "post_tag", - joinColumns = @JoinColumn(name = "post_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id") - ) - private List tags = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public PostDetails getDetails() { - return details; - } - - public List getTags() { - return tags; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - - public void removeComment(PostComment comment) { - comments.remove(comment); - comment.setPost(null); - } - - public void addDetails(PostDetails details) { - this.details = details; - details.setPost(this); - } - - public void removeDetails() { - this.details.setPost(null); - this.details = null; - } - - public void addTag(Tag tag) { - tags.add(tag); - } - } - - @Entity(name = "PostDetails") - @Table(name = "post_details") - @SQLDelete(sql = - "UPDATE post_details " + - "SET deleted = true " + - "WHERE id = ?") - @Loader(namedQuery = "findPostDetailsById") - @NamedQuery(name = "findPostDetailsById", query = - "SELECT pd " + - "FROM PostDetails pd " + - "WHERE " + - " pd.id = ? AND " + - " pd.deleted = false") - @Where(clause = "deleted = false") - public static class PostDetails extends BaseEntity { - - @Id - private Long id; - - @Column(name = "created_on") - private Date createdOn; - - @Column(name = "created_by") - private String createdBy; - - public PostDetails() { - createdOn = new Date(); - } - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") - @MapsId - private Post post; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @SQLDelete(sql = - "UPDATE post_comment " + - "SET deleted = true " + - "WHERE id = ?") - @Loader(namedQuery = "findPostCommentById") - @NamedQuery(name = "findPostCommentById", query = - "SELECT pc " + - "from PostComment pc " + - "WHERE " + - " pc.id = ? AND " + - " pc.deleted = false") - @Where(clause = "deleted = false") - public static class PostComment extends BaseEntity { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } - - @Entity(name = "Tag") - @Table(name = "tag") - @SQLDelete(sql = - "UPDATE tag " + - "SET deleted = true " + - "WHERE id = ?") - @Loader(namedQuery = "findTagById") - @NamedQuery(name = "findTagById", query = - "SELECT t " + - "FROM Tag t " + - "WHERE " + - " t.id = ? AND " + - " t.deleted = false") - @Where(clause = "deleted = false") - public static class Tag extends BaseEntity { - - @Id - private String id; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - } - - @MappedSuperclass - public static abstract class BaseEntity { - - private boolean deleted; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/softdelete/SoftDeleteVersionTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/softdelete/SoftDeleteVersionTest.java deleted file mode 100644 index 1ff9b808d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/softdelete/SoftDeleteVersionTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping.softdelete; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.annotations.Loader; -import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; -import org.junit.Test; - -import javax.persistence.*; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -/** - * @author Vlad Mihalcea - */ -public class SoftDeleteVersionTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Tag.class - }; - } - - @Override - public void init() { - super.init(); - - doInJPA( entityManager -> { - Tag javaTag = new Tag(); - javaTag.setId("Java"); - entityManager.persist(javaTag); - - Tag jpaTag = new Tag(); - jpaTag.setId("JPA"); - entityManager.persist(jpaTag); - - Tag hibernateTag = new Tag(); - hibernateTag.setId("Hibernate"); - entityManager.persist(hibernateTag); - - Tag miscTag = new Tag(); - miscTag.setId("Misc"); - entityManager.persist(miscTag); - } ); - } - - @Test - public void testRemoveTag() { - - doInJPA( entityManager -> { - Tag miscTag = entityManager.getReference(Tag.class, "Misc"); - entityManager.remove(miscTag); - } ); - - doInJPA( entityManager -> { - //That would not work without @Loader(namedQuery = "findTagById") - assertNull(entityManager.find(Tag.class, "Misc")); - } ); - - doInJPA( entityManager -> { - List tags = entityManager.createQuery("select t from Tag t", Tag.class).getResultList(); - //That would not work without @Where(clause = "deleted = false") - assertEquals(3, tags.size()); - } ); - } - - @Entity(name = "Tag") - @Table(name = "tag") - @SQLDelete(sql = - "UPDATE tag " + - "SET deleted = true " + - "WHERE id = ? and version = ?") - @Loader(namedQuery = "findTagById") - @NamedQuery(name = "findTagById", query = - "SELECT t " + - "FROM Tag t " + - "WHERE " + - " t.id = ?1 AND " + - " t.deleted = false") - @Where(clause = "deleted = false") - public static class Tag extends BaseEntity { - - @Id - private String id; - - @Version - private int version; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - } - - @MappedSuperclass - public static abstract class BaseEntity { - - private boolean deleted; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/metadata/MetadataTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/metadata/MetadataTest.java deleted file mode 100644 index bca49e56a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/metadata/MetadataTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.metadata; - -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import org.hibernate.boot.Metadata; -import org.hibernate.boot.model.relational.Database; -import org.hibernate.boot.model.relational.Namespace; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.integrator.spi.Integrator; -import org.hibernate.mapping.Table; -import org.hibernate.service.spi.SessionFactoryServiceRegistry; - -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; - -/** - * @author Vlad Mihalcea - */ -public class MetadataTest extends AbstractMySQLIntegrationTest { - - public static class MetadataExtractorIntegrator implements org.hibernate.integrator.spi.Integrator { - - public static final MetadataExtractorIntegrator INSTANCE = new MetadataExtractorIntegrator(); - - private Database database; - - public Database getDatabase() { - return database; - } - - @Override - public void integrate( - Metadata metadata, - SessionFactoryImplementor sessionFactory, - SessionFactoryServiceRegistry serviceRegistry) { - - database = metadata.getDatabase(); - } - - @Override - public void disintegrate( - SessionFactoryImplementor sessionFactory, - SessionFactoryServiceRegistry serviceRegistry) { - - } - } - - @Override - protected Integrator integrator() { - return MetadataExtractorIntegrator.INSTANCE; - } - - @Override - protected Class[] entities() { - return new BlogEntityProvider().entities(); - } - - @Test - public void test() { - for(Namespace namespace : MetadataExtractorIntegrator.INSTANCE.getDatabase().getNamespaces()) { - for( Table table : namespace.getTables()) { - LOGGER.info( "Table {} has the following columns: {}", - table, - StreamSupport.stream( - Spliterators.spliteratorUnknownSize( table.getColumnIterator(), Spliterator.ORDERED), false) - .collect( Collectors.toList()) ); - } - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/naming/ExtendedNamingTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/naming/ExtendedNamingTest.java deleted file mode 100644 index ebc21d10f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/naming/ExtendedNamingTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.naming; - -import java.util.Properties; - -/** - * @author Vlad Mihalcea - */ -public class ExtendedNamingTest extends DefaultNamingTest { - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.physical_naming_strategy", "com.vladmihalcea.book.hpjp.hibernate.naming.OracleNamingStrategy"); - return properties; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/NativeQueryEntityMappingTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/NativeQueryEntityMappingTest.java deleted file mode 100644 index 63adc21c2..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/NativeQueryEntityMappingTest.java +++ /dev/null @@ -1,252 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query; - -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Properties; -import java.util.Set; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.EntityResult; -import javax.persistence.FieldResult; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.JoinTable; -import javax.persistence.ManyToMany; -import javax.persistence.NamedNativeQuery; -import javax.persistence.SqlResultSetMapping; -import javax.persistence.Table; - -import org.hibernate.Criteria; -import org.hibernate.SQLQuery; -import org.hibernate.cfg.AvailableSettings; -import org.hibernate.query.NativeQuery; -import org.hibernate.transform.ResultTransformer; - -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class NativeQueryEntityMappingTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - Tag.class, - }; - } - - @Test - public void test() { - final Long postId = doInJPA(entityManager -> { - Post post1 = new Post("JPA with Hibernate"); - Post post2 = new Post("Native Hibernate"); - - Tag tag1 = new Tag("Java"); - Tag tag2 = new Tag("Hibernate"); - - post1.addTag(tag1); - post1.addTag(tag2); - - post2.addTag(tag1); - - entityManager.persist(post1); - entityManager.persist(post2); - - return post1.id; - }); - doInJPA(entityManager -> { - List tuples = entityManager.createNativeQuery( - "SELECT * "+ - "FROM post p "+ - "LEFT JOIN post_tag pt ON p.id = pt.post_id "+ - "LEFT JOIN tag t ON t.id = pt.tag_id ") - .unwrap( NativeQuery.class ) - .addEntity("post", Post.class) - .addJoin("tag", "post.tags") - .getResultList(); - - assertEquals(3, tuples.size()); - }); - - doInJPA(entityManager -> { - List tuples = entityManager - .createNamedQuery("find_posts_with_tags") - .getResultList(); - - assertEquals(3, tuples.size()); - }); - } - - @NamedNativeQuery( - name = "find_posts_with_tags", - query = - "SELECT " + - " p.id as \"p.id\", "+ - " p.title as \"p.title\", "+ - " t.id as \"t.id\", "+ - " t.name as \"t.name\" "+ - "FROM post p "+ - "LEFT JOIN post_tag pt ON p.id = pt.post_id "+ - "LEFT JOIN tag t ON t.id = pt.tag_id ", - resultSetMapping = "posts_with_tags" - ) - @SqlResultSetMapping( - name = "posts_with_tags", - entities = { - @EntityResult( - entityClass = Post.class, - fields = { - @FieldResult( name = "id", column = "p.id" ), - @FieldResult( name = "title", column = "p.title" ), - } - ), - @EntityResult( - entityClass = Tag.class, - fields = { - @FieldResult( name = "id", column = "t.id" ), - @FieldResult( name = "name", column = "t.name" ), - } - ) - } - ) - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Post() {} - - public Post(String title) { - this.title = title; - } - - @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE}) - @JoinTable(name = "post_tag", - joinColumns = @JoinColumn(name = "post_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id") - ) - private Set tags = new HashSet<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public Set getTags() { - return tags; - } - - public void setTags(Set tags) { - this.tags = tags; - } - - public void addTag(Tag tag) { - tags.add(tag); - tag.getPosts().add(this); - } - - public void removeTag(Tag tag) { - tags.remove(tag); - tag.getPosts().remove(this); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Post post = (Post) o; - return Objects.equals( title, post.title); - } - - @Override - public int hashCode() { - return Objects.hash(title); - } - } - - @Entity(name = "Tag") - @Table(name = "tag") - public static class Tag { - - @Id - @GeneratedValue - private Long id; - - private String name; - - @ManyToMany(mappedBy = "tags") - private Set posts = new HashSet<>(); - - public Tag() {} - - public Tag(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Set getPosts() { - return posts; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Tag tag = (Tag) o; - return Objects.equals(name, tag.name); - } - - @Override - public int hashCode() { - return Objects.hash(name); - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/SQLInjectionTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/SQLInjectionTest.java deleted file mode 100644 index 35c35b3c9..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/SQLInjectionTest.java +++ /dev/null @@ -1,326 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query; - -import com.vladmihalcea.book.hpjp.hibernate.forum.Post; -import com.vladmihalcea.book.hpjp.hibernate.forum.PostComment; -import com.vladmihalcea.book.hpjp.hibernate.forum.PostDetails; -import com.vladmihalcea.book.hpjp.hibernate.forum.Tag; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.Session; -import org.junit.Test; - -import javax.persistence.Tuple; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Root; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.Statement; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -public class SQLInjectionTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostDetails.class, - PostComment.class, - Tag.class - }; - } - - @Override - public void init() { - super.init(); - doInJPA(entityManager -> { - Tag tag = new Tag(); - tag.setId(1L); - tag.setName("Java"); - entityManager.persist(tag); - - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - PostComment comment1 = new PostComment(); - comment1.setId(1L); - comment1.setReview("Good"); - - PostComment comment2 = new PostComment(); - comment2.setId(2L); - comment2.setReview("Excellent"); - - post.addComment(comment1); - post.addComment(comment2); - entityManager.persist(post); - }); - } - - @Test - public void testSelectCustomFunction() { - /*doInJPA(entityManager -> { - Post post = findPersonByFirstAndLastName("Vlad", "Mihalcea' and FUNCTION('current_database',) is not null and '' = '"); - LOGGER.info("Found entity {}", post); - });*/ - } - - @Test - public void testStatementUpdateDropTable() { - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - assertEquals("Good", comment.getReview()); - }); - - updatePostCommentReviewUsingStatement(1L, "Awesome"); - - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - assertEquals("Awesome", comment.getReview()); - }); - - try { - updatePostCommentReviewUsingStatement(1L, "'; DROP TABLE post_comment; -- '"); - } catch (Exception e) { - LOGGER.error("Failure", e); - } - - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - assertNotNull(comment); - }); - } - - @Test - public void testStatementSelectDropTable() { - assertEquals("Good", getPostCommentReviewUsingStatement("1")); - try { - getPostCommentReviewUsingStatement("1; DROP TABLE post_comment"); - } catch (Exception expected) { - LOGGER.error("Failure", expected); - } - assertEquals("Good", getPostCommentReviewUsingStatement("1")); - } - - @Test - public void testPreparedStatementUpdateDropTable() { - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - assertEquals("Good", comment.getReview()); - }); - - updatePostCommentReviewUsingPreparedStatement(1L, "Awesome"); - - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - assertEquals("Awesome", comment.getReview()); - }); - - try { - updatePostCommentReviewUsingPreparedStatement(1L, "'; DROP TABLE post_comment; -- '"); - } catch (Exception e) { - LOGGER.error("Failure", e); - } - - doInJPA(entityManager -> { - PostComment comment = entityManager.find(PostComment.class, 1L); - assertNotNull(comment); - }); - } - - @Test - public void testPreparedStatementSelectDropTable() { - assertEquals("Good", getPostCommentReviewUsingPreparedStatement("1")); - try { - getPostCommentReviewUsingPreparedStatement("1; DROP TABLE post_comment"); - } catch (Exception expected) { - LOGGER.error("Failure", expected); - } - assertEquals("Good", getPostCommentReviewUsingPreparedStatement("1")); - } - - @Test - public void testGetPostCommentByReview() { - getPostCommentByReview("1 AND 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) )"); - } - - @Test - public void testPreparedStatementSelectAndWait() { - assertEquals("Good", getPostCommentReviewUsingPreparedStatement("1")); - try { - getPostCommentReviewUsingPreparedStatement("1 AND 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) )"); - } catch (Exception expected) { - LOGGER.error("Failure", expected); - } - assertEquals("Good", getPostCommentReviewUsingPreparedStatement("1")); - } - - public void updatePostCommentReviewUsingStatement(Long id, String review) { - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - session.doWork(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate( - "UPDATE post_comment " + - "SET review = '" + review + "' " + - "WHERE id = " + id); - } - }); - }); - } - - public void updatePostCommentReviewUsingPreparedStatement(Long id, String review) { - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - session.doWork(connection -> { - String sql = - "UPDATE post_comment " + - "SET review = '" + review + "' " + - "WHERE id = " + id; - try(PreparedStatement statement = connection.prepareStatement(sql)) { - statement.executeUpdate(); - } - }); - }); - } - - public String getPostCommentReviewUsingStatement(String id) { - return doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - return session.doReturningWork(connection -> { - String sql = - "SELECT review " + - "FROM post_comment " + - "WHERE id = " + id; - try(Statement statement = connection.createStatement()) { - try(ResultSet resultSet = statement.executeQuery(sql)) { - return resultSet.next() ? resultSet.getString(1) : null; - } - } - }); - }); - } - - public String getPostCommentReviewUsingPreparedStatement(String id) { - return doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - return session.doReturningWork(connection -> { - String sql = - "SELECT review " + - "FROM post_comment " + - "WHERE id = " + id; - try(PreparedStatement statement = connection.prepareStatement(sql)) { - try(ResultSet resultSet = statement.executeQuery()) { - return resultSet.next() ? resultSet.getString(1) : null; - } - } - }); - }); - } - - public PostComment getPostCommentByReview(String review) { - return doInJPA(entityManager -> { - return entityManager.createQuery( - "select p " + - "from PostComment p " + - "where p.review = :review", PostComment.class) - .setParameter("review", review) - .getSingleResult(); - }); - } - - @Test - public void testSelectAllEntities() { - doInJPA(entityManager -> { - List posts = findAll("com.vladmihalcea.book.hpjp.hibernate.forum.Post"); - posts = findAll("java.lang.Object"); - posts.size(); - }); - } - - @Test - public void testGetPostByTitleSuccess() { - doInJPA(entityManager -> { - List posts = getPostsByTitle("High-Performance Java Persistence"); - }); - } - - @Test - public void testPostGetByTitleAndWait() { - doInJPA(entityManager -> { - List posts = getPostsByTitle( - "High-Performance Java Persistence' and " + - "FUNCTION('1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ) --',) is '" - ); - assertEquals(1, posts.size()); - }); - } - - @Test - public void testTuples() { - doInJPA(entityManager -> { - List tuples = getTuples(); - assertEquals(1, tuples.size()); - }); - } - - public List getPostsByTitle(String title) { - return doInJPA(entityManager -> { - return entityManager.createQuery( - "select p " + - "from Post p " + - "where" + - " p.title = '" + title + "'", Post.class) - .getResultList(); - }); - } - - public List getTuples() { - return doInJPA(entityManager -> { - Class entityClass = Post.class; - CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery query = cb.createTupleQuery(); - Root root = query.from(entityClass); - query.select( - cb.tuple( - root.get("id"), - cb.function("now", Date.class) - ) - ); - - return entityManager.createQuery(query).getResultList(); - }); - } - - /*public Person findPersonByFirstAndLastName(String firstName, String lastName) { - return doInJPA(entityManager -> { - return entityManager.createQuery( - "select p " + - "from Person p " + - "where" + - " p.firstName = '" + firstName + "'" + - " and p.lastName = '" +lastName + "'", Person.class) - .getSingleResult(); - }); - }*/ - - public List findAll(String entityName) { - return (List) doInJPA(entityManager -> { - try { - return entityManager.unwrap(Session.class).createQuery( - "select e " + - "from " + Class.forName(entityName).getName() + " e ") - .getResultList(); - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException(e); - } - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/dto/Country.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/dto/Country.java deleted file mode 100644 index bfc0f5394..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/dto/Country.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.dto; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; - -/** - * @author Vlad Mihalcea - */ -@Entity(name = "Country") -public class Country { - - @Id - @GeneratedValue - private Long id; - - private String name; - - private String locale; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getLocale() { - return locale; - } - - public void setLocale(String locale) { - this.locale = locale; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/dto/DTOWithEntityTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/dto/DTOWithEntityTest.java deleted file mode 100644 index 89663b941..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/dto/DTOWithEntityTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.dto; - -import java.util.List; - -import org.hibernate.query.Query; -import org.hibernate.transform.ResultTransformer; - -import org.junit.Assert; -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class DTOWithEntityTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - Person.class, - Country.class - }; - } - - @Test - public void test() { - doInJPA( entityManager -> { - Country usa = new Country(); - usa.setName( "USA" ); - usa.setLocale( "en-US" ); - entityManager.persist( usa ); - - Country romania = new Country(); - romania.setName( "Romania" ); - romania.setLocale( "ro-RO" ); - entityManager.persist( romania ); - - Person chris = new Person(); - chris.setName( "Chris" ); - chris.setLocale( "en-US" ); - entityManager.persist( chris ); - - Person vlad = new Person(); - vlad.setName( "Vlad" ); - vlad.setLocale( "ro-RO" ); - entityManager.persist( vlad ); - } ); - - doInJPA( entityManager -> { - LOGGER.info( "Using constructor resultl set" ); - List personAndAddressDTOs = entityManager.createQuery( - "select new " + - " com.vladmihalcea.book.hpjp.hibernate.query.dto.PersonAndCountryDTO(" + - " p, " + - " c.name" + - " ) " + - "from Person p " + - "join Country c on p.locale = c.locale " + - "order by p.id", PersonAndCountryDTO.class) - .getResultList(); - - PersonAndCountryDTO firstEntry = personAndAddressDTOs.get( 0 ); - Assert.assertEquals( "Chris", firstEntry.getPerson().getName()); - assertEquals("USA", firstEntry.getCountry()); - } ); - - doInJPA( entityManager -> { - LOGGER.info( "Using ResultTransformer" ); - List personAndAddressDTOs = entityManager.createQuery( - "select p, c.name " + - "from Person p " + - "join Country c on p.locale = c.locale " + - "order by p.id") - .unwrap( Query.class ) - .setResultTransformer( new ResultTransformer() { - @Override - public Object transformTuple(Object[] tuple, String[] aliases) { - return new PersonAndCountryDTO( - (Person) tuple[0], - (String) tuple[1] - ); - } - - @Override - public List transformList(List collection) { - return collection; - } - } ) - .getResultList(); - - PersonAndCountryDTO firstEntry = personAndAddressDTOs.get( 0 ); - Assert.assertEquals( "Chris", firstEntry.getPerson().getName()); - assertEquals("USA", firstEntry.getCountry()); - } ); - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/dto/Person.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/dto/Person.java deleted file mode 100644 index 3f38a22c4..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/dto/Person.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.dto; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; - -/** - * @author Vlad Mihalcea - */ -@Entity(name = "Person") -public class Person { - - @Id - @GeneratedValue - private Long id; - - private String name; - - private String locale; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getLocale() { - return locale; - } - - public void setLocale(String locale) { - this.locale = locale; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/PostComment.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/PostComment.java deleted file mode 100644 index 655070f2c..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/PostComment.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.hierarchical; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -@Entity(name = "PostComment") -public class PostComment { - - @Id - @GeneratedValue - private Long id; - - @ManyToOne - @JoinColumn(name = "parent_id") - private PostComment parent; - - private String description; - - @Enumerated(EnumType.STRING) - private Status status; - - @Transient - private List children = new ArrayList<>(); - - public PostComment() { - } - - public PostComment(String value, Status status) { - this.description = value; - this.status = status; - } - - public PostComment getParent() { - return parent; - } - - public String getDescription() { - return description; - } - - public Status getStatus() { - return status; - } - - public List getChildren() { - return children; - } - - public void addChild(PostComment child) { - children.add(child); - child.parent = this; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/Status.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/Status.java deleted file mode 100644 index 7a3579d9a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/Status.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.hierarchical; - -/** - * @author Vlad Mihalcea - */ -public enum Status { - - APPROVED, - PENDING -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/TreeCTETest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/TreeCTETest.java deleted file mode 100644 index 0df946336..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/TreeCTETest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.hierarchical; - -import org.hibernate.SQLQuery; -import org.junit.Test; - -import java.util.List; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class TreeCTETest extends AbstractTreeTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new PostgreSQLDataSourceProvider(); - } - - @Test - public void test() { - - List comments = doInJPA(entityManager -> { - return (List) entityManager.createNativeQuery( - "WITH RECURSIVE comment_tree(id, parent_id, description, status) AS ( " + - " SELECT c.id, c.parent_id, c.description, status " + - " FROM PostComment c " + - " WHERE LOWER(c.description) LIKE :token AND c.status = :status " + - " UNION ALL " + - " SELECT c.id, c.parent_id, c.description, c.status " + - " FROM PostComment c " + - " INNER JOIN comment_tree ct on ct.id = c.parent_id " + - " WHERE c.status = :status " + - ") " + - "SELECT id, parent_id, description, status " + - "FROM comment_tree ") - .setParameter("status", Status.APPROVED.name()) - .setParameter("token", "high-performance%") - .unwrap(SQLQuery.class) - .addEntity(PostComment.class) - .setResultTransformer(PostCommentTreeTransformer.INSTANCE) - .list(); - }); - assertEquals(1, comments.size()); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/TreeTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/TreeTest.java deleted file mode 100644 index f020504e4..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/TreeTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.hierarchical; - -import org.hibernate.*; -import org.hibernate.transform.ResultTransformer; -import org.junit.Test; - -import java.util.Collections; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -public class TreeTest extends AbstractTreeTest { - - @Test - public void test() { - - List comments = doInJPA(entityManager -> { - return (List) entityManager - .unwrap(Session.class) - .createQuery( - "SELECT c " + - "FROM PostComment c " + - "WHERE c.status = :status") - .setParameter("status", Status.APPROVED) - .setResultTransformer(PostCommentTreeTransformer.INSTANCE) - .list(); - }); - assertEquals(2, comments.size()); - } - - @Test - public void testRecursion() { - PostComment comment = doInJPA(entityManager -> { - PostComment root = entityManager.createQuery("select n from Comment n where n.parent is null", PostComment.class).getSingleResult(); - fetchChildren(root); - return root; - }); - fetchChildren(comment); - } - - public void fetchChildren(PostComment comment) { - for (PostComment _comment : comment.getChildren()) { - fetchChildren(_comment); - } - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/Component.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/Component.java deleted file mode 100644 index e5ad563b3..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/Component.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.pivot; - -import javax.persistence.Entity; -import javax.persistence.Id; - -/** - * @author Vlad Mihalcea - */ -@Entity -public class Component { - - @Id - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/Property.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/Property.java deleted file mode 100644 index 0b5e45193..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/Property.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.pivot; - -import javax.persistence.Column; -import javax.persistence.EmbeddedId; -import javax.persistence.Entity; - -/** - * - * @author Vlad Mihalcea - */ -@Entity -public class Property { - - @EmbeddedId - private PropertyId id; - - @Column(name = "property_value") - private String value; - - public PropertyId getId() { - return id; - } - - public void setId(PropertyId id) { - this.id = id; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/Service.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/Service.java deleted file mode 100644 index 9e5165ff1..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/Service.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.pivot; - -import javax.persistence.Entity; -import javax.persistence.Id; - -/** - * @author Vlad Mihalcea - */ -@Entity -public class Service { - - @Id - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/PostCommentScore.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/PostCommentScore.java deleted file mode 100644 index 364961615..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/PostCommentScore.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.Date; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public class PostCommentScore { - - private Long id; - private Long parentId; - private String review; - private Date createdOn; - private long score; - - private List children = new ArrayList<>(); - - public PostCommentScore(Number id, Number parentId, String review, Date createdOn, Number score) { - this.id = id.longValue(); - this.parentId = parentId != null ? parentId.longValue() : null; - this.review = review; - this.createdOn = createdOn; - this.score = score.longValue(); - } - - public PostCommentScore() { - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Long getParentId() { - return parentId; - } - - public void setParentId(Long parentId) { - this.parentId = parentId; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public long getScore() { - return score; - } - - public void setScore(long score) { - this.score = score; - } - - public long getTotalScore() { - long total = getScore(); - for(PostCommentScore child : children) { - total += child.getTotalScore(); - } - return total; - } - - public List getChildren() { - List copy = new ArrayList<>(children); - copy.sort(Comparator.comparing(PostCommentScore::getCreatedOn)); - return copy; - } - - public void addChild(PostCommentScore child) { - children.add(child); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/PostCommentScoreResultTransformer.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/PostCommentScoreResultTransformer.java deleted file mode 100644 index d5033cdca..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/PostCommentScoreResultTransformer.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive; - -import org.hibernate.transform.ResultTransformer; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * @author Vlad Mihalcea - */ -public class PostCommentScoreResultTransformer implements ResultTransformer { - - private Map postCommentScoreMap = new HashMap<>(); - - private List roots = new ArrayList<>(); - - @Override - public Object transformTuple(Object[] tuple, String[] aliases) { - PostCommentScore commentScore = (PostCommentScore) tuple[0]; - Long parentId = commentScore.getParentId(); - if (parentId == null) { - roots.add(commentScore); - } else { - PostCommentScore parent = postCommentScoreMap.get(parentId); - if (parent != null) { - parent.addChild(commentScore); - } - } - postCommentScoreMap.putIfAbsent(commentScore.getId(), commentScore); - return commentScore; - } - - @Override - public List transformList(List collection) { - return roots; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/AbstractPostCommentScorePerformanceTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/AbstractPostCommentScorePerformanceTest.java deleted file mode 100644 index 1c7774e98..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/AbstractPostCommentScorePerformanceTest.java +++ /dev/null @@ -1,405 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive.complex; - -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Slf4jReporter; -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import javax.persistence.*; -import java.io.Serializable; -import java.util.*; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -@RunWith(Parameterized.class) -public abstract class AbstractPostCommentScorePerformanceTest extends AbstractPostgreSQLIntegrationTest { - - private MetricRegistry metricRegistry = new MetricRegistry(); - - protected com.codahale.metrics.Timer timer = metricRegistry.timer(getClass().getSimpleName()); - - private Slf4jReporter logReporter = Slf4jReporter - .forRegistry(metricRegistry) - .outputTo(LOGGER) - .convertDurationsTo(TimeUnit.MILLISECONDS) - .build(); - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - User.class, - PostCommentVote.class, - }; - } - - private User user1; - private User user2; - - private int postCount; - private int commentCount; - - public AbstractPostCommentScorePerformanceTest(int postCount, int commentCount) { - this.postCount = postCount; - this.commentCount = commentCount; - } - - @Parameterized.Parameters - public static Collection parameters() { - List postCountSizes = new ArrayList<>(); - int postCount = 10; - postCountSizes.add(new Integer[] {postCount, 4}); - postCountSizes.add(new Integer[] {postCount, 4}); - postCountSizes.add(new Integer[] {postCount, 4}); - postCountSizes.add(new Integer[] {postCount, 8}); - postCountSizes.add(new Integer[] {postCount, 16}); - postCountSizes.add(new Integer[] {postCount, 32}); - postCountSizes.add(new Integer[] {postCount, 64}); - return postCountSizes; - } - - @Override - public void init() { - super.init(); - doInJPA(entityManager -> { - user1 = new User(); - user1.setUsername("JohnDoe"); - entityManager.persist(user1); - - user2 = new User(); - user2.setUsername("JohnDoeJr"); - entityManager.persist(user2); - }); - for (long i = 0; i < postCount; i++) { - insertPost(i); - } - } - - private void insertPost(Long postId) { - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(postId); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - - for (int i = 0; i < commentCount; i++) { - PostComment comment1 = new PostComment(); - comment1.setPost(post); - comment1.setReview(String.format("Comment %d", i)); - entityManager.persist(comment1); - - PostCommentVote user1Comment1 = new PostCommentVote(user1, comment1); - user1Comment1.setUp(entropy()); - entityManager.persist(user1Comment1); - - for (int j = 0; j < commentCount / 2; j++) { - PostComment comment1_1 = new PostComment(); - comment1_1.setParent(comment1); - comment1_1.setPost(post); - comment1_1.setReview(String.format("Comment %d-%d", i, j)); - entityManager.persist(comment1_1); - - PostCommentVote user1Comment1_1 = new PostCommentVote(user1, comment1_1); - user1Comment1_1.setUp(entropy()); - entityManager.persist(user1Comment1_1); - - PostCommentVote user2Comment1_1 = new PostCommentVote(user2, comment1_1); - user2Comment1_1.setUp(entropy()); - entityManager.persist(user2Comment1_1); - - for (int k = 0; k < commentCount / 4 ; k++) { - PostComment comment1_1_1 = new PostComment(); - comment1_1_1.setParent(comment1_1_1); - comment1_1_1.setPost(post); - comment1_1_1.setReview(String.format("Comment %d-%d-%d", i, j, k)); - entityManager.persist(comment1_1_1); - - PostCommentVote user1Comment1_1_1 = new PostCommentVote(user1, comment1_1_1); - user1Comment1_1_1.setUp(entropy()); - entityManager.persist(user1Comment1_1_1); - - PostCommentVote user2Comment1_1_2 = new PostCommentVote(user2, comment1_1_1); - user2Comment1_1_2.setUp(entropy()); - entityManager.persist(user2Comment1_1_2); - } - } - } - }); - } - - private boolean entropy() { - return Math.random() > 0.5d; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.jdbc.batch_size", "5"); - properties.put("hibernate.order_inserts", "true"); - properties.put("hibernate.order_updates", "true"); - properties.put("hibernate.jdbc.batch_versioned_data", "true"); - return properties; - } - - @Test - public void test() { - int rank = 3; - for (long postId = 0; postId < postCount; postId++) { - List result = postCommentScores(postId, rank); - assertNotNull(result); - } - logReporter.report(); - } - - protected abstract List postCommentScores(Long postId, int rank); - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @SqlResultSetMapping( - name = "PostCommentScore", - classes = @ConstructorResult( - targetClass = PostCommentScore.class, - columns = { - @ColumnResult(name = "id"), - @ColumnResult(name = "parent_id"), - @ColumnResult(name = "root_id"), - @ColumnResult(name = "review"), - @ColumnResult(name = "created_on"), - @ColumnResult(name = "score") - } - ) - ) - public static class PostComment { - - @Id - @GeneratedValue - private Long id; - - @ManyToOne - @JoinColumn(name = "post_id") - private Post post; - - @ManyToOne - @JoinColumn(name = "parent_id") - private PostComment parent; - - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "created_on") - private Date createdOn = new Date(); - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public PostComment getParent() { - return parent; - } - - public void setParent(PostComment parent) { - this.parent = parent; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PostComment that = (PostComment) o; - return Objects.equals(getPost(), that.getPost()) && - Objects.equals(getParent(), that.getParent()) && - Objects.equals(getReview(), that.getReview()); - } - - @Override - public int hashCode() { - return Objects.hash(getPost(), getReview()); - } - - @Override - public String toString() { - return "PostComment{" + - "review='" + review + '\'' + - ", post=" + post + - '}'; - } - } - - @Entity(name = "User") - @Table(name = "forum_user") - public static class User { - - @Id - @GeneratedValue - private Long id; - - private String username; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - User user = (User) o; - return Objects.equals(getUsername(), user.getUsername()); - } - - @Override - public int hashCode() { - return Objects.hash(getUsername()); - } - - @Override - public String toString() { - return "User{" + - "username='" + username + '\'' + - '}'; - } - } - - @Entity(name = "PostCommentVote") - @Table(name = "post_comment_vote") - public static class PostCommentVote implements Serializable { - - @Id - @ManyToOne - private User user; - - @Id - @ManyToOne - private PostComment comment; - - private boolean up; - - private PostCommentVote() { - } - - public PostCommentVote(User user, PostComment comment) { - this.user = user; - this.comment = comment; - } - - public User getUser() { - return user; - } - - public void setUser(User user) { - this.user = user; - } - - public PostComment getComment() { - return comment; - } - - public void setComment(PostComment comment) { - this.comment = comment; - } - - public boolean isUp() { - return up; - } - - public void setUp(boolean up) { - this.up = up; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PostCommentVote that = (PostCommentVote) o; - return Objects.equals(getUser(), that.getUser()) && - Objects.equals(getComment(), that.getComment()); - } - - @Override - public int hashCode() { - return Objects.hash(getUser(), getComment()); - } - - @Override - public String toString() { - return "PostCommentVote{" + - "user=" + user + - ", comment=" + comment + - '}'; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreFetchProjectionPerformanceTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreFetchProjectionPerformanceTest.java deleted file mode 100644 index 0044782f0..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreFetchProjectionPerformanceTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive.complex; - -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -/** - * @author Vlad Mihalcea - */ -public class PostCommentScoreFetchProjectionPerformanceTest extends AbstractPostCommentScorePerformanceTest { - - public PostCommentScoreFetchProjectionPerformanceTest(int postCount, int commentCount) { - super(postCount, commentCount); - } - - @Override - protected List postCommentScores(Long postId, int rank) { - return doInJPA(entityManager -> { - long startNanos = System.nanoTime(); - List postCommentScores = entityManager.createQuery( - "select new com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore(" + - " pc.id, pc.parent.id, pc.review, pc.createdOn, sum( case when pcv.up is null then 0 when pcv.up = true then 1 else -1 end ) " + - ") " + - "from PostComment pc " + - "left join PostCommentVote pcv on pc.id = pcv.comment " + - "where pc.post.id = :postId " + - "group by pc.id, pc.parent.id, pc.review, pc.createdOn ") - .setParameter("postId", postId) - .getResultList(); - - Map> postCommentScoreMap = postCommentScores.stream().collect(Collectors.groupingBy(PostCommentScore::getId)); - - List roots = new ArrayList<>(); - - for(PostCommentScore postCommentScore : postCommentScores) { - Long parentId = postCommentScore.getParentId(); - if(parentId == null) { - roots.add(postCommentScore); - } else { - PostCommentScore parent = postCommentScoreMap.get(parentId).get(0); - parent.addChild(postCommentScore); - } - } - - roots.sort(Comparator.comparing(PostCommentScore::getTotalScore).reversed()); - - if(roots.size() > rank) { - roots = roots.subList(0, rank); - } - timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - return roots; - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreRecursiveCTEPerformanceTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreRecursiveCTEPerformanceTest.java deleted file mode 100644 index fcfc53024..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreRecursiveCTEPerformanceTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive.complex; - -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; -import org.hibernate.SQLQuery; -import org.hibernate.transform.ResultTransformer; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -/** - * @author Vlad Mihalcea - */ -public class PostCommentScoreRecursiveCTEPerformanceTest extends AbstractPostCommentScorePerformanceTest { - - public PostCommentScoreRecursiveCTEPerformanceTest(int postCount, int commentCount) { - super(postCount, commentCount); - } - - @Override - protected List postCommentScores(Long postId, int rank) { - return doInJPA(entityManager -> { - long startNanos = System.nanoTime(); - List postCommentScores = entityManager.createNativeQuery( - "SELECT id, parent_id, root_id, review, created_on, score " + - "FROM ( " + - " SELECT " + - " id, parent_id, root_id, review, created_on, score, " + - " dense_rank() OVER (ORDER BY total_score DESC) rank " + - " FROM ( " + - " SELECT " + - " id, parent_id, root_id, review, created_on, score, " + - " SUM(score) OVER (PARTITION BY root_id) total_score " + - " FROM (" + - " WITH RECURSIVE post_comment_score(id, root_id, post_id, " + - " parent_id, review, created_on, user_id, score) AS (" + - " SELECT id, id, post_id, parent_id, review, created_on, user_id, " + - " CASE WHEN up IS NULL THEN 0 WHEN up = true THEN 1 " + - " ELSE - 1 END score " + - " FROM post_comment " + - " LEFT JOIN post_comment_vote ON comment_id = id " + - " WHERE post_id = :postId AND parent_id IS NULL " + - " UNION ALL " + - " SELECT distinct pc.id, pcs.root_id, pc.post_id, pc.parent_id, " + - " pc.review, pc.created_on, pcv.user_id, CASE WHEN pcv.up IS NULL THEN 0 " + - " WHEN pcv.up = true THEN 1 ELSE - 1 END score " + - " FROM post_comment pc " + - " LEFT JOIN post_comment_vote pcv ON pcv.comment_id = pc.id " + - " INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id " + - " WHERE pc.parent_id = pcs.id " + - " ) " + - " SELECT id, parent_id, root_id, review, created_on, SUM(score) score" + - " FROM post_comment_score " + - " GROUP BY id, parent_id, root_id, review, created_on" + - " ) score_by_comment " + - " ) score_total " + - " ORDER BY total_score DESC, created_on ASC " + - ") total_score_group " + - "WHERE rank <= :rank", "PostCommentScore").unwrap(SQLQuery.class) - .setParameter("postId", postId).setParameter("rank", rank) - .setResultTransformer(new PostCommentScoreResultTransformer()) - .list(); - timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - return postCommentScores; - }); - } - - public static class PostCommentScoreResultTransformer implements ResultTransformer { - - private Map postCommentScoreMap = new HashMap<>(); - - private List roots = new ArrayList<>(); - - @Override - public Object transformTuple(Object[] tuple, String[] aliases) { - PostCommentScore postCommentScore = (PostCommentScore) tuple[0]; - if(postCommentScore.getParentId() == null) { - roots.add(postCommentScore); - } else { - PostCommentScore parent = postCommentScoreMap.get(postCommentScore.getParentId()); - if(parent != null) { - parent.addChild(postCommentScore); - } - } - postCommentScoreMap.putIfAbsent(postCommentScore.getId(), postCommentScore); - return postCommentScore; - } - - @Override - public List transformList(List collection) { - return roots; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreTest.java deleted file mode 100644 index 5c3ed8f17..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreTest.java +++ /dev/null @@ -1,590 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive.complex; - -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.SQLQuery; -import org.hibernate.transform.ResultTransformer; -import org.junit.Test; - -import javax.persistence.*; -import java.io.Serializable; -import java.util.*; -import java.util.stream.Collectors; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class PostCommentScoreTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - User.class, - PostCommentVote.class, - }; - } - - @Override - public void init() { - super.init(); - doInJPA(entityManager -> { - User user1 = new User(); - user1.setUsername("JohnDoe"); - entityManager.persist(user1); - - User user2 = new User(); - user2.setUsername("JohnDoeJr"); - entityManager.persist(user2); - - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - - PostComment comment1 = new PostComment(); - comment1.setPost(post); - comment1.setReview("Comment 1"); - entityManager.persist(comment1); - - PostCommentVote user1Comment1 = new PostCommentVote(user1, comment1); - user1Comment1.setUp(true); - entityManager.persist(user1Comment1); - - PostComment comment1_1 = new PostComment(); - comment1_1.setParent(comment1); - comment1_1.setPost(post); - comment1_1.setReview("Comment 1_1"); - entityManager.persist(comment1_1); - - PostCommentVote user1Comment1_1 = new PostCommentVote(user1, comment1_1); - user1Comment1_1.setUp(true); - entityManager.persist(user1Comment1_1); - - PostCommentVote user2Comment1_1 = new PostCommentVote(user2, comment1_1); - user2Comment1_1.setUp(true); - entityManager.persist(user2Comment1_1); - - PostComment comment1_2 = new PostComment(); - comment1_2.setParent(comment1); - comment1_2.setPost(post); - comment1_2.setReview("Comment 1_2"); - entityManager.persist(comment1_2); - - PostCommentVote user1Comment1_2 = new PostCommentVote(user1, comment1_2); - user1Comment1_2.setUp(true); - entityManager.persist(user1Comment1_2); - - PostCommentVote user2Comment1_3 = new PostCommentVote(user2, comment1_2); - user2Comment1_3.setUp(true); - entityManager.persist(user2Comment1_3); - - PostComment comment1_2_1 = new PostComment(); - comment1_2_1.setParent(comment1_2); - comment1_2_1.setPost(post); - comment1_2_1.setReview("Comment 1_2_1"); - entityManager.persist(comment1_2_1); - - PostCommentVote user1Comment1_2_1 = new PostCommentVote(user1, comment1_2_1); - user1Comment1_2_1.setUp(true); - entityManager.persist(user1Comment1_2_1); - - PostComment comment2 = new PostComment(); - comment2.setPost(post); - comment2.setReview("Comment 2"); - entityManager.persist(comment2); - - PostCommentVote user1Comment2 = new PostCommentVote(user1, comment2); - user1Comment2.setUp(true); - entityManager.persist(user1Comment2); - - PostComment comment2_1 = new PostComment(); - comment2_1.setParent(comment2); - comment2_1.setPost(post); - comment2_1.setReview("Comment 2_1"); - entityManager.persist(comment2_1); - - PostCommentVote user1Comment2_1 = new PostCommentVote(user1, comment2_1); - user1Comment2_1.setUp(true); - entityManager.persist(user1Comment2_1); - - PostCommentVote user2Comment2_1 = new PostCommentVote(user2, comment2_1); - user2Comment2_1.setUp(true); - entityManager.persist(user2Comment2_1); - - PostComment comment2_2 = new PostComment(); - comment2_2.setParent(comment2); - comment2_2.setPost(post); - comment2_2.setReview("Comment 2_2"); - entityManager.persist(comment2_2); - - PostCommentVote user1Comment2_2 = new PostCommentVote(user1, comment2_2); - user1Comment2_2.setUp(true); - entityManager.persist(user1Comment2_2); - - PostComment comment3 = new PostComment(); - comment3.setPost(post); - comment3.setReview("Comment 3"); - entityManager.persist(comment3); - - PostCommentVote user1Comment3 = new PostCommentVote(user1, comment3); - user1Comment3.setUp(true); - entityManager.persist(user1Comment3); - - PostComment comment3_1 = new PostComment(); - comment3_1.setParent(comment3); - comment3_1.setPost(post); - comment3_1.setReview("Comment 3_1"); - entityManager.persist(comment3_1); - - PostCommentVote user1Comment3_1 = new PostCommentVote(user1, comment3_1); - user1Comment3_1.setUp(true); - entityManager.persist(user1Comment3_1); - - PostCommentVote user2Comment3_1 = new PostCommentVote(user2, comment3_1); - user2Comment3_1.setUp(false); - entityManager.persist(user2Comment3_1); - - PostComment comment3_2 = new PostComment(); - comment3_2.setParent(comment3); - comment3_2.setPost(post); - comment3_2.setReview("Comment 3_2"); - entityManager.persist(comment3_2); - - PostCommentVote user1Comment3_2 = new PostCommentVote(user1, comment3_2); - user1Comment3_2.setUp(true); - entityManager.persist(user1Comment3_2); - - PostComment comment4 = new PostComment(); - comment4.setPost(post); - comment4.setReview("Comment 4"); - entityManager.persist(comment4); - - PostCommentVote user1Comment4 = new PostCommentVote(user1, comment4); - user1Comment4.setUp(false); - entityManager.persist(user1Comment4); - - PostComment comment5 = new PostComment(); - comment5.setPost(post); - comment5.setReview("Comment 5"); - entityManager.persist(comment5); - - entityManager.flush(); - - }); - } - - @Test - public void test() { - LOGGER.info("Recursive CTE and Window Functions"); - Long postId = 1L; - int rank = 3; - List resultCTEJoin = postCommentScoresCTEJoin(postId, rank); - assertEquals(3, resultCTEJoin.size()); - - List resultCTESelect = postCommentScoresCTESelect(postId, rank); - assertEquals(3, resultCTESelect.size()); - - List resultInMemory = postCommentScoresInMemory(postId, rank); - assertEquals(3, resultInMemory.size()); - - for (int i = 0; i < resultCTEJoin.size(); i++) { - assertEquals(resultCTEJoin.get(i).getTotalScore(), resultInMemory.get(i).getTotalScore()); - assertEquals(resultCTEJoin.get(i).getTotalScore(), resultCTESelect.get(i).getTotalScore()); - } - } - - private List postCommentScoresCTEJoin(Long postId, int rank) { - return doInJPA(entityManager -> { - List postCommentScores = entityManager.createNativeQuery( - "SELECT id, parent_id, root_id, review, created_on, score " + - "FROM ( " + - " SELECT " + - " id, parent_id, root_id, review, created_on, score, " + - " dense_rank() OVER (ORDER BY total_score DESC) rank " + - " FROM ( " + - " SELECT " + - " id, parent_id, root_id, review, created_on, score, " + - " SUM(score) OVER (PARTITION BY root_id) total_score " + - " FROM (" + - " WITH RECURSIVE post_comment_score(id, root_id, post_id, " + - " parent_id, review, created_on, user_id, score) AS (" + - " SELECT id, id, post_id, parent_id, review, created_on, user_id, " + - " CASE WHEN up IS NULL THEN 0 WHEN up = true THEN 1 " + - " ELSE - 1 END score " + - " FROM post_comment " + - " LEFT JOIN post_comment_vote ON comment_id = id " + - " WHERE post_id = :postId AND parent_id IS NULL " + - " UNION ALL " + - " SELECT distinct pc.id, pcs.root_id, pc.post_id, pc.parent_id, " + - " pc.review, pc.created_on, pcv.user_id, CASE WHEN pcv.up IS NULL THEN 0 " + - " WHEN pcv.up = true THEN 1 ELSE - 1 END score " + - " FROM post_comment pc " + - " LEFT JOIN post_comment_vote pcv ON pcv.comment_id = pc.id " + - " INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id " + - " WHERE pc.parent_id = pcs.id " + - " ) " + - " SELECT id, parent_id, root_id, review, created_on, SUM(score) score" + - " FROM post_comment_score " + - " GROUP BY id, parent_id, root_id, review, created_on" + - " ) score_by_comment " + - " ) score_total " + - " ORDER BY total_score DESC, created_on ASC " + - ") total_score_group " + - "WHERE rank <= :rank", "PostCommentScore").unwrap(SQLQuery.class) - .setParameter("postId", postId).setParameter("rank", rank) - .setResultTransformer(new PostCommentScoreResultTransformer()) - .list(); - return postCommentScores; - }); - } - - private List postCommentScoresCTESelect(Long postId, int rank) { - return doInJPA(entityManager -> { - List postCommentScores = entityManager.createNativeQuery( - "SELECT id, parent_id, root_id, review, created_on, score " + - "FROM ( " + - " SELECT " + - " id, parent_id, root_id, review, created_on, score, " + - " dense_rank() OVER (ORDER BY total_score DESC) rank " + - " FROM ( " + - " SELECT " + - " id, parent_id, root_id, review, created_on, score, " + - " SUM(score) OVER (PARTITION BY root_id) total_score " + - " FROM (" + - " WITH RECURSIVE post_comment_score(id, root_id, post_id, " + - " parent_id, review, created_on, score) AS (" + - " SELECT id, id, post_id, parent_id, review, created_on, " + - " COALESCE (( SELECT SUM (CASE WHEN up = true THEN 1 ELSE - 1 END ) FROM post_comment_vote WHERE comment_id = id ), 0) score " + - " FROM post_comment " + - " WHERE post_id = :postId AND parent_id IS NULL " + - " UNION ALL " + - " SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, " + - " pc.review, pc.created_on, " + - " COALESCE(( SELECT SUM (CASE WHEN up = true THEN 1 ELSE - 1 END ) FROM post_comment_vote WHERE comment_id = pc.id ), 0) score " + - " FROM post_comment pc " + - " INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id " + - " WHERE pc.parent_id = pcs.id " + - " ) " + - " SELECT id, parent_id, root_id, review, created_on, score" + - " FROM post_comment_score" + - " ) score_by_comment " + - " ) score_total " + - " ORDER BY total_score DESC, created_on ASC " + - ") total_score_group " + - "WHERE rank <= :rank", "PostCommentScore").unwrap(SQLQuery.class) - .setParameter("postId", postId).setParameter("rank", rank) - .setResultTransformer(new PostCommentScoreResultTransformer()) - .list(); - return postCommentScores; - }); - } - - protected List postCommentScoresInMemory(Long postId, int rank) { - return doInJPA(entityManager -> { - List postCommentScores = entityManager.createQuery( - "select new com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore(" + - " pc.id, pc.parent.id, 0, pc.review, pc.createdOn, sum( case when pcv.up is null then 0 when pcv.up = true then 1 else -1 end ) " + - ") " + - "from PostComment pc " + - "left join PostCommentVote pcv on pc.id = pcv.comment " + - "where pc.post.id = :postId " + - "group by pc.id, pc.parent.id, pc.review, pc.createdOn ") - .setParameter("postId", postId) - .getResultList(); - - Map> postCommentScoreMap = postCommentScores.stream().collect(Collectors.groupingBy(PostCommentScore::getId)); - - List roots = new ArrayList<>(); - - for(PostCommentScore postCommentScore : postCommentScores) { - Long parentId = postCommentScore.getParentId(); - if(parentId == null) { - roots.add(postCommentScore); - } else { - PostCommentScore parent = postCommentScoreMap.get(parentId).get(0); - parent.addChild(postCommentScore); - } - } - - roots.sort(Comparator.comparing(PostCommentScore::getTotalScore).reversed()); - - if(roots.size() > rank) { - roots = roots.subList(0, rank); - } - return roots; - }); - } - - public static class PostCommentScoreResultTransformer implements ResultTransformer { - - private Map postCommentScoreMap = new HashMap<>(); - - private List roots = new ArrayList<>(); - - @Override - public Object transformTuple(Object[] tuple, String[] aliases) { - PostCommentScore postCommentScore = (PostCommentScore) tuple[0]; - if(postCommentScore.getParentId() == null) { - roots.add(postCommentScore); - } else { - PostCommentScore parent = postCommentScoreMap.get(postCommentScore.getParentId()); - if(parent != null) { - parent.addChild(postCommentScore); - } - } - postCommentScoreMap.putIfAbsent(postCommentScore.getId(), postCommentScore); - return postCommentScore; - } - - @Override - public List transformList(List collection) { - return roots; - } - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @SqlResultSetMapping( - name = "PostCommentScore", - classes = @ConstructorResult( - targetClass = PostCommentScore.class, - columns = { - @ColumnResult(name = "id"), - @ColumnResult(name = "parent_id"), - @ColumnResult(name = "root_id"), - @ColumnResult(name = "review"), - @ColumnResult(name = "created_on"), - @ColumnResult(name = "score") - } - ) - ) - public static class PostComment { - - @Id - @GeneratedValue - private Long id; - - @ManyToOne - @JoinColumn(name = "post_id") - private Post post; - - @ManyToOne - @JoinColumn(name = "parent_id") - private PostComment parent; - - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "created_on") - private Date createdOn = new Date(); - - private String review; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public PostComment getParent() { - return parent; - } - - public void setParent(PostComment parent) { - this.parent = parent; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PostComment that = (PostComment) o; - return Objects.equals(getPost(), that.getPost()) && - Objects.equals(getParent(), that.getParent()) && - Objects.equals(getReview(), that.getReview()); - } - - @Override - public int hashCode() { - return Objects.hash(getPost(), getParent(), getReview()); - } - - @Override - public String toString() { - return "PostComment{" + - "review='" + review + '\'' + - ", post=" + post + - '}'; - } - } - - @Entity(name = "User") - @Table(name = "forum_user") - public static class User { - - @Id - @GeneratedValue - private Long id; - - private String username; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - User user = (User) o; - return Objects.equals(getUsername(), user.getUsername()); - } - - @Override - public int hashCode() { - return Objects.hash(getUsername()); - } - - @Override - public String toString() { - return "User{" + - "username='" + username + '\'' + - '}'; - } - } - - @Entity(name = "PostCommentVote") - @Table(name = "post_comment_vote") - public static class PostCommentVote implements Serializable { - - @Id - @ManyToOne - private User user; - - @Id - @ManyToOne - private PostComment comment; - - private boolean up; - - private PostCommentVote() { - } - - public PostCommentVote(User user, PostComment comment) { - this.user = user; - this.comment = comment; - } - - public User getUser() { - return user; - } - - public void setUser(User user) { - this.user = user; - } - - public PostComment getComment() { - return comment; - } - - public void setComment(PostComment comment) { - this.comment = comment; - } - - public boolean isUp() { - return up; - } - - public void setUp(boolean up) { - this.up = up; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PostCommentVote that = (PostCommentVote) o; - return Objects.equals(getUser(), that.getUser()) && - Objects.equals(getComment(), that.getComment()); - } - - @Override - public int hashCode() { - return Objects.hash(getUser(), getComment()); - } - - @Override - public String toString() { - return "PostCommentVote{" + - "user=" + user + - ", comment=" + comment + - '}'; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/AbstractPostCommentScorePerformanceTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/AbstractPostCommentScorePerformanceTest.java deleted file mode 100644 index c0a406ac0..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/AbstractPostCommentScorePerformanceTest.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive.simple; - -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Slf4jReporter; -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -@RunWith(Parameterized.class) -public abstract class AbstractPostCommentScorePerformanceTest extends AbstractPostgreSQLIntegrationTest { - - protected MetricRegistry metricRegistry = new MetricRegistry(); - - protected com.codahale.metrics.Timer timer = metricRegistry.timer(getClass().getSimpleName()); - - protected Slf4jReporter logReporter = Slf4jReporter - .forRegistry(metricRegistry) - .outputTo(LOGGER) - .convertDurationsTo(TimeUnit.MILLISECONDS) - .build(); - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - }; - } - - private int postCount; - private int commentCount; - - public AbstractPostCommentScorePerformanceTest(int postCount, int commentCount) { - this.postCount = postCount; - this.commentCount = commentCount; - } - - @Parameterized.Parameters - public static Collection parameters() { - List postCountSizes = new ArrayList<>(); - int postCount = 2; - /*postCountSizes.add(new Integer[] {postCount, 16}); - postCountSizes.add(new Integer[] {postCount, 4}); - postCountSizes.add(new Integer[] {postCount, 8}); - postCountSizes.add(new Integer[] {postCount, 16}); - postCountSizes.add(new Integer[] {postCount, 24}); - postCountSizes.add(new Integer[] {postCount, 32}); - postCountSizes.add(new Integer[] {postCount, 48});*/ - postCountSizes.add(new Integer[] {postCount, 64}); - return postCountSizes; - } - - @Override - public void init() { - super.init(); - for (long i = 0; i < postCount; i++) { - insertPost(i); - } - } - - private int randomScore() { - double random = Math.random() + 10; - return (int) random; - } - - private void insertPost(Long postId) { - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(postId); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - - for (int i = 0; i < commentCount; i++) { - PostComment comment1 = new PostComment(); - comment1.setPost(post); - comment1.setReview(String.format("Comment %d", i)); - comment1.setScore(randomScore()); - entityManager.persist(comment1); - - for (int j = 0; j < commentCount / 2; j++) { - PostComment comment1_1 = new PostComment(); - comment1_1.setParent(comment1); - comment1_1.setPost(post); - comment1_1.setReview(String.format("Comment %d-%d", i, j)); - comment1_1.setScore(randomScore()); - entityManager.persist(comment1_1); - - for (int k = 0; k < commentCount / 4 ; k++) { - PostComment comment1_1_1 = new PostComment(); - comment1_1_1.setParent(comment1_1_1); - comment1_1_1.setPost(post); - comment1_1_1.setReview(String.format("Comment %d-%d-%d", i, j, k)); - comment1_1_1.setScore(randomScore()); - entityManager.persist(comment1_1_1); - } - entityManager.flush(); - } - entityManager.flush(); - entityManager.clear(); - } - - LOGGER.info("Added {} PostComments", entityManager - .createQuery("select count(pc) from PostComment pc where pc.post = :post") - .setParameter("post", post) - .getSingleResult() - ); - }); - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.jdbc.batch_size", "50"); - properties.put("hibernate.order_inserts", "true"); - properties.put("hibernate.order_updates", "true"); - properties.put("hibernate.jdbc.batch_versioned_data", "true"); - return properties; - } - - @Test - public void test() { - int rank = 3; - int iterations = 25; - for (int i = 0; i < iterations; i++) { - for (long postId = 0; postId < postCount; postId++) { - List result = postCommentScores(postId, rank); - assertNotNull(result); - } - } - logReporter.report(); - } - - protected abstract List postCommentScores(Long postId, int rank); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/Post.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/Post.java deleted file mode 100644 index f62395825..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/Post.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive.simple; - -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; - -/** - * @author Vlad Mihalcea - */ -@Entity(name = "Post") -@Table(name = "post") -public class Post { - - @Id - private Long id; - - private String title; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostComment.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostComment.java deleted file mode 100644 index 5550d8080..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostComment.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive.simple; - -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; - -import javax.persistence.*; -import java.util.Date; - -/** - * @author Vlad Mihalcea - */ -@Entity(name = "PostComment") -@Table(name = "post_comment") -@SqlResultSetMapping( - name = "PostCommentScore", - classes = @ConstructorResult( - targetClass = PostCommentScore.class, - columns = { - @ColumnResult(name = "id"), - @ColumnResult(name = "parent_id"), - @ColumnResult(name = "review"), - @ColumnResult(name = "created_on"), - @ColumnResult(name = "score") - } - ) -) -public class PostComment { - - @Id - @GeneratedValue - private Long id; - - @ManyToOne - @JoinColumn(name = "post_id") - private Post post; - - @ManyToOne - @JoinColumn(name = "parent_id") - private PostComment parent; - - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "created_on") - private Date createdOn = new Date(); - - private String review; - - private int score; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public PostComment getParent() { - return parent; - } - - public void setParent(PostComment parent) { - this.parent = parent; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public int getScore() { - return score; - } - - public void setScore(int score) { - this.score = score; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionPerformanceTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionPerformanceTest.java deleted file mode 100644 index 9ac86c8be..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionPerformanceTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive.simple; - -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; - -import java.util.*; -import java.util.concurrent.TimeUnit; - -/** - * @author Vlad Mihalcea - */ -public class PostCommentScoreFetchProjectionPerformanceTest extends AbstractPostCommentScorePerformanceTest { - - protected com.codahale.metrics.Timer inMemoryProcessingTimer = metricRegistry.timer("In-memory processing timer"); - - public PostCommentScoreFetchProjectionPerformanceTest(int postCount, int commentCount) { - super(postCount, commentCount); - } - - @Override - protected List postCommentScores(Long postId, int rank) { - return doInJPA(entityManager -> { - long startNanos = System.nanoTime(); - List postCommentScores = entityManager.createQuery( - "select new " + - " com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore(" + - " pc.id, pc.parent.id, pc.review, pc.createdOn, pc.score ) " + - "from PostComment pc " + - "where pc.post.id = :postId ") - .setParameter("postId", postId) - .getResultList(); - - long startInMemoryProcessingNanos = System.nanoTime(); - List roots = new ArrayList<>(); - - if (!postCommentScores.isEmpty()) { - Map postCommentScoreMap = new HashMap<>(); - for(PostCommentScore postCommentScore : postCommentScores) { - Long id = postCommentScore.getId(); - if (!postCommentScoreMap.containsKey(id)) { - postCommentScoreMap.put(id, postCommentScore); - } - } - - for(PostCommentScore postCommentScore : postCommentScores) { - Long parentId = postCommentScore.getParentId(); - if(parentId == null) { - roots.add(postCommentScore); - } else { - PostCommentScore parent = postCommentScoreMap.get(parentId); - parent.addChild(postCommentScore); - } - } - - roots.sort( - Comparator.comparing(PostCommentScore::getTotalScore).reversed() - ); - - if(roots.size() > rank) { - roots = roots.subList(0, rank); - } - } - inMemoryProcessingTimer.update(System.nanoTime() - startInMemoryProcessingNanos, TimeUnit.NANOSECONDS); - timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - return roots; - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostCommentScoreRecursiveCTEPerformanceTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostCommentScoreRecursiveCTEPerformanceTest.java deleted file mode 100644 index d1e38e30a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostCommentScoreRecursiveCTEPerformanceTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive.simple; - -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScoreResultTransformer; -import org.hibernate.SQLQuery; - -import java.util.List; -import java.util.concurrent.TimeUnit; - -/** - * @author Vlad Mihalcea - */ -public class PostCommentScoreRecursiveCTEPerformanceTest extends AbstractPostCommentScorePerformanceTest { - - public PostCommentScoreRecursiveCTEPerformanceTest(int postCount, int commentCount) { - super(postCount, commentCount); - } - - @Override - protected List postCommentScores(Long postId, int rank) { - return doInJPA(entityManager -> { - long startNanos = System.nanoTime(); - List postCommentScores = entityManager.createNativeQuery( - "SELECT id, parent_id, root_id, review, created_on, score " + - "FROM ( " + - " SELECT " + - " id, parent_id, root_id, review, created_on, score, " + - " dense_rank() OVER (ORDER BY total_score DESC) rank " + - " FROM ( " + - " SELECT " + - " id, parent_id, root_id, review, created_on, score, " + - " SUM(score) OVER (PARTITION BY root_id) total_score " + - " FROM (" + - " WITH RECURSIVE post_comment_score(id, root_id, post_id, " + - " parent_id, review, created_on, score) AS (" + - " SELECT " + - " id, id, post_id, parent_id, review, created_on, score" + - " FROM post_comment " + - " WHERE post_id = :postId AND parent_id IS NULL " + - " UNION ALL " + - " SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, " + - " pc.review, pc.created_on, pc.score " + - " FROM post_comment pc " + - " INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id " + - " WHERE pc.parent_id = pcs.id " + - " ) " + - " SELECT id, parent_id, root_id, review, created_on, score " + - " FROM post_comment_score " + - " ) score_by_comment " + - " ) score_total " + - " ORDER BY total_score DESC, id ASC " + - ") total_score_group " + - "WHERE rank <= :rank", "PostCommentScore") - .unwrap(SQLQuery.class) - .setParameter("postId", postId) - .setParameter("rank", rank) - .setResultTransformer(new PostCommentScoreResultTransformer()) - .list(); - timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - return postCommentScores; - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostCommentScoreTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostCommentScoreTest.java deleted file mode 100644 index 8c066371d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostCommentScoreTest.java +++ /dev/null @@ -1,224 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive.simple; - -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScoreResultTransformer; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.SQLQuery; -import org.junit.Test; - -import java.util.*; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class PostCommentScoreTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class - }; - } - - @Override - public void init() { - super.init(); - initData(); - } - - protected void initData() { - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - - PostComment comment1 = new PostComment(); - comment1.setPost(post); - comment1.setReview("Comment 1"); - comment1.setScore(1); - entityManager.persist(comment1); - - PostComment comment1_1 = new PostComment(); - comment1_1.setParent(comment1); - comment1_1.setPost(post); - comment1_1.setReview("Comment 1_1"); - comment1_1.setScore(2); - entityManager.persist(comment1_1); - - PostComment comment1_2 = new PostComment(); - comment1_2.setParent(comment1); - comment1_2.setPost(post); - comment1_2.setReview("Comment 1_2"); - comment1_2.setScore(2); - entityManager.persist(comment1_2); - - PostComment comment1_2_1 = new PostComment(); - comment1_2_1.setParent(comment1_2); - comment1_2_1.setPost(post); - comment1_2_1.setReview("Comment 1_2_1"); - comment1_2_1.setScore(1); - entityManager.persist(comment1_2_1); - - PostComment comment2 = new PostComment(); - comment2.setPost(post); - comment2.setReview("Comment 2"); - comment2.setScore(1); - entityManager.persist(comment2); - - PostComment comment2_1 = new PostComment(); - comment2_1.setParent(comment2); - comment2_1.setPost(post); - comment2_1.setReview("Comment 2_1"); - comment2_1.setScore(1); - entityManager.persist(comment2_1); - - PostComment comment2_2 = new PostComment(); - comment2_2.setParent(comment2); - comment2_2.setPost(post); - comment2_2.setReview("Comment 2_2"); - comment2_2.setScore(1); - entityManager.persist(comment2_2); - - PostComment comment3 = new PostComment(); - comment3.setPost(post); - comment3.setReview("Comment 3"); - comment3.setScore(1); - entityManager.persist(comment3); - - PostComment comment3_1 = new PostComment(); - comment3_1.setParent(comment3); - comment3_1.setPost(post); - comment3_1.setReview("Comment 3_1"); - comment3_1.setScore(10); - entityManager.persist(comment3_1); - - PostComment comment3_2 = new PostComment(); - comment3_2.setParent(comment3); - comment3_2.setPost(post); - comment3_2.setReview("Comment 3_2"); - comment3_2.setScore(-2); - entityManager.persist(comment3_2); - - PostComment comment4 = new PostComment(); - comment4.setPost(post); - comment4.setReview("Comment 4"); - comment4.setScore(-5); - entityManager.persist(comment4); - - PostComment comment5 = new PostComment(); - comment5.setPost(post); - comment5.setReview("Comment 5"); - entityManager.persist(comment5); - - entityManager.flush(); - - }); - } - - @Test - public void test() { - LOGGER.info("Recursive CTE and Window Functions"); - Long postId = 1L; - int rank = 3; - List resultCTEJoin = postCommentScoresCTEJoin(postId, rank); - assertEquals(3, resultCTEJoin.size()); - - List resultInMemory = postCommentScoresInMemory(postId, rank); - assertEquals(3, resultInMemory.size()); - - for (int i = 0; i < resultCTEJoin.size(); i++) { - assertEquals(resultCTEJoin.get(i).getTotalScore(), resultInMemory.get(i).getTotalScore()); - } - } - - protected List postCommentScoresCTEJoin(Long postId, int rank) { - return doInJPA(entityManager -> { - List postCommentScores = entityManager.createNativeQuery( - "SELECT id, parent_id, review, created_on, score " + - "FROM ( " + - " SELECT " + - " id, parent_id, review, created_on, score, " + - " dense_rank() OVER (ORDER BY total_score DESC) rank " + - " FROM ( " + - " SELECT " + - " id, parent_id, review, created_on, score, " + - " SUM(score) OVER (PARTITION BY root_id) total_score " + - " FROM (" + - " WITH RECURSIVE post_comment_score(id, root_id, post_id, " + - " parent_id, review, created_on, score) AS (" + - " SELECT " + - " id, id, post_id, parent_id, review, created_on, score" + - " FROM post_comment " + - " WHERE post_id = :postId AND parent_id IS NULL " + - " UNION ALL " + - " SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, " + - " pc.review, pc.created_on, pc.score " + - " FROM post_comment pc " + - " INNER JOIN post_comment_score pcs " + - " ON pc.parent_id = pcs.id " + - " WHERE pc.parent_id = pcs.id " + - " ) " + - " SELECT id, parent_id, root_id, review, created_on, score " + - " FROM post_comment_score " + - " ) score_by_comment " + - " ) score_total " + - " ORDER BY total_score DESC, id ASC " + - ") total_score_group " + - "WHERE rank <= :rank", "PostCommentScore").unwrap(SQLQuery.class) - .setParameter("postId", postId) - .setParameter("rank", rank) - .setResultTransformer(new PostCommentScoreResultTransformer()) - .list(); - return postCommentScores; - }); - } - - protected List postCommentScoresInMemory(Long postId, int rank) { - return doInJPA(entityManager -> { - List postCommentScores = entityManager.createQuery( - "select new " + - " com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore(" + - " pc.id, pc.parent.id, pc.review, pc.createdOn, pc.score ) " + - "from PostComment pc " + - "where pc.post.id = :postId ") - .setParameter("postId", postId) - .getResultList(); - - List roots = new ArrayList<>(); - - if (!postCommentScores.isEmpty()) { - Map postCommentScoreMap = new HashMap<>(); - for(PostCommentScore postCommentScore : postCommentScores) { - Long id = postCommentScore.getId(); - if (!postCommentScoreMap.containsKey(id)) { - postCommentScoreMap.put(id, postCommentScore); - } - } - - for(PostCommentScore postCommentScore : postCommentScores) { - Long parentId = postCommentScore.getParentId(); - if(parentId == null) { - roots.add(postCommentScore); - } else { - PostCommentScore parent = postCommentScoreMap.get(parentId); - parent.addChild(postCommentScore); - } - } - - roots.sort( - Comparator.comparing(PostCommentScore::getTotalScore).reversed() - ); - - if(roots.size() > rank) { - roots = roots.subList(0, rank); - } - } - return roots; - }); - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/spatial/SpatialTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/spatial/SpatialTest.java deleted file mode 100644 index 095510149..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/spatial/SpatialTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.spatial; - -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Point; -import com.vividsolutions.jts.io.ParseException; -import com.vividsolutions.jts.io.WKTReader; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; - -import org.hibernate.annotations.Type; -import org.hibernate.spatial.dialect.postgis.PostgisDialect; -import org.junit.Test; - -import javax.persistence.Entity; -import javax.persistence.Id; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class SpatialTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Address.class, - }; - } - - @Override - protected DataSourceProvider dataSourceProvider() { - return new PostgreSQLDataSourceProvider() { - @Override - public String hibernateDialect() { - return PostgisDialect.class.getName(); - } - }; - } - - @Test - public void test() { - Long addressId = doInJPA(entityManager -> { - try { - Address address = new Address(); - address.setId(1L); - address.setStreet("5th Avenue"); - address.setNumber("1 A"); - address.setLocation((Point) new WKTReader().read("POINT(60 12)")); - - entityManager.persist(address); - return address.getId(); - } catch (ParseException e) { - throw new RuntimeException(e); - } - }); - - doInJPA(entityManager -> { - Address address = entityManager.find(Address.class, addressId); - Coordinate coordinate = address.getLocation().getCoordinate(); - assertEquals(60.0d, coordinate.getOrdinate(Coordinate.X), 0.1); - assertEquals(12.0d, coordinate.getOrdinate(Coordinate.Y), 0.1); - }); - } - - @Entity(name = "Address") - public static class Address { - - @Id - private Long id; - - private String street; - - private String number; - - @Type(type = "jts_geometry") - private Point location; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getStreet() { - return street; - } - - public void setStreet(String street) { - this.street = street; - } - - public String getNumber() { - return number; - } - - public void setNumber(String number) { - this.number = number; - } - - public Point getLocation() { - return location; - } - - public void setLocation(Point location) { - this.location = location; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/DropFlywayDBTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/DropFlywayDBTest.java deleted file mode 100644 index 8d9eca545..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/DropFlywayDBTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.schema.flyway; - -import com.vladmihalcea.book.hpjp.util.spring.config.jpa.HikariCPPostgreSQLJpaConfiguration; -import org.hibernate.Session; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.support.EncodedResource; -import org.springframework.jdbc.datasource.init.ScriptUtils; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.TransactionException; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.annotation.Resource; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; - -/** - * @author Vlad Mihalcea - */ -@RunWith(SpringJUnit4ClassRunner.class) -//@ContextConfiguration(classes = AbstractHsqldbJpaConfiguration.class) -@ContextConfiguration(classes = HikariCPPostgreSQLJpaConfiguration.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class DropFlywayDBTest { - - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - @PersistenceContext - private EntityManager entityManager; - - @Autowired - private TransactionTemplate transactionTemplate; - - @Resource - private String databaseType; - - private boolean drop = true; - - @Test - public void test() { - if (drop) { - try { - transactionTemplate.execute((TransactionCallback) transactionStatus -> { - Session session = entityManager.unwrap(Session.class); - session.doWork(connection -> { - ScriptUtils.executeSqlScript(connection, - new EncodedResource( - new ClassPathResource( - String.format("flyway/db/%1$s/drop/drop.sql", databaseType) - ) - ), - true, true, - ScriptUtils.DEFAULT_COMMENT_PREFIX, - ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER, - ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER, - ScriptUtils.DEFAULT_COMMENT_PREFIX); - }); - return null; - }); - } catch (TransactionException e) { - LOGGER.error("Failure", e); - } - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/FlywayTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/FlywayTest.java deleted file mode 100644 index 5afe40bd2..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/FlywayTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.schema.flyway; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.TransactionException; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; - -import static com.vladmihalcea.book.hpjp.hibernate.schema.flyway.FlywayEntities.Post; - -/** - * @author Vlad Mihalcea - */ -@RunWith(SpringJUnit4ClassRunner.class) -//@ContextConfiguration(classes = HsqldbFlywayConfiguration.class) -@ContextConfiguration(classes = PostgreSQLFlywayConfiguration.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class FlywayTest { - - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - @PersistenceContext - private EntityManager entityManager; - - @Autowired - private TransactionTemplate transactionTemplate; - - @Test - public void test() { - try { - transactionTemplate.execute((TransactionCallback) transactionStatus -> { - Post post = new Post(); - entityManager.persist(post); - return null; - }); - } catch (TransactionException e) { - LOGGER.error("Failure", e); - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/HsqldbFlywayConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/HsqldbFlywayConfiguration.java deleted file mode 100644 index 78c15a95a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/HsqldbFlywayConfiguration.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.schema.flyway; - -import com.vladmihalcea.book.hpjp.util.spring.config.flyway.AbstractHsqldbFlywayConfiguration; -import org.springframework.context.annotation.Configuration; - -/** - * @author Vlad Mihalcea - */ -@Configuration -public class HsqldbFlywayConfiguration extends AbstractHsqldbFlywayConfiguration { - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/PostgreSQLFlywayConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/PostgreSQLFlywayConfiguration.java deleted file mode 100644 index 5275f7ee4..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/PostgreSQLFlywayConfiguration.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.schema.flyway; - -import com.vladmihalcea.book.hpjp.util.spring.config.flyway.AbstractPostgreSQLFlywayConfiguration; -import org.springframework.context.annotation.Configuration; - -/** - * @author Vlad Mihalcea - */ -@Configuration -public class PostgreSQLFlywayConfiguration extends AbstractPostgreSQLFlywayConfiguration { - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/MySQLStoredProcedureTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/MySQLStoredProcedureTest.java deleted file mode 100644 index 8d35e8804..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/MySQLStoredProcedureTest.java +++ /dev/null @@ -1,275 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.sp; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import org.hibernate.Session; -import org.hibernate.procedure.ProcedureCall; -import org.hibernate.result.Output; -import org.hibernate.result.ResultSetOutput; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.ParameterMode; -import javax.persistence.StoredProcedureQuery; -import java.sql.CallableStatement; -import java.sql.SQLException; -import java.sql.Statement; -import java.sql.Types; -import java.util.List; -import java.util.regex.Pattern; - -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.Post; -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.PostComment; -import static org.junit.Assert.*; - -/** - * @author Vlad Mihalcea - */ -public class MySQLStoredProcedureTest extends AbstractMySQLIntegrationTest { - - private BlogEntityProvider entityProvider = new BlogEntityProvider(); - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Before - public void init() { - super.init(); - doInJDBC(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate("DROP PROCEDURE IF EXISTS count_comments"); - } - catch (SQLException ignore) { - } - }); - doInJDBC(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate("DROP PROCEDURE IF EXISTS post_comments"); - } - catch (SQLException ignore) { - } - }); - doInJDBC(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate("DROP FUNCTION IF EXISTS fn_count_comments"); - } - catch (SQLException ignore) { - } - }); - doInJDBC(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate("DROP PROCEDURE IF EXISTS getStatistics"); - } - catch (SQLException ignore) { - } - }); - doInJDBC(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate( - "CREATE PROCEDURE count_comments (" + - " IN postId INT, " + - " OUT commentCount INT " + - ") " + - "BEGIN " + - " SELECT COUNT(*) INTO commentCount " + - " FROM post_comment " + - " WHERE post_comment.post_id = postId; " + - "END" - ); - statement.executeUpdate( - "CREATE PROCEDURE post_comments(IN postId INT) " + - "BEGIN " + - " SELECT * " + - " FROM post_comment " + - " WHERE post_id = postId; " + - "END" - ); - statement.executeUpdate( - "CREATE FUNCTION fn_count_comments(postId integer) " + - "RETURNS integer " + - "DETERMINISTIC " + - "READS SQL DATA " + - "BEGIN " + - " DECLARE commentCount integer; " + - " SELECT COUNT(*) INTO commentCount " + - " FROM post_comment " + - " WHERE post_comment.post_id = postId; " + - " RETURN commentCount; " + - "END" - ); - statement.executeUpdate( - "CREATE PROCEDURE getStatistics (OUT A BIGINT UNSIGNED, OUT B BIGINT UNSIGNED, OUT C BIGINT UNSIGNED) " + - "BEGIN " + - " SELECT count(*) into A from post; " + - " SELECT count(*) into B from post_comment; " + - " SELECT count(*) into C from tag; " + - "END" - ); - } - }); - doInJPA(entityManager -> { - Post post = new Post(1L); - post.setTitle("Post"); - - PostComment comment1 = new PostComment("Good"); - comment1.setId(1L); - PostComment comment2 = new PostComment("Excellent"); - comment2.setId(2L); - - post.addComment(comment1); - post.addComment(comment2); - entityManager.persist(post); - }); - } - - @Test - public void testStoredProcedureOutParameter() { - doInJPA(entityManager -> { - StoredProcedureQuery query = entityManager.createStoredProcedureQuery("count_comments"); - query.registerStoredProcedureParameter("postId", Long.class, ParameterMode.IN); - query.registerStoredProcedureParameter("commentCount", Long.class, ParameterMode.OUT); - - query.setParameter("postId", 1L); - - query.execute(); - Long commentCount = (Long) query.getOutputParameterValue("commentCount"); - assertEquals(Long.valueOf(2), commentCount); - }); - } - - @Test - public void testHibernateProcedureCallOutParameter() { - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - ProcedureCall call = session.createStoredProcedureCall("getStatistics"); - call.registerParameter("postId", Long.class, ParameterMode.IN).bindValue(1L); - call.registerParameter("commentCount", Long.class, ParameterMode.OUT); - - Long commentCount = (Long) call.getOutputs().getOutputParameterValue("commentCount"); - assertEquals(Long.valueOf(2), commentCount); - }); - } - - @Test - public void testHibernateProcedureCallMultipleOutParameter() { - doInJPA(entityManager -> { - StoredProcedureQuery query = entityManager - .createStoredProcedureQuery("getStatistics") - .registerStoredProcedureParameter( - "A", Long.class, ParameterMode.OUT) - .registerStoredProcedureParameter( - "B", Long.class, ParameterMode.OUT) - .registerStoredProcedureParameter( - "C", Long.class, ParameterMode.OUT); - - query.execute(); - - Long a = (Long) query - .getOutputParameterValue("A"); - Long b = (Long) query - .getOutputParameterValue("B"); - Long c = (Long) query - .getOutputParameterValue("C"); - }); - } - - @Test - public void testStoredProcedureRefCursor() { - try { - doInJPA(entityManager -> { - StoredProcedureQuery query = entityManager.createStoredProcedureQuery("post_comments"); - query.registerStoredProcedureParameter(1, Long.class, ParameterMode.IN); - query.registerStoredProcedureParameter(2, Class.class, ParameterMode.REF_CURSOR); - query.setParameter(1, 1L); - - query.execute(); - List postComments = query.getResultList(); - assertNotNull(postComments); - }); - } catch (Exception e) { - assertTrue(Pattern.compile("Dialect .*? not known to support REF_CURSOR parameters").matcher(e.getCause().getMessage()).matches()); - } - } - - @Test - public void testStoredProcedureReturnValue() { - doInJPA(entityManager -> { - StoredProcedureQuery query = entityManager.createStoredProcedureQuery("post_comments"); - query.registerStoredProcedureParameter(1, Long.class, ParameterMode.IN); - - query.setParameter(1, 1L); - - List postComments = query.getResultList(); - assertEquals(2, postComments.size()); - }); - } - - @Test - public void testHibernateProcedureCallReturnValueParameter() { - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - ProcedureCall call = session.createStoredProcedureCall("post_comments"); - call.registerParameter(1, Long.class, ParameterMode.IN).bindValue(1L); - - Output output = call.getOutputs().getCurrent(); - if (output.isResultSet()) { - List postComments = ((ResultSetOutput) output).getResultList(); - assertEquals(2, postComments.size()); - } - }); - } - - @Test - public void testFunction() { - try { - doInJPA(entityManager -> { - StoredProcedureQuery query = entityManager.createStoredProcedureQuery("fn_count_comments"); - query.registerStoredProcedureParameter("postId", Long.class, ParameterMode.IN); - - query.setParameter("postId", 1L); - - Long commentCount = (Long) query.getSingleResult(); - assertEquals(Long.valueOf(2), commentCount); - }); - } catch (Exception e) { - assertTrue(Pattern.compile("PROCEDURE high_performance_java_persistence.fn_count_comments does not exist").matcher(e.getCause().getCause().getMessage()).matches()); - } - } - - @Test - public void testFunctionWithJDBC() { - doInJPA(entityManager -> { - Session session = entityManager.unwrap( Session.class ); - Integer commentCount = session.doReturningWork( connection -> { - try (CallableStatement function = connection.prepareCall( - "{ ? = call fn_count_comments(?) }" )) { - function.registerOutParameter( 1, Types.INTEGER ); - function.setInt( 2, 1 ); - function.execute(); - return function.getInt( 1 ); - } - } ); - assertEquals(Integer.valueOf(2), commentCount); - }); - } - - /*@Test - public void testFunctionWithJDBCByName() { - doInJPA(entityManager -> { - final AtomicReference commentCount = new AtomicReference<>(); - Session session = entityManager.wrapArray( Session.class ); - session.doWork( connection -> { - try (CallableStatement function = connection.prepareCall( - "{ ? = call fn_count_comments(?) }" )) { - function.registerOutParameter( "", Types.INTEGER ); - function.setInt( "postId", 1 ); - function.execute(); - commentCount.set( function.getInt( 1 ) ); - } - } ); - assertEquals(Integer.valueOf(2), commentCount.get()); - }); - }*/ -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/OracleDeleteGlobalTableStoredProcedureTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/OracleDeleteGlobalTableStoredProcedureTest.java deleted file mode 100644 index 5d976fb24..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/OracleDeleteGlobalTableStoredProcedureTest.java +++ /dev/null @@ -1,247 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.sp; - -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import javax.persistence.*; -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.*; -import java.util.concurrent.TimeUnit; - -/** - * @author Vlad Mihalcea - */ -@RunWith(Parameterized.class) -public class OracleDeleteGlobalTableStoredProcedureTest extends AbstractOracleXEIntegrationTest { - - private final int infoEntryCount; - private final int errorEntryCount; - private final int warnEntryCount; - - private final int totalEntryCount; - - private Date timestamp = Timestamp.valueOf(LocalDateTime.now().minusDays(60)); - private long millisStep; - - private int batchSize = 50; - - public OracleDeleteGlobalTableStoredProcedureTest(int multiplier) { - infoEntryCount = 100 * multiplier; - errorEntryCount = 50 * multiplier; - warnEntryCount = 100 * multiplier; - - totalEntryCount = ( infoEntryCount + errorEntryCount + warnEntryCount ) * 2; - millisStep = ( new Date().getTime() - timestamp.getTime() ) / totalEntryCount; - } - - @Parameterized.Parameters - public static Collection parameters() { - List multipliers = new ArrayList<>(); - /* multipliers.add(new Integer[] {1}); - multipliers.add(new Integer[] {10}); - multipliers.add(new Integer[] {50}); - multipliers.add(new Integer[] {100});*/ - multipliers.add(new Integer[] {500}); -/* multipliers.add(new Integer[] {1000}); - multipliers.add(new Integer[] {2000});*/ - return multipliers; - } - - @Override - protected Class[] entities() { - return new Class[] { - LogEntry.class - }; - } - - @Before - public void init() { - super.init(); - doInJDBC(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate( - "drop table deletable_rowid" - ); - statement.executeUpdate( - "create global temporary table deletable_rowid(rid urowid) on commit preserve rows " - ); - } - }); - - doInJDBC(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate( - "CREATE OR REPLACE PROCEDURE delete_log_entries ( " + - " logLevel IN VARCHAR2, " + - " daysOld IN NUMBER, " + - " batchSize IN NUMBER, " + - " deletedCount OUT NUMBER " + - ") AS " + - " v_row deletable_rowid%rowtype; " + - "BEGIN " + - " insert into deletable_rowid SELECT rowid FROM log_entry WHERE log_level = 'INFO' AND created_on < (SELECT sysdate - 30 FROM dual); " + - " commit; " + - " " + - " deletedCount:=0; " + - " " + - " for v_row in (select * from deletable_rowid x) " + - " loop " + - " deletedCount:=deletedCount+1; " + - " delete from log_entry where rowid=v_row.rid; " + - " if mod(deletedCount,batchSize)=0 then " + - " commit; " + - " end if; " + - " end loop; " + - " commit; " + - "END; " - ); - } - }); - - EntityManager entityManager = entityManagerFactory().createEntityManager(); - EntityTransaction txn = entityManager.getTransaction(); - txn.begin(); - - try { - - int oldEntryThreshold = totalEntryCount / 2; - - long logTimestamp = timestamp.getTime(); - - for (int i = 0; i < totalEntryCount; i++) { - if(i % batchSize == 0 && i > 0) { - txn.commit(); - txn.begin(); - entityManager.clear(); - } - - LogEntry log = new LogEntry(); - int index = i % oldEntryThreshold; - - if(index < infoEntryCount) { - log.setLevel(LogLevel.INFO); - } else if (index < infoEntryCount + errorEntryCount) { - log.setLevel(LogLevel.ERROR); - } else { - log.setLevel(LogLevel.WARN); - } - log.setMessage(log.getLevel().name()); - logTimestamp += millisStep; - log.setCreatedOn(new Date(logTimestamp)); - entityManager.persist(log); - } - } finally { - txn.commit(); - entityManager.close(); - } - } - - @Test - public void testStoredProcedureOutParameter() { - doInJPA(entityManager -> { - long startNanos = System.nanoTime(); - StoredProcedureQuery query = entityManager - .createStoredProcedureQuery("delete_log_entries") - .registerStoredProcedureParameter(1, String.class, ParameterMode.IN) - .registerStoredProcedureParameter(2, Integer.class, ParameterMode.IN) - .registerStoredProcedureParameter(3, Integer.class, ParameterMode.IN) - .registerStoredProcedureParameter(4, Integer.class, ParameterMode.OUT) - .setParameter(1, LogLevel.INFO.name()) - .setParameter(2, 30) - .setParameter(3, 1000); - query.execute(); - - Integer deleteCount = (Integer) query.getOutputParameterValue(4); - long endNanos = System.nanoTime(); - LOGGER.info("Delete {} entries out of {} took {} ms", deleteCount, totalEntryCount, TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos)); - }); - } - - @Test - public void testBulkDelete() { - doInJPA(entityManager -> { - long startNanos = System.nanoTime(); - int deleteCount = entityManager.createQuery( - "DELETE FROM LogEntry " + - "WHERE level = :level AND createdOn < :timestamp") - .setParameter("level", LogLevel.INFO) - .setParameter("timestamp", Timestamp.valueOf(LocalDateTime.now().minusDays(30))) - .executeUpdate(); - long endNanos = System.nanoTime(); - LOGGER.info("Delete {} entries out of {} took {} ms", deleteCount, totalEntryCount, TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos)); - }); - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.jdbc.batch_size", "50"); - properties.put("hibernate.order_inserts", "true"); - properties.put("hibernate.order_updates", "true"); - properties.put("hibernate.jdbc.batch_versioned_data", "true"); - return properties; - } - - public enum LogLevel { - INFO, - WARN, - ERROR - } - - @Entity(name = "LogEntry") - @Table(name = "log_entry") - public static class LogEntry { - - @Id - @GeneratedValue - private Long id; - - @Column(name = "log_level") - @Enumerated(EnumType.STRING) - private LogLevel level; - - @Column(name = "log_message") - private String message; - - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "created_on") - private Date createdOn; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public LogLevel getLevel() { - return level; - } - - public void setLevel(LogLevel level) { - this.level = level; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/OracleDeleteStoredProcedureTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/OracleDeleteStoredProcedureTest.java deleted file mode 100644 index e42553940..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/OracleDeleteStoredProcedureTest.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.sp; - -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import javax.persistence.*; -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.*; -import java.util.concurrent.TimeUnit; - -/** - * @author Vlad Mihalcea - */ -@RunWith(Parameterized.class) -public class OracleDeleteStoredProcedureTest extends AbstractOracleXEIntegrationTest { - - private final int infoEntryCount; - private final int errorEntryCount; - private final int warnEntryCount; - - private final int totalEntryCount; - - private Date timestamp = Timestamp.valueOf(LocalDateTime.now().minusDays(60)); - private long millisStep; - - private int batchSize = 50; - - public OracleDeleteStoredProcedureTest(int multiplier) { - infoEntryCount = 100 * multiplier; - errorEntryCount = 50 * multiplier; - warnEntryCount = 100 * multiplier; - - totalEntryCount = ( infoEntryCount + errorEntryCount + warnEntryCount ) * 2; - millisStep = ( new Date().getTime() - timestamp.getTime() ) / totalEntryCount; - } - - @Parameterized.Parameters - public static Collection parameters() { - List multipliers = new ArrayList<>(); - multipliers.add(new Integer[] {1}); - multipliers.add(new Integer[] {10}); - multipliers.add(new Integer[] {50}); - multipliers.add(new Integer[] {100}); - multipliers.add(new Integer[] {500}); - multipliers.add(new Integer[] {1000}); - multipliers.add(new Integer[] {2000}); - return multipliers; - } - - @Override - protected Class[] entities() { - return new Class[] { - LogEntry.class - }; - } - - @Before - public void init() { - super.init(); - doInJDBC(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate( - "CREATE OR REPLACE PROCEDURE delete_log_entries ( " + - " logLevel IN VARCHAR2, " + - " daysOld IN NUMBER, " + - " batchSize IN NUMBER, " + - " deletedCount OUT NUMBER " + - ") AS " + - " TYPE ARRAY_NUMBER IS TABLE OF NUMBER; " + - " ids ARRAY_NUMBER; " + - " CURSOR select_cursor IS " + - " SELECT id " + - " FROM log_entry " + - " WHERE log_level = logLevel AND created_on < (SELECT sysdate - daysOld FROM dual); " + - "BEGIN " + - " deletedCount := 0; " + - " OPEN select_cursor; " + - " LOOP " + - " FETCH select_cursor BULK COLLECT INTO ids LIMIT batchSize; " + - " FORALL i IN 1 .. ids.COUNT " + - " DELETE FROM log_entry WHERE id = ids(i); " + - " deletedCount := deletedCount + sql%rowcount; " + - " COMMIT; " + - " EXIT WHEN select_cursor%NOTFOUND; " + - " END LOOP; " + - "CLOSE select_cursor; " + - "EXCEPTION " + - " WHEN NO_DATA_FOUND THEN NULL; " + - " WHEN OTHERS THEN RAISE; " + - "END delete_log_entries;" - ); - } - }); - - EntityManager entityManager = entityManagerFactory().createEntityManager(); - EntityTransaction txn = entityManager.getTransaction(); - txn.begin(); - - try { - - int oldEntryThreshold = totalEntryCount / 2; - - long logTimestamp = timestamp.getTime(); - - for (int i = 0; i < totalEntryCount; i++) { - if(i % batchSize == 0 && i > 0) { - txn.commit(); - txn.begin(); - entityManager.clear(); - } - - LogEntry log = new LogEntry(); - int index = i % oldEntryThreshold; - - if(index < infoEntryCount) { - log.setLevel(LogLevel.INFO); - } else if (index < infoEntryCount + errorEntryCount) { - log.setLevel(LogLevel.ERROR); - } else { - log.setLevel(LogLevel.WARN); - } - log.setMessage(log.getLevel().name()); - logTimestamp += millisStep; - log.setCreatedOn(new Date(logTimestamp)); - entityManager.persist(log); - } - } finally { - txn.commit(); - entityManager.close(); - } - } - - @Test - public void testStoredProcedureOutParameter() { - doInJPA(entityManager -> { - long startNanos = System.nanoTime(); - StoredProcedureQuery query = entityManager - .createStoredProcedureQuery("delete_log_entries") - .registerStoredProcedureParameter(1, String.class, ParameterMode.IN) - .registerStoredProcedureParameter(2, Integer.class, ParameterMode.IN) - .registerStoredProcedureParameter(3, Integer.class, ParameterMode.IN) - .registerStoredProcedureParameter(4, Integer.class, ParameterMode.OUT) - .setParameter(1, LogLevel.INFO.name()) - .setParameter(2, 30) - .setParameter(3, 1000); - query.execute(); - - Integer deleteCount = (Integer) query.getOutputParameterValue(4); - long endNanos = System.nanoTime(); - LOGGER.info("Delete {} entries out of {} took {} ms", deleteCount, totalEntryCount, TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos)); - }); - } - - @Test - public void testBulkDelete() { - doInJPA(entityManager -> { - long startNanos = System.nanoTime(); - int deleteCount = entityManager.createQuery( - "DELETE FROM LogEntry " + - "WHERE level = :level AND createdOn < :timestamp") - .setParameter("level", LogLevel.INFO) - .setParameter("timestamp", Timestamp.valueOf(LocalDateTime.now().minusDays(30))) - .executeUpdate(); - long endNanos = System.nanoTime(); - LOGGER.info("Delete {} entries out of {} took {} ms", deleteCount, totalEntryCount, TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos)); - }); - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put("hibernate.jdbc.batch_size", "50"); - properties.put("hibernate.order_inserts", "true"); - properties.put("hibernate.order_updates", "true"); - properties.put("hibernate.jdbc.batch_versioned_data", "true"); - return properties; - } - - public enum LogLevel { - INFO, - WARN, - ERROR - } - - @Entity(name = "LogEntry") - @Table(name = "log_entry") - public static class LogEntry { - - @Id - @GeneratedValue - private Long id; - - @Column(name = "log_level") - @Enumerated(EnumType.STRING) - private LogLevel level; - - @Column(name = "log_message") - private String message; - - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "created_on") - private Date createdOn; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public LogLevel getLevel() { - return level; - } - - public void setLevel(LogLevel level) { - this.level = level; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/PostgreSQLStoredProcedureTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/PostgreSQLStoredProcedureTest.java deleted file mode 100644 index 1a4bbb714..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/PostgreSQLStoredProcedureTest.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.sp; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import org.hibernate.Session; -import org.hibernate.procedure.ProcedureCall; -import org.hibernate.result.Output; -import org.hibernate.result.ResultSetOutput; -import org.junit.Before; -import org.junit.Test; - -import javax.persistence.ParameterMode; -import javax.persistence.StoredProcedureQuery; -import java.sql.*; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; - -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.Post; -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.PostComment; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * @author Vlad Mihalcea - */ -public class PostgreSQLStoredProcedureTest extends AbstractPostgreSQLIntegrationTest { - - private BlogEntityProvider entityProvider = new BlogEntityProvider(); - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Before - public void init() { - super.init(); - doInJDBC(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate("DROP FUNCTION count_comments(bigint)"); - } - catch (SQLException ignore) { - } - }); - doInJDBC(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate("DROP FUNCTION post_comments(bigint)"); - } - catch (SQLException ignore) { - } - }); - doInJDBC(connection -> { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate( - "CREATE OR REPLACE FUNCTION count_comments( " + - " IN postId bigint, " + - " OUT commentCount bigint) " + - " RETURNS bigint AS " + - "$BODY$ " + - " BEGIN " + - " SELECT COUNT(*) INTO commentCount " + - " FROM post_comment " + - " WHERE post_id = postId; " + - " END; " + - "$BODY$ " + - "LANGUAGE plpgsql;" - ); - - statement.executeUpdate( - "CREATE OR REPLACE FUNCTION post_comments(postId BIGINT) " + - " RETURNS REFCURSOR AS " + - "$BODY$ " + - " DECLARE " + - " postComments REFCURSOR; " + - " BEGIN " + - " OPEN postComments FOR " + - " SELECT * " + - " FROM post_comment " + - " WHERE post_id = postId; " + - " RETURN postComments; " + - " END; " + - "$BODY$ " + - "LANGUAGE plpgsql" - ); - } - }); - doInJPA(entityManager -> { - Post post = new Post(1L); - post.setTitle("Post"); - - PostComment comment1 = new PostComment("Good"); - comment1.setId(1L); - PostComment comment2 = new PostComment("Excellent"); - comment2.setId(2L); - - post.addComment(comment1); - post.addComment(comment2); - entityManager.persist(post); - }); - } - - @Test - public void testStoredProcedureOutParameter() { - doInJPA(entityManager -> { - StoredProcedureQuery query = entityManager - .createStoredProcedureQuery("count_comments") - .registerStoredProcedureParameter("postId", Long.class, ParameterMode.IN) - .registerStoredProcedureParameter("commentCount", Long.class, ParameterMode.OUT) - .setParameter("postId", 1L); - query.execute(); - Long commentCount = (Long) query.getOutputParameterValue("commentCount"); - assertEquals(Long.valueOf(2), commentCount); - }); - } - - - @Test - public void testStoredProcedureRefCursor() { - doInJPA(entityManager -> { - StoredProcedureQuery query = entityManager - .createStoredProcedureQuery("post_comments") - .registerStoredProcedureParameter(1, void.class, ParameterMode.REF_CURSOR) - .registerStoredProcedureParameter(2, Long.class, ParameterMode.IN) - .setParameter(2, 1L); - - List postComments = query.getResultList(); - assertEquals(2, postComments.size()); - }); - } - - @Test - public void testHibernateProcedureCallRefCursor() { - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - ProcedureCall call = session - .createStoredProcedureCall("post_comments"); - call.registerParameter(1, void.class, ParameterMode.REF_CURSOR); - call.registerParameter(2, Long.class, ParameterMode.IN).bindValue(1L); - - Output output = call.getOutputs().getCurrent(); - if (output.isResultSet()) { - List postComments = ((ResultSetOutput) output).getResultList(); - assertEquals(2, postComments.size()); - } - }); - } - - @Test - public void testFunctionWithJDBC() { - doInJPA(entityManager -> { - Session session = entityManager.unwrap( Session.class ); - Long commentCount = session.doReturningWork( connection -> { - try (CallableStatement function = connection.prepareCall( - "{ ? = call count_comments(?) }" )) { - function.registerOutParameter( 1, Types.BIGINT ); - function.setLong( 2, 1L ); - function.execute(); - return function.getLong( 1 ); - } - } ); - assertEquals(Long.valueOf(2), commentCount); - }); - } - - @Test - public void testFunctionWithJDBCByName() { - try { - doInJPA(entityManager -> { - final AtomicReference commentCount = new AtomicReference<>(); - Session session = entityManager.unwrap( Session.class ); - session.doWork( connection -> { - try (CallableStatement function = connection.prepareCall( - "{ ? = call count_comments(?) }" )) { - function.registerOutParameter( "commentCount", Types.BIGINT ); - function.setLong( "postId", 1L ); - function.execute(); - commentCount.set( function.getLong( 1 ) ); - } - } ); - assertEquals(Long.valueOf(2), commentCount.get()); - }); - } catch (Exception e) { - assertEquals(SQLFeatureNotSupportedException.class, e.getCause().getClass()); - } - } - - @Test - public void test_hql_bit_length_function_example() { - doInJPA(entityManager -> { - List bits = entityManager.createQuery( - "select bit_length( c.title ) " + - "from Post c ", Number.class ) - .getResultList(); - assertFalse(bits.isEmpty()); - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/statistics/ConnectionStatisticsTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/statistics/ConnectionStatisticsTest.java deleted file mode 100644 index 5fbd90fa4..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/statistics/ConnectionStatisticsTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.statistics; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import org.hibernate.cfg.AvailableSettings; -import org.junit.Test; - -import java.util.Properties; - -/** - * ConnectionStatisticsTest - Test Hibernate statistics - * - * @author Vlad Mihalcea - */ -public class ConnectionStatisticsTest extends AbstractTest { - - private BlogEntityProvider entityProvider = new BlogEntityProvider(); - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Test - public void testJdbcOneToManyMapping() { - doInJPA(connection -> { - - }); - doInJPA(connection -> { - - }); - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.put(AvailableSettings.GENERATE_STATISTICS, "true"); - properties.put("hibernate.stats.factory", - TransactionStatisticsFactory.class.getName()); - return properties; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/statistics/TransactionStatistics.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/statistics/TransactionStatistics.java deleted file mode 100644 index 37755cd03..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/statistics/TransactionStatistics.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.statistics; - -import org.hibernate.stat.internal.ConcurrentStatisticsImpl; - -import java.util.concurrent.atomic.AtomicLong; - -/** - * @author Vlad Mihalcea - */ -public class TransactionStatistics extends ConcurrentStatisticsImpl { - - private static final ThreadLocal startNanos = new ThreadLocal() { - @Override protected AtomicLong initialValue() { - return new AtomicLong(); - } - }; - - private static final ThreadLocal connectionCounter = new ThreadLocal() { - @Override protected AtomicLong initialValue() { - return new AtomicLong(); - } - }; - - private StatisticsReport report = new StatisticsReport(); - - @Override public void connect() { - connectionCounter.get().incrementAndGet(); - startNanos.get().compareAndSet(0, System.nanoTime()); - super.connect(); - } - - @Override public void endTransaction(boolean success) { - try { - report.transactionTime(System.nanoTime() - startNanos.get().get()); - report.connectionsCount(connectionCounter.get().get()); - report.generate(); - } finally { - startNanos.remove(); - connectionCounter.remove(); - } - super.endTransaction(success); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/statistics/TransactionStatisticsFactory.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/statistics/TransactionStatisticsFactory.java deleted file mode 100644 index 6d56da833..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/statistics/TransactionStatisticsFactory.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.statistics; - -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.stat.spi.StatisticsFactory; -import org.hibernate.stat.spi.StatisticsImplementor; - -/** - * @author Vlad Mihalcea - */ -public class TransactionStatisticsFactory implements StatisticsFactory { - - @Override - public StatisticsImplementor buildStatistics(SessionFactoryImplementor sessionFactory) { - return new TransactionStatistics(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/Book.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/Book.java deleted file mode 100644 index 91c13d085..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/Book.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.time; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; -import java.sql.Timestamp; -import java.util.Date; - -/** - * - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "book") -public class Book { - - @Id - private Long id; - - private String title; - - @Column(name = "created_by") - private String createdBy; - - @Column(name = "created_on") - private Timestamp createdOn; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Timestamp createdOn) { - this.createdOn = createdOn; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/forum/Post.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/forum/Post.java deleted file mode 100644 index 5084b7d82..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/forum/Post.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.forum; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "post") -public class Post { - - @Id - @GeneratedValue - private Long id; - - private String title; - - public Post() { - } - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true, fetch = FetchType.LAZY) - private PostDetails details; - - @ManyToMany - @JoinTable(name = "post_tag", - joinColumns = @JoinColumn(name = "post_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id") - ) - private List tags = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public PostDetails getDetails() { - return details; - } - - public List getTags() { - return tags; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - - public void addDetails(PostDetails details) { - this.details = details; - details.setPost(this); - } - - public void removeDetails() { - this.details.setPost(null); - this.details = null; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/forum/PostComment.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/forum/PostComment.java deleted file mode 100644 index 380aeaf49..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/forum/PostComment.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.forum; - -import javax.persistence.*; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "post_comment") -public class PostComment { - - @Id - @GeneratedValue - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public PostComment() { - } - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/forum/PostDetails.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/forum/PostDetails.java deleted file mode 100644 index 68fa660ef..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/forum/PostDetails.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.forum; - -import javax.persistence.*; -import java.util.Date; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "post_details") -public class PostDetails { - - @Id - @GeneratedValue - private Long id; - - @Column(name = "created_on") - private Date createdOn; - - @Column(name = "created_by") - private String createdBy; - - public PostDetails() { - createdOn = new Date(); - } - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") - @MapsId - private Post post; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/forum/Tag.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/forum/Tag.java deleted file mode 100644 index bd0d77029..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/forum/Tag.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.forum; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Table; - -/** - * @author Vlad Mihalcea - */ -@Entity -@Table(name = "tag") -public class Tag { - - @Id - @GeneratedValue - private Long id; - - private String name; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/HibernateTransactionManagerTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/HibernateTransactionManagerTest.java deleted file mode 100644 index c7184e480..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/HibernateTransactionManagerTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Tag; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate.config.HibernateTransactionManagerConfiguration; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate.service.ForumService; -import org.hibernate.SessionFactory; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.TransactionException; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = HibernateTransactionManagerConfiguration.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class HibernateTransactionManagerTest { - - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - @Autowired - private TransactionTemplate transactionTemplate; - - @Autowired - private SessionFactory sessionFactory; - - @Autowired - private ForumService forumService; - - @Before - public void init() { - try { - transactionTemplate.execute((TransactionCallback) transactionStatus -> { - Tag hibernate = new Tag(); - hibernate.setName("hibernate"); - sessionFactory.getCurrentSession().persist(hibernate); - - Tag jpa = new Tag(); - jpa.setName("jpa"); - sessionFactory.getCurrentSession().persist(jpa); - return null; - }); - } catch (TransactionException e) { - LOGGER.error("Failure", e); - } - - } - - @Test - public void test() { - Post post = forumService.newPost("High-Performance Java Persistence", "hibernate", "jpa"); - assertNotNull(post.getId()); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/config/HibernateTransactionManagerConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/config/HibernateTransactionManagerConfiguration.java deleted file mode 100644 index c174bb311..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/config/HibernateTransactionManagerConfiguration.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate.config; - -import com.vladmihalcea.book.hpjp.util.DataSourceProxyType; -import com.vladmihalcea.book.hpjp.util.logging.InlineQueryLogEntryCreator; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import net.ttddyy.dsproxy.listener.SLF4JQueryLoggingListener; -import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; -import org.hibernate.SessionFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.*; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.orm.hibernate5.HibernateTransactionManager; -import org.springframework.orm.hibernate5.LocalSessionFactoryBean; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.sql.DataSource; -import java.util.Properties; - -/** - * - * @author Vlad Mihalcea - */ -@Configuration -@PropertySource({"/META-INF/jdbc-hsqldb.properties"}) -@ComponentScan(basePackages = "com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate") -@EnableTransactionManagement -@EnableAspectJAutoProxy -public class HibernateTransactionManagerConfiguration { - - public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); - - @Value("${jdbc.dataSourceClassName}") - private String dataSourceClassName; - - @Value("${jdbc.url}") - private String jdbcUrl; - - @Value("${jdbc.username}") - private String jdbcUser; - - @Value("${jdbc.password}") - private String jdbcPassword; - - @Value("${hibernate.dialect}") - private String hibernateDialect; - - @Bean(destroyMethod = "close") - public DataSource actualDataSource() { - Properties driverProperties = new Properties(); - driverProperties.setProperty("url", jdbcUrl); - driverProperties.setProperty("user", jdbcUser); - driverProperties.setProperty("password", jdbcPassword); - - Properties properties = new Properties(); - properties.put("dataSourceClassName", dataSourceClassName); - properties.put("dataSourceProperties", driverProperties); - properties.setProperty("maximumPoolSize", String.valueOf(3)); - return new HikariDataSource(new HikariConfig(properties)); - } - - @Bean - public static PropertySourcesPlaceholderConfigurer properties() { - return new PropertySourcesPlaceholderConfigurer(); - } - - @Bean - public DataSource dataSource() { - SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); - loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); - return ProxyDataSourceBuilder - .create(actualDataSource()) - .name(DATA_SOURCE_PROXY_NAME) - .listener(loggingListener) - .build(); - } - - @Bean - public LocalSessionFactoryBean sessionFactory() { - LocalSessionFactoryBean localSessionFactoryBean = new LocalSessionFactoryBean(); - localSessionFactoryBean.setDataSource(dataSource()); - localSessionFactoryBean.setPackagesToScan(packagesToScan()); - localSessionFactoryBean.setHibernateProperties(additionalProperties()); - return localSessionFactoryBean; - } - - @Bean - public HibernateTransactionManager transactionManager(SessionFactory sessionFactory){ - HibernateTransactionManager transactionManager = new HibernateTransactionManager(); - transactionManager.setSessionFactory(sessionFactory); - return transactionManager; - } - - @Bean - public TransactionTemplate transactionTemplate(SessionFactory sessionFactory) { - return new TransactionTemplate(transactionManager(sessionFactory)); - } - - protected Properties additionalProperties() { - Properties properties = new Properties(); - properties.setProperty("hibernate.dialect", hibernateDialect); - properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); - return properties; - } - - protected String[] packagesToScan() { - return new String[]{ - "com.vladmihalcea.book.hpjp.hibernate.transaction.forum" - }; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/GenericDAO.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/GenericDAO.java deleted file mode 100644 index ff4829a29..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/GenericDAO.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate.dao; - -import java.io.Serializable; - -/** - * @author Vlad Mihalcea - */ -public interface GenericDAO { - - T findById(ID id); - - T persist(T entity); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/GenericDAOImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/GenericDAOImpl.java deleted file mode 100644 index 520fe0f8b..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/GenericDAOImpl.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate.dao; - -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -import java.io.Serializable; - -/** - * @author Vlad Mihalcea - */ -@Repository -@Transactional -public abstract class GenericDAOImpl implements GenericDAO { - - @Autowired - private SessionFactory sessionFactory; - - private final Class entityClass; - - protected SessionFactory getSessionFactory() { - return sessionFactory; - } - - protected Session getSession() { - return sessionFactory.getCurrentSession(); - } - - protected GenericDAOImpl(Class entityClass) { - this.entityClass = entityClass; - } - - @Override - public T findById(ID id) { - return getSession().get(entityClass, id); - } - - @Override - public T persist(T entity) { - getSession().persist(entity); - return entity; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/PostDAO.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/PostDAO.java deleted file mode 100644 index 1c007cfd7..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/PostDAO.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; - -/** - * @author Vlad Mihalcea - */ -public interface PostDAO extends GenericDAO { - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/PostDAOImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/PostDAOImpl.java deleted file mode 100644 index 3bcaa2796..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/PostDAOImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import org.springframework.stereotype.Repository; - -/** - * @author Vlad Mihalcea - */ -@Repository -public class PostDAOImpl extends GenericDAOImpl implements PostDAO { - - protected PostDAOImpl() { - super(Post.class); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/TagDAO.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/TagDAO.java deleted file mode 100644 index 0c1f57299..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/TagDAO.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Tag; - -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public interface TagDAO extends GenericDAO { - - List findByName(String... tags); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/TagDAOImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/TagDAOImpl.java deleted file mode 100644 index 192f5f7f3..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/dao/TagDAOImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Tag; -import org.springframework.stereotype.Repository; - -import java.util.Arrays; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -@Repository -public class TagDAOImpl extends GenericDAOImpl implements TagDAO { - - protected TagDAOImpl() { - super(Tag.class); - } - - @Override - public List findByName(String... tags) { - if(tags.length == 0) { - throw new IllegalArgumentException("There's no tag name to search for!"); - } - return getSession().createQuery( - "select t " + - "from Tag t " + - "where t.name in :tags") - .setParameterList("tags", Arrays.asList(tags)) - .list(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/service/ForumService.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/service/ForumService.java deleted file mode 100644 index 60a1d52ef..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/service/ForumService.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate.service; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import org.springframework.stereotype.Service; - -/** - * @author Vlad Mihalcea - */ -@Service -public interface ForumService { - - Post newPost(String title, String... tags); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/service/ForumServiceImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/service/ForumServiceImpl.java deleted file mode 100644 index baac15bbd..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/hibernate/service/ForumServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate.service; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate.dao.PostDAO; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.hibernate.dao.TagDAO; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -/** - * @author Vlad Mihalcea - */ -@Repository -public class ForumServiceImpl implements ForumService { - - @Autowired - private PostDAO postDAO; - - @Autowired - private TagDAO tagDAO; - - @Override - @Transactional - public Post newPost(String title, String... tags) { - Post post = new Post(); - post.setTitle(title); - post.getTags().addAll(tagDAO.findByName(tags)); - return postDAO.persist(post); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/JPATransactionManagerTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/JPATransactionManagerTest.java deleted file mode 100644 index 2a1da6036..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/JPATransactionManagerTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Tag; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.config.JPATransactionManagerConfiguration; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.dao.PostBatchDAO; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.service.ForumService; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.TransactionException; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; - -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = JPATransactionManagerConfiguration.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class JPATransactionManagerTest { - - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - @Autowired - private TransactionTemplate transactionTemplate; - - @PersistenceContext - private EntityManager entityManager; - - @Autowired - private ForumService forumService; - - @Autowired - private PostBatchDAO postBatchDAO; - - @Test - public void test() { - try { - transactionTemplate.execute((TransactionCallback) transactionStatus -> { - Tag hibernate = new Tag(); - hibernate.setName("hibernate"); - entityManager.persist(hibernate); - - Tag jpa = new Tag(); - jpa.setName("jpa"); - entityManager.persist(jpa); - return null; - }); - } catch (TransactionException e) { - LOGGER.error("Failure", e); - } - - try { - transactionTemplate.execute((TransactionCallback) transactionStatus -> { - postBatchDAO.savePosts(); - return null; - }); - } catch (TransactionException e) { - LOGGER.error("Failure", e); - } - - Post post = forumService.newPost("High-Performance Java Persistence", "hibernate", "jpa"); - assertNotNull(post.getId()); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/config/JPATransactionManagerConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/config/JPATransactionManagerConfiguration.java deleted file mode 100644 index a00d28f07..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/config/JPATransactionManagerConfiguration.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.config; - -import com.vladmihalcea.book.hpjp.util.DataSourceProxyType; -import com.vladmihalcea.book.hpjp.util.logging.InlineQueryLogEntryCreator; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import net.ttddyy.dsproxy.listener.SLF4JQueryLoggingListener; -import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; -import org.hibernate.jpa.HibernatePersistenceProvider; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.*; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.JpaVendorAdapter; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.persistence.EntityManagerFactory; -import javax.sql.DataSource; -import java.util.Properties; - -/** - * - * @author Vlad Mihalcea - */ -@Configuration -@PropertySource({"/META-INF/jdbc-hsqldb.properties"}) -@ComponentScan(basePackages = "com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa") -@EnableTransactionManagement -@EnableAspectJAutoProxy -public class JPATransactionManagerConfiguration { - - public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); - - @Value("${jdbc.dataSourceClassName}") - private String dataSourceClassName; - - @Value("${jdbc.url}") - private String jdbcUrl; - - @Value("${jdbc.username}") - private String jdbcUser; - - @Value("${jdbc.password}") - private String jdbcPassword; - - @Value("${hibernate.dialect}") - private String hibernateDialect; - - @Bean - public static PropertySourcesPlaceholderConfigurer properties() { - return new PropertySourcesPlaceholderConfigurer(); - } - - @Bean(destroyMethod = "close") - public DataSource actualDataSource() { - Properties driverProperties = new Properties(); - driverProperties.setProperty("url", jdbcUrl); - driverProperties.setProperty("user", jdbcUser); - driverProperties.setProperty("password", jdbcPassword); - - Properties properties = new Properties(); - properties.put("dataSourceClassName", dataSourceClassName); - properties.put("dataSourceProperties", driverProperties); - properties.setProperty("maximumPoolSize", String.valueOf(3)); - return new HikariDataSource(new HikariConfig(properties)); - } - - @Bean - public DataSource dataSource() { - SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); - loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); - return ProxyDataSourceBuilder - .create(actualDataSource()) - .name(DATA_SOURCE_PROXY_NAME) - .listener(loggingListener) - .build(); - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactory() { - LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - localContainerEntityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); - localContainerEntityManagerFactoryBean.setPersistenceProvider(new HibernatePersistenceProvider()); - localContainerEntityManagerFactoryBean.setDataSource(dataSource()); - localContainerEntityManagerFactoryBean.setPackagesToScan(packagesToScan()); - - JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); - localContainerEntityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); - localContainerEntityManagerFactoryBean.setJpaProperties(additionalProperties()); - return localContainerEntityManagerFactoryBean; - } - - @Bean - public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ - JpaTransactionManager transactionManager = new JpaTransactionManager(); - transactionManager.setEntityManagerFactory(entityManagerFactory); - return transactionManager; - } - - @Bean - public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { - return new TransactionTemplate(transactionManager(entityManagerFactory)); - } - - protected Properties additionalProperties() { - Properties properties = new Properties(); - properties.setProperty("hibernate.dialect", hibernateDialect); - properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); - return properties; - } - - protected String[] packagesToScan() { - return new String[]{ - "com.vladmihalcea.book.hpjp.hibernate.transaction.forum" - }; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/GenericDAO.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/GenericDAO.java deleted file mode 100644 index 7e5e3a8bf..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/GenericDAO.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.dao; - -import java.io.Serializable; - -/** - * @author Vlad Mihalcea - */ -public interface GenericDAO { - - T findById(ID id); - - T persist(T entity); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/GenericDAOImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/GenericDAOImpl.java deleted file mode 100644 index c18fa559b..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/GenericDAOImpl.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.dao; - -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.io.Serializable; - -/** - * @author Vlad Mihalcea - */ -@Repository -@Transactional -public abstract class GenericDAOImpl implements GenericDAO { - - @PersistenceContext - private EntityManager entityManager; - - private final Class entityClass; - - protected EntityManager getEntityManager() { - return entityManager; - } - - protected GenericDAOImpl(Class entityClass) { - this.entityClass = entityClass; - } - - @Override - public T findById(ID id) { - return entityManager.find(entityClass, id); - } - - @Override - public T persist(T entity) { - entityManager.persist(entity); - return entity; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/PostBatchDAO.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/PostBatchDAO.java deleted file mode 100644 index 0d4e94daf..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/PostBatchDAO.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; - -/** - * @author Vlad Mihalcea - */ -public interface PostBatchDAO extends GenericDAO { - - void savePosts(); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/PostBatchDAOImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/PostBatchDAOImpl.java deleted file mode 100644 index c13e7c21a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/PostBatchDAOImpl.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import org.hibernate.Session; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import javax.persistence.PersistenceContextType; - -/** - * @author Vlad Mihalcea - */ -@Repository -public class PostBatchDAOImpl extends GenericDAOImpl implements PostBatchDAO { - - @PersistenceContext(type = PersistenceContextType.EXTENDED) - private EntityManager entityManager; - - int entityCount = 10; - - protected PostBatchDAOImpl() { - super(Post.class); - } - - @Transactional - public void savePosts() { - entityManager.unwrap(Session.class).setJdbcBatchSize(10); - try { - for ( long i = 0; i < entityCount; ++i ) { - Post post = new Post(); - post.setTitle(String.format( "Post nr %d", i )); - entityManager.persist( post ); - } - entityManager.flush(); - } finally { - entityManager.unwrap(Session.class).setJdbcBatchSize(null); - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/PostDAO.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/PostDAO.java deleted file mode 100644 index e59e789cd..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/PostDAO.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.dao.GenericDAO; - -/** - * @author Vlad Mihalcea - */ -public interface PostDAO extends GenericDAO { - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/PostDAOImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/PostDAOImpl.java deleted file mode 100644 index c1f1eb40d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/PostDAOImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import org.springframework.stereotype.Repository; - -/** - * @author Vlad Mihalcea - */ -@Repository -public class PostDAOImpl extends GenericDAOImpl implements PostDAO { - - protected PostDAOImpl() { - super(Post.class); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/TagDAO.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/TagDAO.java deleted file mode 100644 index b0b7cc82f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/TagDAO.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Tag; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.dao.GenericDAO; - -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public interface TagDAO extends GenericDAO { - - List findByName(String... tags); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/TagDAOImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/TagDAOImpl.java deleted file mode 100644 index 2d01dadd2..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/dao/TagDAOImpl.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Tag; -import org.hibernate.Session; -import org.springframework.stereotype.Repository; - -import java.util.Arrays; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -@Repository -public class TagDAOImpl extends GenericDAOImpl implements TagDAO { - - protected TagDAOImpl() { - super(Tag.class); - } - - @Override - public List findByName(String... tags) { - if(tags.length == 0) { - throw new IllegalArgumentException("There's no tag name to search for!"); - } - Session session = getEntityManager().unwrap(Session.class); - - return getEntityManager().createQuery( - "select t " + - "from Tag t " + - "where t.name in :tags", Tag.class) - .setParameter("tags", Arrays.asList(tags)) - .getResultList(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/service/ForumService.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/service/ForumService.java deleted file mode 100644 index f10372f6a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/service/ForumService.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.service; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import org.springframework.stereotype.Service; - -/** - * @author Vlad Mihalcea - */ -@Service -public interface ForumService { - - Post newPost(String title, String... tags); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/service/ForumServiceImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/service/ForumServiceImpl.java deleted file mode 100644 index 3fc9f7bd3..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jpa/service/ForumServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.service; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.dao.PostDAO; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jpa.dao.TagDAO; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -/** - * @author Vlad Mihalcea - */ -@Repository -public class ForumServiceImpl implements ForumService { - - @Autowired - private PostDAO postDAO; - - @Autowired - private TagDAO tagDAO; - - @Override - @Transactional - public Post newPost(String title, String... tags) { - Post post = new Post(); - post.setTitle(title); - post.getTags().addAll(tagDAO.findByName(tags)); - return postDAO.persist(post); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/JTATransactionManagerTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/JTATransactionManagerTest.java deleted file mode 100644 index ad051108a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/JTATransactionManagerTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Tag; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta.config.JTATransactionManagerConfiguration; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta.service.ForumService; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.TransactionException; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; - -import static org.junit.Assert.assertNotNull; - -/** - * @author Vlad Mihalcea - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = JTATransactionManagerConfiguration.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class JTATransactionManagerTest { - - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - @Autowired - private TransactionTemplate transactionTemplate; - - @PersistenceContext - private EntityManager entityManager; - - @Autowired - private ForumService forumService; - - @Test - public void test() { - try { - transactionTemplate.execute((TransactionCallback) transactionStatus -> { - Tag hibernate = new Tag(); - hibernate.setName("hibernate"); - entityManager.persist(hibernate); - - Tag jpa = new Tag(); - jpa.setName("jpa"); - entityManager.persist(jpa); - return null; - }); - } catch (TransactionException e) { - LOGGER.error("Failure", e); - } - - Post post = forumService.newPost("High-Performance Java Persistence", "hibernate", "jpa"); - assertNotNull(post.getId()); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/config/JTATransactionManagerConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/config/JTATransactionManagerConfiguration.java deleted file mode 100644 index bc1db0729..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/config/JTATransactionManagerConfiguration.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta.config; - -import bitronix.tm.BitronixTransactionManager; -import bitronix.tm.TransactionManagerServices; -import bitronix.tm.resource.jdbc.PoolingDataSource; -import com.vladmihalcea.book.hpjp.util.DataSourceProxyType; -import com.vladmihalcea.book.hpjp.util.logging.InlineQueryLogEntryCreator; -import net.ttddyy.dsproxy.listener.SLF4JQueryLoggingListener; -import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; -import org.hibernate.engine.transaction.jta.platform.internal.BitronixJtaPlatform; -import org.hibernate.jpa.HibernatePersistenceProvider; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.*; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.orm.jpa.JpaVendorAdapter; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.transaction.jta.JtaTransactionManager; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.sql.DataSource; -import java.util.Properties; - -/** - * - * @author Vlad Mihalcea - */ -@Configuration -@PropertySource({"/META-INF/jta-hsqldb.properties"}) -@ComponentScan(basePackages = "com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta") -@EnableTransactionManagement -@EnableAspectJAutoProxy -public class JTATransactionManagerConfiguration { - - public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); - - @Value("${jdbc.dataSourceClassName}") - private String dataSourceClassName; - - @Value("${jdbc.username}") - private String jdbcUser; - - @Value("${jdbc.password}") - private String jdbcPassword; - - @Value("${jdbc.url}") - private String jdbcUrl; - - @Value("${btm.config.journal:null}") - private String btmJournal; - - @Value("${hibernate.dialect}") - private String hibernateDialect; - - @Bean - public static PropertySourcesPlaceholderConfigurer properties() { - return new PropertySourcesPlaceholderConfigurer(); - } - - @Bean(destroyMethod = "close") - public DataSource actualDataSource() { - PoolingDataSource poolingDataSource = new PoolingDataSource(); - poolingDataSource.setClassName(dataSourceClassName); - poolingDataSource.setUniqueName(getClass().getName()); - poolingDataSource.setMinPoolSize(0); - poolingDataSource.setMaxPoolSize(5); - poolingDataSource.setAllowLocalTransactions(true); - poolingDataSource.setDriverProperties(new Properties()); - poolingDataSource.getDriverProperties().put("user", jdbcUser); - poolingDataSource.getDriverProperties().put("password", jdbcPassword); - poolingDataSource.getDriverProperties().put("url", jdbcUrl); - return poolingDataSource; - } - - @Bean - @DependsOn(value = {"btmConfig", "actualDataSource"}) - public DataSource dataSource() { - SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); - loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); - return ProxyDataSourceBuilder - .create(actualDataSource()) - .name(DATA_SOURCE_PROXY_NAME) - .listener(loggingListener) - .build(); - } - - @Bean - @DependsOn("btmConfig") - public LocalContainerEntityManagerFactoryBean entityManagerFactory() { - LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - localContainerEntityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); - localContainerEntityManagerFactoryBean.setPersistenceProvider(new HibernatePersistenceProvider()); - localContainerEntityManagerFactoryBean.setDataSource(dataSource()); - localContainerEntityManagerFactoryBean.setPackagesToScan(packagesToScan()); - - JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); - localContainerEntityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); - localContainerEntityManagerFactoryBean.setJpaProperties(additionalProperties()); - return localContainerEntityManagerFactoryBean; - } - - @Bean - public bitronix.tm.Configuration btmConfig() { - bitronix.tm.Configuration configuration = TransactionManagerServices.getConfiguration(); - configuration.setServerId("spring-btm"); - configuration.setWarnAboutZeroResourceTransaction(true); - configuration.setJournal(btmJournal); - return configuration; - } - - @Bean(destroyMethod = "shutdown") - @DependsOn(value = "btmConfig") - public BitronixTransactionManager jtaTransactionManager() { - return TransactionManagerServices.getTransactionManager(); - } - - @Bean - public JtaTransactionManager transactionManager() { - BitronixTransactionManager bitronixTransactionManager = jtaTransactionManager(); - JtaTransactionManager transactionManager = new JtaTransactionManager(); - transactionManager.setTransactionManager(bitronixTransactionManager); - transactionManager.setUserTransaction(bitronixTransactionManager); - transactionManager.setAllowCustomIsolationLevels(true); - return transactionManager; - } - - @Bean - public TransactionTemplate transactionTemplate() { - return new TransactionTemplate(transactionManager()); - } - - protected Properties additionalProperties() { - Properties properties = new Properties(); - properties.setProperty("hibernate.transaction.jta.platform", BitronixJtaPlatform.class.getName()); - properties.setProperty("hibernate.dialect", hibernateDialect); - properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); - return properties; - } - - protected String[] packagesToScan() { - return new String[]{ - "com.vladmihalcea.book.hpjp.hibernate.transaction.forum" - }; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/GenericDAO.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/GenericDAO.java deleted file mode 100644 index 210049d3f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/GenericDAO.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta.dao; - -import java.io.Serializable; - -/** - * @author Vlad Mihalcea - */ -public interface GenericDAO { - - T findById(ID id); - - T persist(T entity); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/GenericDAOImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/GenericDAOImpl.java deleted file mode 100644 index 357205e7d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/GenericDAOImpl.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta.dao; - -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.io.Serializable; - -/** - * @author Vlad Mihalcea - */ -@Repository -@Transactional -public abstract class GenericDAOImpl implements GenericDAO { - - @PersistenceContext - private EntityManager entityManager; - - private final Class entityClass; - - protected EntityManager getEntityManager() { - return entityManager; - } - - protected GenericDAOImpl(Class entityClass) { - this.entityClass = entityClass; - } - - @Override - public T findById(ID id) { - return entityManager.find(entityClass, id); - } - - @Override - public T persist(T entity) { - entityManager.persist(entity); - return entity; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/PostDAO.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/PostDAO.java deleted file mode 100644 index a7020d841..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/PostDAO.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; - -/** - * @author Vlad Mihalcea - */ -public interface PostDAO extends GenericDAO { - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/PostDAOImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/PostDAOImpl.java deleted file mode 100644 index 445d8ea7a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/PostDAOImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import org.springframework.stereotype.Repository; - -/** - * @author Vlad Mihalcea - */ -@Repository -public class PostDAOImpl extends GenericDAOImpl implements PostDAO { - - protected PostDAOImpl() { - super(Post.class); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/TagDAO.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/TagDAO.java deleted file mode 100644 index 4e07480ca..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/TagDAO.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Tag; - -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -public interface TagDAO extends GenericDAO { - - List findByName(String... tags); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/TagDAOImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/TagDAOImpl.java deleted file mode 100644 index 958ad586e..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/dao/TagDAOImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta.dao; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Tag; -import org.springframework.stereotype.Repository; - -import java.util.Arrays; -import java.util.List; - -/** - * @author Vlad Mihalcea - */ -@Repository -public class TagDAOImpl extends GenericDAOImpl implements TagDAO { - - protected TagDAOImpl() { - super(Tag.class); - } - - @Override - public List findByName(String... tags) { - if(tags.length == 0) { - throw new IllegalArgumentException("There's no tag name to search for!"); - } - return getEntityManager().createQuery( - "select t " + - "from Tag t " + - "where t.name in :tags", Tag.class) - .setParameter("tags", Arrays.asList(tags)) - .getResultList(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/service/ForumService.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/service/ForumService.java deleted file mode 100644 index 67db61edd..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/service/ForumService.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta.service; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import org.springframework.stereotype.Service; - -/** - * @author Vlad Mihalcea - */ -@Service -public interface ForumService { - - Post newPost(String title, String... tags); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/service/ForumServiceImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/service/ForumServiceImpl.java deleted file mode 100644 index 6dd5bfd9f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/spring/jta/service/ForumServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta.service; - -import com.vladmihalcea.book.hpjp.hibernate.transaction.forum.Post; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta.dao.PostDAO; -import com.vladmihalcea.book.hpjp.hibernate.transaction.spring.jta.dao.TagDAO; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -/** - * @author Vlad Mihalcea - */ -@Repository -public class ForumServiceImpl implements ForumService { - - @Autowired - private PostDAO postDAO; - - @Autowired - private TagDAO tagDAO; - - @Override - @Transactional - public Post newPost(String title, String... tags) { - Post post = new Post(); - post.setTitle(title); - post.getTags().addAll(tagDAO.findByName(tags)); - return postDAO.persist(post); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/CharacterType.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/CharacterType.java deleted file mode 100644 index 8e33a142d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/CharacterType.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type; - -import org.hibernate.engine.spi.SharedSessionContractImplementor; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Types; - -/** - * @author Vlad Mihalcea - */ -public class CharacterType extends ImmutableType { - - public CharacterType() { - super(Character.class); - } - - @Override - public int[] sqlTypes() { return new int[]{Types.CHAR}; } - - @Override - public Character get(ResultSet rs, String[] names, - SharedSessionContractImplementor session, Object owner) throws SQLException { - String value = rs.getString(names[0]); - return (value != null && value.length() > 0) ? value.charAt(0) : null; - } - - @Override - public void set(PreparedStatement st, Character value, int index, - SharedSessionContractImplementor session) throws SQLException { - if (value == null) { - st.setNull(index, Types.CHAR); - } else { - st.setString(index, String.valueOf(value)); - } - } -} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/DateTimeTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/DateTimeTest.java deleted file mode 100644 index 4a5af105b..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/DateTimeTest.java +++ /dev/null @@ -1,182 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.junit.Test; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.sql.Timestamp; -import java.time.*; -import java.util.Date; -import java.util.List; -import java.util.Properties; - -import org.hibernate.cfg.AvailableSettings; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class DateTimeTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - LocalDateEvent.class, - ZonedDateTimeEvent.class, - OffsetDateTimeEvent.class, - TimestampEvent.class - }; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.setProperty( AvailableSettings.JDBC_TIME_ZONE, "UTC" ); - return properties; - } - - @Test - public void testLocalDateEvent() { - doInJPA(entityManager -> { - LocalDateEvent event = new LocalDateEvent(); - event.id = 1L; - event.startDate = LocalDate.of(1, 1, 1); - entityManager.persist(event); - }); - - doInJPA(entityManager -> { - LocalDateEvent event = entityManager.find(LocalDateEvent.class, 1L); - try { - assertEquals(LocalDate.of(1, 1, 1), event.startDate); - } catch (Throwable e) { - e.printStackTrace(); - } - }); - } - - @Test - public void testOffsetDateTimeEvent() { - doInJPA(entityManager -> { - OffsetDateTimeEvent event = new OffsetDateTimeEvent(); - event.id = 1L; - event.startDate = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); - entityManager.persist(event); - }); - - doInJPA(entityManager -> { - OffsetDateTimeEvent event = entityManager.find(OffsetDateTimeEvent.class, 1L); - try { - assertEquals(OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC), event.startDate); - } catch (Throwable e) { - e.printStackTrace(); - } - }); - } - - @Test - public void testZonedDateTimeEvent() { - doInJPA(entityManager -> { - ZonedDateTimeEvent event = new ZonedDateTimeEvent(); - event.id = 1L; - event.startDate = ZonedDateTime.of( - 2017, 6, 24, 15, 45, 23, 0, ZoneOffset.systemDefault()); - entityManager.persist(event); - }); - - doInJPA(entityManager -> { - ZonedDateTimeEvent event = entityManager.find(ZonedDateTimeEvent.class, 1L); - try { - assertEquals(ZonedDateTime.of( - 2017, 6, 24, 15, 45, 23, 0, ZoneOffset.systemDefault()), event.startDate); - } catch (Throwable e) { - e.printStackTrace(); - } - }); - } - - @Test - public void testTruncEvent() { - doInJPA(entityManager -> { - TimestampEvent event = new TimestampEvent(); - event.id = 1L; - event.createdOn = new Date(); - entityManager.persist(event); - }); - - doInJPA(entityManager -> { - List events = entityManager - .createQuery( - "select e " + - "from TimestampEvent e " + - "where cast(e.createdOn as date) >= :createdOn " + - "order by e.createdOn asc") - .setParameter("createdOn", Timestamp.from(LocalDateTime.of(LocalDate.now(), LocalTime.MIDNIGHT).toInstant(ZoneOffset.UTC)), TemporalType.DATE) - //.setParameter("createdOn", new Date(0), TemporalType.DATE) - .getResultList(); - assertEquals(1, events.size()); - }); - doInJPA(entityManager -> { - - LocalDateTime dt = LocalDateTime.now(); - ZonedDateTime zdt = dt.atZone(ZoneOffset.systemDefault()); - ZoneOffset offset = zdt.getOffset(); - - List events = entityManager - .createQuery( - "select e " + - "from TimestampEvent e " + - "where function('trunc', e.createdOn) >= :createdOn " + - "order by e.createdOn asc") - .setParameter("createdOn", Timestamp.from(LocalDateTime.of(LocalDate.now(), LocalTime.MIDNIGHT).minusSeconds(offset.getTotalSeconds()).toInstant(ZoneOffset.UTC)), TemporalType.DATE) - .getResultList(); - assertEquals(1, events.size()); - }); - } - - @Entity(name = "LocalDateEvent") - public static class LocalDateEvent { - - @Id - private Long id; - - @NotNull - @Column(name = "START_DATE", nullable = false) - private LocalDate startDate; - } - - @Entity(name = "OffsetDateTimeEvent") - public static class OffsetDateTimeEvent { - - @Id - private Long id; - - @NotNull - @Column(name = "START_DATE", nullable = false) - private OffsetDateTime startDate; - } - - @Entity(name = "ZonedDateTimeEvent") - public static class ZonedDateTimeEvent { - - @Id - private Long id; - - @NotNull - @Column(name = "START_DATE", nullable = false) - private ZonedDateTime startDate; - } - - @Entity(name = "TimestampEvent") - public static class TimestampEvent { - - @Id - private Long id; - - @Column(name = "START_DATE") - @Temporal(TemporalType.TIMESTAMP) - private Date createdOn; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/IPv4.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/IPv4.java deleted file mode 100644 index 857aadc37..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/IPv4.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type; - -import java.io.Serializable; -import java.net.Inet4Address; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Objects; - -/** - * @author Vlad Mihalcea - */ -public class IPv4 implements Serializable { - - private final String address; - - public IPv4(String address) { - this.address = address; - } - - public String getAddress() { - return address; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - return Objects.equals(address, IPv4.class.cast(o).address); - } - - @Override - public int hashCode() { - return Objects.hash(address); - } - - public InetAddress toInetAddress() throws UnknownHostException { - return Inet4Address.getByName(address); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/IPv4Type.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/IPv4Type.java deleted file mode 100644 index c68aa24bb..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/IPv4Type.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type; - -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.postgresql.util.PGobject; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Types; - -/** - * @author Vlad Mihalcea - */ -public class IPv4Type extends ImmutableType { - - public IPv4Type() { - super(IPv4.class); - } - - @Override - public int[] sqlTypes() { return new int[]{Types.OTHER}; } - - @Override - public IPv4 get(ResultSet rs, String[] names, - SharedSessionContractImplementor session, Object owner) throws SQLException { - String ip = rs.getString(names[0]); - return (ip != null) ? new IPv4(ip) : null; - } - - @Override - public void set(PreparedStatement st, IPv4 value, int index, - SharedSessionContractImplementor session) throws SQLException { - if (value == null) { - st.setNull(index, Types.OTHER); - } else { - PGobject holder = new PGobject(); - holder.setType("inet"); - holder.setValue(value.getAddress()); - st.setObject(index, holder); - } - } -} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/IPv4TypeTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/IPv4TypeTest.java deleted file mode 100644 index f566f5b78..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/IPv4TypeTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.Session; -import org.hibernate.annotations.Type; -import org.hibernate.annotations.TypeDef; - -import org.junit.Test; - -import javax.persistence.*; -import java.net.UnknownHostException; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.concurrent.atomic.AtomicReference; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * @author Vlad Mihalcea - */ -public class IPv4TypeTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Event.class - }; - } - - @Override - public void init() { - super.init(); - doInJDBC(connection -> { - try ( - Statement statement = connection.createStatement(); - ) { - statement.executeUpdate("CREATE INDEX ON event USING gist (ip inet_ops)"); - } catch (SQLException e) { - fail(e.getMessage()); - } - }); - } - - @Test - public void test() { - final AtomicReference eventHolder = new AtomicReference<>(); - doInJPA(entityManager -> { - entityManager.persist(new Event()); - Event event = new Event("192.168.0.231"); - entityManager.persist(event); - eventHolder.set(event); - }); - doInJPA(entityManager -> { - Event event = entityManager.find(Event.class, eventHolder.get().getId()); - event.setIp("192.168.0.123"); - }); - doInJPA(entityManager -> { - Event event = entityManager.createQuery("select e from Event e where ip is not null", Event.class).getSingleResult(); - assertEquals("192.168.0.123", event.getIp().getAddress()); - - try { - assertEquals("192.168.0.123", event.getIp().toInetAddress().getHostAddress()); - } catch (UnknownHostException e) { - fail(e.getMessage()); - } - - Session session = entityManager.unwrap(Session.class); - session.doWork(connection -> { - try(PreparedStatement ps = connection.prepareStatement( - "select * " + - "from Event e " + - "where " + - " e.ip && ?::inet = TRUE" - )) { - ps.setObject(1, "192.168.0.1/24"); - ResultSet rs = ps.executeQuery(); - while(rs.next()) { - Long id = rs.getLong(1); - String ip = rs.getString(2); - assertEquals("192.168.0.123", ip); - } - } - }); - - Event matchingEvent = (Event) entityManager.createNativeQuery( - "SELECT {e.*} " + - "FROM event e " + - "WHERE " + - " e.ip && CAST(:network AS inet) = TRUE", Event.class) - .setParameter("network", "192.168.0.1/24") - .getSingleResult(); - assertEquals("192.168.0.123", matchingEvent.getIp().getAddress()); - }); - } - - @Entity(name = "Event") - @Table(name = "event") - @TypeDef( name = "ipv4", typeClass = IPv4Type.class, defaultForType = IPv4.class ) - public static class Event { - - @Id - @GeneratedValue - private Long id; - - //@Type(type = "com.vladmihalcea.book.hpjp.hibernate.type.IPv4Type") - @Column(name = "ip", columnDefinition = "inet") - private IPv4 ip; - - public Event() {} - - public Event(String address) { - this.ip = new IPv4(address); - } - - public Long getId() { - return id; - } - - public IPv4 getIp() { - return ip; - } - - public void setIp(String address) { - this.ip = new IPv4(address); - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/ImmutableType.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/ImmutableType.java deleted file mode 100644 index a026148d8..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/ImmutableType.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type; - -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.usertype.UserType; - -import java.io.Serializable; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Objects; - -/** - * @author Vlad Mihalcea - */ -public abstract class ImmutableType implements UserType { - - private final Class clazz; - - protected ImmutableType(Class clazz) { - this.clazz = clazz; - } - - @Override - public Object nullSafeGet(ResultSet rs, String[] names, - SharedSessionContractImplementor session, Object owner) throws SQLException { - return get(rs, names, session, owner); - } - - @Override - public void nullSafeSet(PreparedStatement st, Object value, int index, - SharedSessionContractImplementor session) throws SQLException { - set(st, clazz.cast(value), index, session); - } - - protected abstract T get(ResultSet rs, String[] names, - SharedSessionContractImplementor session, Object owner) throws SQLException; - - protected abstract void set(PreparedStatement st, T value, int index, - SharedSessionContractImplementor session) throws SQLException; - - - @Override - public Class returnedClass() { - return clazz; - } - - @Override - public boolean equals(Object x, Object y) { - return Objects.equals(x, y); - } - - @Override - public int hashCode(Object x) { - return x.hashCode(); - } - - @Override - public Object deepCopy(Object value) { - return value; - } - - @Override - public boolean isMutable() { - return false; - } - - @Override - public Serializable disassemble(Object o) { - return (Serializable) o; - } - - @Override - public Object assemble(Serializable cached, Object owner) { - return cached; - } - - @Override - public Object replace(Object o, Object target, Object owner) { - return o; - } -} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/LocalDateTimeTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/LocalDateTimeTest.java deleted file mode 100644 index e4bf1ec95..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/LocalDateTimeTest.java +++ /dev/null @@ -1,209 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type; - -import java.time.Duration; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; - -import org.hibernate.Session; -import org.hibernate.annotations.NaturalId; - -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; - -/** - * @author Vlad Mihalcea - */ -public class LocalDateTimeTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Employee.class, - Meeting.class - }; - } - - @Test - public void testLocalDateEvent() { - doInJPA( entityManager -> { - Employee employee = new Employee(); - employee.setName( "Vlad Mihalcea" ); - employee.setBirthday( - LocalDate.of( - 1981, 12, 10 - ) - ); - employee.setUpdatedOn( - LocalDateTime.of( - 2015, 12, 1, - 8, 0, 0 - ) - ); - - entityManager.persist( employee ); - - Meeting meeting = new Meeting(); - meeting.setId( 1L ); - meeting.setCreatedBy( employee ); - meeting.setStartsAt( - ZonedDateTime.of( - 2017, 6, 25, - 11, 30, 0, 0, - ZoneId.systemDefault() - ) - ); - meeting.setDuration( - Duration.of( 45, ChronoUnit.MINUTES ) - ); - - entityManager.persist( meeting ); - } ); - - doInJPA( entityManager -> { - Employee employee = entityManager - .unwrap( Session.class ) - .bySimpleNaturalId( Employee.class ) - .load( "Vlad Mihalcea" ); - assertEquals( - LocalDate.of( - 1981, 12, 10 - ), - employee.getBirthday() - ); - assertEquals( - LocalDateTime.of( - 2015, 12, 1, - 8, 0, 0 - ), - employee.getUpdatedOn() - ); - - Meeting meeting = entityManager.find( Meeting.class, 1L ); - assertSame( - employee, meeting.getCreatedBy() - ); - assertEquals( - ZonedDateTime.of( - 2017, 6, 25, - 11, 30, 0, 0, - ZoneId.systemDefault() - ), - meeting.getStartsAt() - ); - assertEquals( - Duration.of( 45, ChronoUnit.MINUTES ), - meeting.getDuration() - ); - } ); - } - - @Entity(name = "Employee") - public static class Employee { - - @Id - @GeneratedValue - private Long id; - - @NaturalId - private String name; - - private LocalDate birthday; - - @Column(name = "updated_on") - private LocalDateTime updatedOn; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public LocalDate getBirthday() { - return birthday; - } - - public void setBirthday(LocalDate birthday) { - this.birthday = birthday; - } - - public LocalDateTime getUpdatedOn() { - return updatedOn; - } - - public void setUpdatedOn(LocalDateTime updatedOn) { - this.updatedOn = updatedOn; - } - } - - @Entity(name = "Meeting") - public static class Meeting { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "employee_id") - private Employee createdBy; - - @Column(name = "starts_at") - private ZonedDateTime startsAt; - - private Duration duration; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Employee getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(Employee createdBy) { - this.createdBy = createdBy; - } - - public ZonedDateTime getStartsAt() { - return startsAt; - } - - public void setStartsAt(ZonedDateTime startsAt) { - this.startsAt = startsAt; - } - - public Duration getDuration() { - return duration; - } - - public void setDuration(Duration duration) { - this.duration = duration; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/PostgresUUIDTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/PostgresUUIDTest.java deleted file mode 100644 index ef90a7eb2..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/PostgresUUIDTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.junit.Test; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Table; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicReference; - -/** - * @author Vlad Mihalcea - */ -public class PostgresUUIDTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Event.class - }; - } - @Test - public void test() { - final AtomicReference eventHolder = new AtomicReference<>(); - doInJPA(entityManager -> { - entityManager.persist(new Event()); - Event event = new Event(); - entityManager.persist(event); - eventHolder.set(event); - }); - } - - @Entity(name = "Event") - @Table(name = "event") - public static class Event { - - @Id - @GeneratedValue - private Long id; - - private UUID uuid = UUID.randomUUID(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/TaskTypingTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/TaskTypingTest.java deleted file mode 100644 index 492562845..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/TaskTypingTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.TaskEntityProvider; -import org.junit.Test; - -/** - * EntityGraphMapperTest - Test mapping to entity - * - * @author Vlad Mihalcea - */ -public class TaskTypingTest extends AbstractMySQLIntegrationTest { - - private TaskEntityProvider entityProvider = new TaskEntityProvider(); - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Test - public void testJdbcOneToManyMapping() { - doInJDBC(connection -> { - - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/array/ArrayTypeTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/array/ArrayTypeTest.java deleted file mode 100644 index 2f76ef2c2..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/array/ArrayTypeTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.array; - -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Table; - -import org.hibernate.annotations.Type; - -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.BaseEntity; -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.Location; -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.Ticket; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class ArrayTypeTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Event.class, - }; - } - - @Override - protected DataSourceProvider dataSourceProvider() { - return new PostgreSQLDataSourceProvider() { - @Override - public String hibernateDialect() { - return PostgreSQL95ArrayDialect.class.getName(); - } - }; - } - - @Test - public void test() { - doInJPA(entityManager -> { - Event nullEvent = new Event(); - nullEvent.setId(0L); - entityManager.persist(nullEvent); - - Event event = new Event(); - event.setId(1L); - event.setSensorNames(new String[] {"Temperature", "Pressure"}); - event.setSensorValues( new int[] {12, 756} ); - entityManager.persist(event); - }); - doInJPA(entityManager -> { - Event event = entityManager.find(Event.class, 1L); - - assertArrayEquals( new String[] {"Temperature", "Pressure"}, event.getSensorNames() ); - assertArrayEquals( new int[] {12, 756}, event.getSensorValues() ); - }); - } - - @Entity(name = "Event") - @Table(name = "event") - public static class Event extends BaseEntity { - - @Type( type = "string-array" ) - @Column(name = "sensor_names", columnDefinition = "text[]") - private String[] sensorNames; - - @Type( type = "int-array" ) - @Column(name = "sensor_values", columnDefinition = "integer[]") - private int[] sensorValues; - - public String[] getSensorNames() { - return sensorNames; - } - - public void setSensorNames(String[] sensorNames) { - this.sensorNames = sensorNames; - } - - public int[] getSensorValues() { - return sensorValues; - } - - public void setSensorValues(int[] sensorValues) { - this.sensorValues = sensorValues; - } - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/array/PostgreSQL95ArrayDialect.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/array/PostgreSQL95ArrayDialect.java deleted file mode 100644 index 95ec4dbc5..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/array/PostgreSQL95ArrayDialect.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.array; - -import java.sql.Types; - -import org.hibernate.dialect.PostgreSQL95Dialect; - -/** - * @author Vlad Mihalcea - */ -public class PostgreSQL95ArrayDialect extends PostgreSQL95Dialect { - - public PostgreSQL95ArrayDialect() { - super(); - this.registerColumnType( Types.ARRAY, "array" ); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeTest.java deleted file mode 100644 index 2db3c5d80..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeTest.java +++ /dev/null @@ -1,149 +0,0 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json; - -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.BaseEntity; -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.Location; -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.Ticket; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.annotations.Type; -import org.junit.Test; - -import javax.persistence.*; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class PostgreSQLJsonBinaryTypeTest extends AbstractPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Event.class, - Participant.class - }; - } - - @Test - public void test() { - final AtomicReference eventHolder = new AtomicReference<>(); - final AtomicReference participantHolder = new AtomicReference<>(); - - doInJPA(entityManager -> { - Event nullEvent = new Event(); - nullEvent.setId(0L); - entityManager.persist(nullEvent); - - Location location = new Location(); - location.setCountry("Romania"); - location.setCity("Cluj-Napoca"); - - Event event = new Event(); - event.setId(1L); - event.setLocation(location); - entityManager.persist(event); - - Ticket ticket = new Ticket(); - ticket.setPrice(12.34d); - ticket.setRegistrationCode("ABC123"); - - Participant participant = new Participant(); - participant.setId(1L); - participant.setTicket(ticket); - participant.setEvent(event); - - entityManager.persist(participant); - - eventHolder.set(event); - participantHolder.set(participant); - }); - doInJPA(entityManager -> { - Event event = entityManager.find(Event.class, eventHolder.get().getId()); - assertEquals("Cluj-Napoca", event.getLocation().getCity()); - - Participant participant = entityManager.find(Participant.class, participantHolder.get().getId()); - assertEquals("ABC123", participant.getTicket().getRegistrationCode()); - - List participants = entityManager.createNativeQuery( - "select jsonb_pretty(p.ticket) " + - "from participant p " + - "where p.ticket ->> 'price' > '10'") - .getResultList(); - - participants = entityManager.createNativeQuery( - "select jsonb_pretty(p.ticket) " + - "from participant p " + - "where p.ticket ->> 'price' > :price") - .setParameter("price", "10") - .getResultList(); - - event.getLocation().setCity("Constanța"); - assertEquals(Integer.valueOf(0), event.getVersion()); - entityManager.flush(); - assertEquals(Integer.valueOf(1), event.getVersion()); - - assertEquals(1, participants.size()); - }); - doInJPA(entityManager -> { - Event event = entityManager.find(Event.class, eventHolder.get().getId()); - event.getLocation().setCity(null); - assertEquals(Integer.valueOf(1), event.getVersion()); - entityManager.flush(); - assertEquals(Integer.valueOf(2), event.getVersion()); - }); - doInJPA(entityManager -> { - Event event = entityManager.find(Event.class, eventHolder.get().getId()); - event.setLocation(null); - assertEquals(Integer.valueOf(2), event.getVersion()); - entityManager.flush(); - assertEquals(Integer.valueOf(3), event.getVersion()); - }); - } - - @Entity(name = "Event") - @Table(name = "event") - public static class Event extends BaseEntity { - - @Type(type = "jsonb") - @Column(columnDefinition = "jsonb") - private Location location; - - public Location getLocation() { - return location; - } - - public void setLocation(Location location) { - this.location = location; - } - } - - @Entity(name = "Participant") - @Table(name = "participant") - public static class Participant extends BaseEntity { - - @Type(type = "jsonb") - @Column(columnDefinition = "jsonb") - private Ticket ticket; - - @ManyToOne - private Event event; - - public Ticket getTicket() { - return ticket; - } - - public void setTicket(Ticket ticket) { - this.ticket = ticket; - } - - public Event getEvent() { - return event; - } - - public void setEvent(Event event) { - this.event = event; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/BatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/BatchPreparedStatementTest.java deleted file mode 100644 index 46df395e7..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/BatchPreparedStatementTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch; - -import java.sql.PreparedStatement; -import java.sql.SQLException; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; - -/** - * BatchPreparedStatementTest - Test batching with PreparedStatements - * - * @author Vlad Mihalcea - */ -public class BatchPreparedStatementTest extends AbstractBatchPreparedStatementTest { - - public BatchPreparedStatementTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); - } - - @Override - protected void onStatement(PreparedStatement statement) throws SQLException { - statement.addBatch(); - } - - @Override - protected void onEnd(PreparedStatement statement) throws SQLException { - int[] updateCount = statement.executeBatch(); - statement.clearBatch(); - } - - @Override - protected void onFlush(PreparedStatement statement) throws SQLException { - statement.executeBatch(); - } - - @Override - protected int getBatchSize() { - return 100 * 10 ; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/BatchStatementTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/BatchStatementTest.java deleted file mode 100644 index 52b33041f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/BatchStatementTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch; - -import org.junit.runners.Parameterized; - -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; - -/** - * BatchStatementTest - Test batching with Statements - * - * @author Vlad Mihalcea - */ -public class BatchStatementTest extends AbstractBatchStatementTest { - - public BatchStatementTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); - } - - @Parameterized.Parameters - public static Collection rdbmsDataSourceProvider() { - List providers = new ArrayList<>(); - providers.add(new DataSourceProvider[]{new PostgreSQLDataSourceProvider()}); - providers.add(new DataSourceProvider[]{new OracleDataSourceProvider()}); - providers.add(new DataSourceProvider[]{new MySQLDataSourceProvider()}); - providers.add(new DataSourceProvider[]{new SQLServerDataSourceProvider()}); - return providers; - } - - @Override - protected void onStatement(Statement statement, String dml) throws SQLException { - statement.addBatch(dml); - } - - @Override - protected void onEnd(Statement statement) throws SQLException { - int[] updateCount = statement.executeBatch(); - statement.clearBatch(); - } - - @Override - protected void onFlush(Statement statement) throws SQLException { - statement.executeBatch(); - } - - @Override - protected int getBatchSize() { - return 100 ; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/MySqlBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/MySqlBatchPreparedStatementTest.java deleted file mode 100644 index e7193a731..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/MySqlBatchPreparedStatementTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.fail; - -/** - * MySqlBatchStatementTest - Test MySQl JDBC Statement batching with and w/o rewriteBatchedStatements - * - * @author Vlad Mihalcea - */ -@RunWith(Parameterized.class) -public class MySqlBatchPreparedStatementTest extends AbstractMySQLIntegrationTest { - - private final BlogEntityProvider entityProvider = new BlogEntityProvider(); - - private boolean cachePrepStmts; - - private boolean useServerPrepStmts; - - public MySqlBatchPreparedStatementTest(boolean cachePrepStmts, boolean useServerPrepStmts) { - this.cachePrepStmts = cachePrepStmts; - this.useServerPrepStmts = useServerPrepStmts; - } - - @Parameterized.Parameters - public static Collection rdbmsDataSourceProvider() { - List providers = new ArrayList<>(); - providers.add(new Boolean[]{Boolean.FALSE, Boolean.FALSE}); - /*providers.add(new Boolean[]{Boolean.FALSE, Boolean.TRUE}); - providers.add(new Boolean[]{Boolean.TRUE, Boolean.FALSE}); - providers.add(new Boolean[]{Boolean.TRUE, Boolean.TRUE});*/ - return providers; - } - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Override - protected DataSourceProvider dataSourceProvider() { - MySQLDataSourceProvider dataSourceProvider = (MySQLDataSourceProvider) super.dataSourceProvider(); - dataSourceProvider.setCachePrepStmts(cachePrepStmts); - dataSourceProvider.setUseServerPrepStmts(useServerPrepStmts); - return dataSourceProvider; - } - - @Test - public void testInsert() { - LOGGER.info("Test MySQL batch insert with cachePrepStmts={}, useServerPrepStmts={}", cachePrepStmts, useServerPrepStmts); - AtomicInteger statementCount = new AtomicInteger(); - long startNanos = System.nanoTime(); - doInJDBC(connection -> { - AtomicInteger postStatementCount = new AtomicInteger(); - AtomicInteger postCommentStatementCount = new AtomicInteger(); - - try (PreparedStatement postStatement = connection.prepareStatement("insert into post (title, version, id) values (?, ?, ?)"); - PreparedStatement postCommentStatement = connection.prepareStatement("insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"); - ) { - int postCount = getPostCount(); - int postCommentCount = getPostCommentCount(); - - for (int i = 0; i < postCount; i++) { - int index = 0; - - postStatement.setString(++index, String.format("Post no. %1$d", i)); - postStatement.setInt(++index, 0); - postStatement.setLong(++index, i); - executeStatement(postStatement, postStatementCount); - } - postStatement.executeBatch(); - - for (int i = 0; i < postCount; i++) { - for (int j = 0; j < postCommentCount; j++) { - int index = 0; - - postCommentStatement.setLong(++index, i); - postCommentStatement.setString(++index, String.format("Post comment %1$d", j)); - postCommentStatement.setInt(++index, 0); - postCommentStatement.setLong(++index, (postCommentCount * i) + j); - executeStatement(postCommentStatement, postCommentStatementCount); - } - } - postCommentStatement.executeBatch(); - } catch (SQLException e) { - fail(e.getMessage()); - } - }); - LOGGER.info("{}.testInsert for cachePrepStmts={}, useServerPrepStmts={} took {} millis", - getClass().getSimpleName(), - cachePrepStmts, - useServerPrepStmts, - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); - } - - private void executeStatement(PreparedStatement statement, AtomicInteger statementCount) throws SQLException { - statement.addBatch(); - int count = statementCount.incrementAndGet(); - if(count % getBatchSize() == 0) { - statement.executeBatch(); - } - } - - protected int getPostCount() { - return 5000; - } - - protected int getPostCommentCount() { - return 1; - } - - protected int getBatchSize() { - return 100 * 10; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/MySqlBatchStatementTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/MySqlBatchStatementTest.java deleted file mode 100644 index 94102558f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/MySqlBatchStatementTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch; - -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.fail; - -/** - * MySqlBatchStatementTest - Test MySQl JDBC Statement batching with and w/o rewriteBatchedStatements - * - * @author Vlad Mihalcea - */ -@RunWith(Parameterized.class) -public class MySqlBatchStatementTest extends AbstractMySQLIntegrationTest { - - public static final String INSERT_POST = "insert into post (title, version, id) values ('Post no. %1$d', 0, %1$d)"; - - public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (%1$d, 'Post comment %2$d', 0, %2$d)"; - - private final BlogEntityProvider entityProvider = new BlogEntityProvider(); - - private boolean rewriteBatchedStatements; - - public MySqlBatchStatementTest(boolean rewriteBatchedStatements) { - this.rewriteBatchedStatements = rewriteBatchedStatements; - } - - @Parameterized.Parameters - public static Collection rdbmsDataSourceProvider() { - List providers = new ArrayList<>(); - providers.add(new Boolean[]{Boolean.FALSE}); - providers.add(new Boolean[]{Boolean.TRUE}); - return providers; - } - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Override - protected DataSourceProvider dataSourceProvider() { - MySQLDataSourceProvider dataSourceProvider = (MySQLDataSourceProvider) super.dataSourceProvider(); - dataSourceProvider.setRewriteBatchedStatements(rewriteBatchedStatements); - return dataSourceProvider; - } - - @Test - public void testInsert() { - LOGGER.info("Test MySQL batch insert with rewriteBatchedStatements={}", rewriteBatchedStatements); - AtomicInteger statementCount = new AtomicInteger(); - long startNanos = System.nanoTime(); - doInJDBC(connection -> { - try (Statement statement = connection.createStatement()) { - int postCount = getPostCount(); - int postCommentCount = getPostCommentCount(); - - for (int i = 0; i < postCount; i++) { - executeStatement(statement, String.format(INSERT_POST, i), statementCount); - } - statement.executeBatch(); - - for (int i = 0; i < postCount; i++) { - for (int j = 0; j < postCommentCount; j++) { - executeStatement(statement, String.format(INSERT_POST_COMMENT, i, (postCommentCount * i) + j), statementCount); - } - } - statement.executeBatch(); - } catch (SQLException e) { - fail(e.getMessage()); - } - }); - LOGGER.info("{}.testInsert for rewriteBatchedStatements={} took {} millis", - getClass().getSimpleName(), - rewriteBatchedStatements, - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); - } - - private void executeStatement(Statement statement, String dml, AtomicInteger statementCount) throws SQLException { - statement.addBatch(dml); - int count = statementCount.incrementAndGet(); - if(count % getBatchSize() == 0) { - statement.executeBatch(); - } - } - - protected int getPostCount() { - return 1000; - } - - protected int getPostCommentCount() { - return 4; - } - - protected int getBatchSize() { - return 100 * 10; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/NoBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/NoBatchPreparedStatementTest.java deleted file mode 100644 index 03fe48f89..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/NoBatchPreparedStatementTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch; - -import java.sql.PreparedStatement; -import java.sql.SQLException; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; - -/** - * NoBatchPreparedStatementTest - Test without batching PreparedStatements - * - * @author Vlad Mihalcea - */ -public class NoBatchPreparedStatementTest extends AbstractBatchPreparedStatementTest { - - public NoBatchPreparedStatementTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); - } - - @Override - protected void onStatement(PreparedStatement statement) throws SQLException { - statement.executeUpdate(); - } - - @Override - protected void onEnd(PreparedStatement statement) throws SQLException { - } - - @Override - protected void onFlush(PreparedStatement statement) throws SQLException { - - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/NoBatchStatementTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/NoBatchStatementTest.java deleted file mode 100644 index d06379bd6..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/NoBatchStatementTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch; - -import java.sql.SQLException; -import java.sql.Statement; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; - -/** - * BatchStatementTest - Test without batching Statements - * - * @author Vlad Mihalcea - */ -public class NoBatchStatementTest extends AbstractBatchStatementTest { - - private int count; - - public NoBatchStatementTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); - } - - @Override - protected void onStatement(Statement statement, String dml) throws SQLException { - statement.executeUpdate(dml); - count++; - } - - @Override - protected void onEnd(Statement statement) throws SQLException { - //assertEquals((getPostCommentCount() + 1) * getPostCount(), count); - } - - @Override - protected void onFlush(Statement statement) { - - } -} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/SimpleBatchTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/SimpleBatchTest.java deleted file mode 100644 index 0b1bfaa5a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/SimpleBatchTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import org.junit.Test; - -import java.sql.PreparedStatement; -import java.sql.Statement; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class SimpleBatchTest extends AbstractPostgreSQLIntegrationTest { - - private BlogEntityProvider blogEntityProvider = new BlogEntityProvider(); - - @Override - protected Class[] entities() { - return blogEntityProvider.entities(); - } - - @Test - public void testStatement() { - LOGGER.info("Test Statement batch insert"); - doInJDBC(connection -> { - try (Statement statement = connection.createStatement()) { - - statement.addBatch( - "insert into post (title, version, id) " + - "values ('Post no. 1', 0, 1)"); - - statement.addBatch( - "insert into post_comment (post_id, review, version, id) " + - "values (1, 'Post comment 1.1', 0, 1)"); - statement.addBatch( - "insert into post_comment (post_id, review, version, id) " + - "values (1, 'Post comment 1.2', 0, 2)"); - - int[] updateCounts = statement.executeBatch(); - - assertEquals(3, updateCounts.length); - } - }); - } - - @Test - public void testPreparedStatement() { - LOGGER.info("Test Statement batch insert"); - doInJDBC(connection -> { - PreparedStatement postStatement = connection.prepareStatement( - "insert into post (title, version, id) " + - "values (?, ?, ?)"); - - postStatement.setString(1, String.format("Post no. %1$d", 1)); - postStatement.setInt(2, 0); - postStatement.setLong(3, 1); - postStatement.addBatch(); - - postStatement.setString(1, String.format("Post no. %1$d", 2)); - postStatement.setInt(2, 0); - postStatement.setLong(3, 2); - postStatement.addBatch(); - - int[] updateCounts = postStatement.executeBatch(); - - assertEquals(2, updateCounts.length); - - postStatement.close(); - }); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/AbstractSequenceGeneratedKeysBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/AbstractSequenceGeneratedKeysBatchPreparedStatementTest.java deleted file mode 100644 index ff327db14..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/AbstractSequenceGeneratedKeysBatchPreparedStatementTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch.generatedkeys.sequence; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.SequenceBatchEntityProvider; -import org.junit.Test; - -import java.sql.*; -import java.util.concurrent.TimeUnit; - -/** - * AbstractSequenceGeneratedKeysBatchPreparedStatementTest - Base class for testing JDBC PreparedStatement generated keys for Sequences - * - * @author Vlad Mihalcea - */ -public abstract class AbstractSequenceGeneratedKeysBatchPreparedStatementTest extends AbstractTest { - - private SequenceBatchEntityProvider entityProvider = new SequenceBatchEntityProvider(); - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Test - public void testBatch() { - doInJDBC(this::batchInsert); - } - - protected int getPostCount() { - return 5 * 1000; - } - - protected int getBatchSize() { - return 25; - } - - protected int getAllocationSize() { - return 1; - } - - protected void batchInsert(Connection connection) throws SQLException { - DatabaseMetaData databaseMetaData = connection.getMetaData(); - LOGGER.info("{} Driver supportsGetGeneratedKeys: {}", dataSourceProvider().database(), databaseMetaData.supportsGetGeneratedKeys()); - - dropSequence(connection); - createSequence(connection); - - long startNanos = System.nanoTime(); - int postCount = getPostCount(); - int batchSize = getBatchSize(); - try(PreparedStatement postStatement = connection.prepareStatement( - "INSERT INTO post (id, title, version) VALUES (?, ?, ?)")) { - for (int i = 0; i < postCount; i++) { - if(i > 0 && i % batchSize == 0) { - postStatement.executeBatch(); - } - postStatement.setLong(1, getNextSequenceValue(connection)); - postStatement.setString(2, String.format("Post no. %1$d", i)); - postStatement.setInt(3, 0); - postStatement.addBatch(); - } - postStatement.executeBatch(); - } - - LOGGER.info("{}.testInsert for {} using allocation size {} took {} millis", - getClass().getSimpleName(), - dataSourceProvider().getClass().getSimpleName(), - getAllocationSize(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); - } - - private long getNextSequenceValue(Connection connection) - throws SQLException { - try(Statement statement = connection.createStatement()) { - try(ResultSet resultSet = statement.executeQuery( - callSequenceSyntax())) { - resultSet.next(); - return resultSet.getLong(1); - } - } - } - - protected abstract String callSequenceSyntax(); - - protected void dropSequence(Connection connection) { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate("drop sequence post_seq"); - } catch (Exception ignore) {} - } - - protected void createSequence(Connection connection) { - try(Statement statement = connection.createStatement()) { - statement.executeUpdate(String.format("create sequence post_seq start with 1 increment by %d", getAllocationSize())); - } catch (Exception ignore) {} - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/OracleSequenceCallTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/OracleSequenceCallTest.java deleted file mode 100644 index ace35bd78..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/OracleSequenceCallTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch.generatedkeys.sequence; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; - -/** - * OracleSequenceCallTest - Oracle sequence call - * - * @author Vlad Mihalcea - */ -public class OracleSequenceCallTest extends AbstractSequenceCallTest { - - @Override - protected String callSequenceSyntax() { - return "select post_seq.NEXTVAL from dual"; - } - - @Override - protected DataSourceProvider dataSourceProvider() { - return new OracleDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/OracleSequenceGeneratedKeysBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/OracleSequenceGeneratedKeysBatchPreparedStatementTest.java deleted file mode 100644 index 1bfc526da..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/OracleSequenceGeneratedKeysBatchPreparedStatementTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch.generatedkeys.sequence; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; - -/** - * OracleSequenceGeneratedKeysBatchPreparedStatementTest - Oracle class for testing JDBC PreparedStatement generated keys for Sequences - * - * @author Vlad Mihalcea - */ -public class OracleSequenceGeneratedKeysBatchPreparedStatementTest extends AbstractSequenceGeneratedKeysBatchPreparedStatementTest { - - @Override - protected String callSequenceSyntax() { - return "select post_seq.NEXTVAL from dual"; - } - - @Override - protected DataSourceProvider dataSourceProvider() { - return new OracleDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/PostgreSQLSequenceCallTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/PostgreSQLSequenceCallTest.java deleted file mode 100644 index 400e346f7..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/PostgreSQLSequenceCallTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch.generatedkeys.sequence; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; - -/** - * PostgreSQLSequenceCallTest - PostgreSQL sequence call - * - * @author Vlad Mihalcea - */ -public class PostgreSQLSequenceCallTest extends AbstractSequenceCallTest { - - @Override - protected String callSequenceSyntax() { - return "select nextval('post_seq')"; - } - - @Override - protected DataSourceProvider dataSourceProvider() { - return new PostgreSQLDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/PostgreSQLSequenceGeneratedKeysBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/PostgreSQLSequenceGeneratedKeysBatchPreparedStatementTest.java deleted file mode 100644 index e436ea713..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/PostgreSQLSequenceGeneratedKeysBatchPreparedStatementTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch.generatedkeys.sequence; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; - -/** - * PostgreSQLSequenceGeneratedKeysBatchPreparedStatementTest - PostgreSQL class for testing JDBC PreparedStatement generated keys for Sequences - * - * @author Vlad Mihalcea - */ -public class PostgreSQLSequenceGeneratedKeysBatchPreparedStatementTest extends AbstractSequenceGeneratedKeysBatchPreparedStatementTest { - - @Override - protected String callSequenceSyntax() { - return "select nextval('post_seq')"; - } - - @Override - protected DataSourceProvider dataSourceProvider() { - return new PostgreSQLDataSourceProvider(); - } -} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/SQLServerSequenceCallTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/SQLServerSequenceCallTest.java deleted file mode 100644 index 8fedeea05..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/SQLServerSequenceCallTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch.generatedkeys.sequence; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; - -/** - * PostgreSQLSequenceCallTest - PostgreSQL sequence call - * - * @author Vlad Mihalcea - */ -public class SQLServerSequenceCallTest extends AbstractSequenceCallTest { - - @Override - protected String callSequenceSyntax() { - return "select NEXT VALUE FOR post_seq"; - } - - @Override - protected DataSourceProvider dataSourceProvider() { - return new SQLServerDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/SQLServerSequenceGeneratedKeysBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/SQLServerSequenceGeneratedKeysBatchPreparedStatementTest.java deleted file mode 100644 index b1bf28ed2..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/SQLServerSequenceGeneratedKeysBatchPreparedStatementTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch.generatedkeys.sequence; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; - -/** - * SQLServerSequenceGeneratedKeysBatchPreparedStatementTest - SQL Server class for testing JDBC PreparedStatement generated keys for Sequences - * - * @author Vlad Mihalcea - */ -public class SQLServerSequenceGeneratedKeysBatchPreparedStatementTest extends AbstractSequenceGeneratedKeysBatchPreparedStatementTest { - - @Override - protected String callSequenceSyntax() { - return "select NEXT VALUE FOR post_seq"; - } - - @Override - protected DataSourceProvider dataSourceProvider() { - return new SQLServerDataSourceProvider(); - } -} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/StatementCachePoolableTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/StatementCachePoolableTest.java deleted file mode 100644 index b15f6f57c..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/StatementCachePoolableTest.java +++ /dev/null @@ -1,220 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.caching; - -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.ReflectionUtils; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.JTDSDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; -import net.sourceforge.jtds.jdbcx.JtdsDataSource; -import org.junit.Test; -import org.junit.runners.Parameterized; -import org.postgresql.ds.PGSimpleDataSource; - -import javax.sql.DataSource; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.fail; - -/** - * StatementCacheTest - Test Statement cache - * - * @author Vlad Mihalcea - */ -public class StatementCachePoolableTest extends DataSourceProviderIntegrationTest { - - public static class CachingOracleDataSourceProvider extends OracleDataSourceProvider { - private final int cacheSize; - - CachingOracleDataSourceProvider(int cacheSize) { - this.cacheSize = cacheSize; - } - - @Override - public DataSource dataSource() { - DataSource dataSource = super.dataSource(); - try { - Properties connectionProperties = ReflectionUtils.invokeGetter(dataSource, "connectionProperties"); - if(connectionProperties == null) { - connectionProperties = new Properties(); - } - connectionProperties.put("oracle.jdbc.implicitStatementCacheSize", Integer.toString(cacheSize)); - ReflectionUtils.invokeSetter(dataSource, "connectionProperties", connectionProperties); - } catch (Exception e) { - fail(e.getMessage()); - } - return dataSource; - } - - @Override - public String toString() { - return "CachingOracleDataSourceProvider{" + - "cacheSize=" + cacheSize + - '}'; - } - } - - public static class CachingJTDSDataSourceProvider extends JTDSDataSourceProvider { - private final int cacheSize; - - CachingJTDSDataSourceProvider(int cacheSize) { - this.cacheSize = cacheSize; - } - - @Override - public DataSource dataSource() { - JtdsDataSource dataSource = (JtdsDataSource) super.dataSource(); - dataSource.setMaxStatements(cacheSize); - return dataSource; - } - - @Override - public String toString() { - return "CachingJTDSDataSourceProvider{" + - "cacheSize=" + cacheSize + - '}'; - } - } - - public static class CachingPostgreSQLDataSourceProvider extends PostgreSQLDataSourceProvider { - private final int cacheSize; - - CachingPostgreSQLDataSourceProvider(int cacheSize) { - this.cacheSize = cacheSize; - } - - @Override - public DataSource dataSource() { - PGSimpleDataSource dataSource = (PGSimpleDataSource) super.dataSource(); - dataSource.setPreparedStatementCacheQueries(cacheSize); - return dataSource; - } - - @Override - public String toString() { - return "CachingPostgreSQLDataSourceProvider{" + - "cacheSize=" + cacheSize + - '}'; - } - } - - public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; - - public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"; - - private BlogEntityProvider entityProvider = new BlogEntityProvider(); - - public StatementCachePoolableTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); - } - - @Parameterized.Parameters - public static Collection rdbmsDataSourceProvider() { - List providers = new ArrayList<>(); - providers.add(new DataSourceProvider[]{ - new CachingOracleDataSourceProvider(1) - }); - providers.add(new DataSourceProvider[]{ - new CachingJTDSDataSourceProvider(1) - }); - providers.add(new DataSourceProvider[]{ - new CachingPostgreSQLDataSourceProvider(1) - }); - MySQLDataSourceProvider mySQLCachingDataSourceProvider = new MySQLDataSourceProvider(); - mySQLCachingDataSourceProvider.setUseServerPrepStmts(true); - mySQLCachingDataSourceProvider.setCachePrepStmts(true); - providers.add(new DataSourceProvider[]{ - mySQLCachingDataSourceProvider - }); - return providers; - } - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Override - public void init() { - super.init(); - doInJDBC(connection -> { - try ( - PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); - PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); - ) { - int postCount = getPostCount(); - int postCommentCount = getPostCommentCount(); - - int index; - - for (int i = 0; i < postCount; i++) { - index = 0; - postStatement.setString(++index, String.format("Post no. %1$d", i)); - postStatement.setInt(++index, 0); - postStatement.setLong(++index, i); - postStatement.executeUpdate(); - } - - for (int i = 0; i < postCount; i++) { - for (int j = 0; j < postCommentCount; j++) { - index = 0; - postCommentStatement.setLong(++index, i); - postCommentStatement.setString(++index, String.format("Post comment %1$d", j)); - postCommentStatement.setInt(++index, (int) (Math.random() * 1000)); - postCommentStatement.setLong(++index, (postCommentCount * i) + j); - postCommentStatement.executeUpdate(); - } - } - } catch (SQLException e) { - fail(e.getMessage()); - } - }); - } - - @Test - public void selectWhenCaching() { - AtomicInteger counter = new AtomicInteger(); - doInJDBC(connection -> { - for (int i = 0; i < 2; i++) { - try (PreparedStatement statement = connection.prepareStatement( - "select p.title, pd.created_on " + - "from post p " + - "left join post_details pd on p.id = pd.id " + - "where EXISTS ( " + - " select 1 from post_comment where post_id > p.id and version = ?" + - ")" - )) { - statement.setPoolable(false); - statement.setInt(1, counter.incrementAndGet()); - statement.execute(); - } catch (Throwable e) { - LOGGER.error("Failed test", e); - } - } - }); - LOGGER.info("When using {}, throughput is {} statements", - dataSourceProvider(), - counter.get()); - } - - protected int getPostCount() { - return 1000; - } - - protected int getPostCommentCount() { - return 5; - } - - @Override - protected boolean proxyDataSource() { - return false; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/StatementCacheTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/StatementCacheTest.java deleted file mode 100644 index 9a5a107f7..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/StatementCacheTest.java +++ /dev/null @@ -1,237 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.caching; - -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.ReflectionUtils; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.JTDSDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; -import net.sourceforge.jtds.jdbcx.JtdsDataSource; -import org.junit.Test; -import org.junit.runners.Parameterized; -import org.postgresql.ds.PGSimpleDataSource; - -import javax.sql.DataSource; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.fail; - -/** - * StatementCacheTest - Test Statement cache - * - * @author Vlad Mihalcea - */ -public class StatementCacheTest extends DataSourceProviderIntegrationTest { - public static class CachingOracleDataSourceProvider extends OracleDataSourceProvider { - private final int cacheSize; - - CachingOracleDataSourceProvider(int cacheSize) { - this.cacheSize = cacheSize; - } - - @Override - public DataSource dataSource() { - DataSource dataSource = super.dataSource(); - try { - Properties connectionProperties = ReflectionUtils.invokeGetter(dataSource, "connectionProperties"); - if(connectionProperties == null) { - connectionProperties = new Properties(); - } - connectionProperties.put("oracle.jdbc.implicitStatementCacheSize", Integer.toString(cacheSize)); - ReflectionUtils.invokeSetter(dataSource, "connectionProperties", connectionProperties); - } catch (Exception e) { - fail(e.getMessage()); - } - return dataSource; - } - - @Override - public String toString() { - return "CachingOracleDataSourceProvider{" + - "cacheSize=" + cacheSize + - '}'; - } - } - - public static class CachingJTDSDataSourceProvider extends JTDSDataSourceProvider { - private final int cacheSize; - - CachingJTDSDataSourceProvider(int cacheSize) { - this.cacheSize = cacheSize; - } - - @Override - public DataSource dataSource() { - JtdsDataSource dataSource = (JtdsDataSource) super.dataSource(); - dataSource.setMaxStatements(cacheSize); - return dataSource; - } - - @Override - public String toString() { - return "CachingJTDSDataSourceProvider{" + - "cacheSize=" + cacheSize + - '}'; - } - } - - public static class CachingPostgreSQLDataSourceProvider extends PostgreSQLDataSourceProvider { - private final int cacheSize; - - CachingPostgreSQLDataSourceProvider(int cacheSize) { - this.cacheSize = cacheSize; - } - - @Override - public DataSource dataSource() { - PGSimpleDataSource dataSource = (PGSimpleDataSource) super.dataSource(); - dataSource.setPreparedStatementCacheQueries(cacheSize); - return dataSource; - } - - @Override - public String toString() { - return "CachingPostgreSQLDataSourceProvider{" + - "cacheSize=" + cacheSize + - '}'; - } - } - - public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; - - public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"; - - private BlogEntityProvider entityProvider = new BlogEntityProvider(); - - public StatementCacheTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); - } - - @Parameterized.Parameters - public static Collection rdbmsDataSourceProvider() { - List providers = new ArrayList<>(); - providers.add(new DataSourceProvider[]{ - new CachingOracleDataSourceProvider(1) - }); - providers.add(new DataSourceProvider[]{ - new CachingOracleDataSourceProvider(0) - }); - providers.add(new DataSourceProvider[]{ - new CachingJTDSDataSourceProvider(1) - }); - providers.add(new DataSourceProvider[]{ - new CachingJTDSDataSourceProvider(0) - }); - providers.add(new DataSourceProvider[]{ - new CachingPostgreSQLDataSourceProvider(1) - }); - providers.add(new DataSourceProvider[]{ - new CachingPostgreSQLDataSourceProvider(0) - }); - MySQLDataSourceProvider mySQLCachingDataSourceProvider = new MySQLDataSourceProvider(); - mySQLCachingDataSourceProvider.setUseServerPrepStmts(false); - mySQLCachingDataSourceProvider.setCachePrepStmts(true); - providers.add(new DataSourceProvider[]{ - mySQLCachingDataSourceProvider - }); - MySQLDataSourceProvider mySQLNoCachingDataSourceProvider = new MySQLDataSourceProvider(); - mySQLNoCachingDataSourceProvider.setUseServerPrepStmts(false); - mySQLNoCachingDataSourceProvider.setCachePrepStmts(false); - providers.add(new DataSourceProvider[]{ - mySQLNoCachingDataSourceProvider - }); - return providers; - } - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Override - public void init() { - super.init(); - doInJDBC(connection -> { - try ( - PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); - PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); - ) { - int postCount = getPostCount(); - int postCommentCount = getPostCommentCount(); - - int index; - - for (int i = 0; i < postCount; i++) { - index = 0; - postStatement.setString(++index, String.format("Post no. %1$d", i)); - postStatement.setInt(++index, 0); - postStatement.setLong(++index, i); - postStatement.executeUpdate(); - } - - for (int i = 0; i < postCount; i++) { - for (int j = 0; j < postCommentCount; j++) { - index = 0; - postCommentStatement.setLong(++index, i); - postCommentStatement.setString(++index, String.format("Post comment %1$d", j)); - postCommentStatement.setInt(++index, (int) (Math.random() * 1000)); - postCommentStatement.setLong(++index, (postCommentCount * i) + j); - postCommentStatement.executeUpdate(); - } - } - } catch (SQLException e) { - fail(e.getMessage()); - } - }); - } - - @Test - public void selectWhenCaching() { - long ttlMillis = System.currentTimeMillis() + getRunMillis(); - AtomicInteger counter = new AtomicInteger(); - doInJDBC(connection -> { - while (System.currentTimeMillis() < ttlMillis) - try (PreparedStatement statement = connection.prepareStatement( - "select p.title, pd.created_on " + - "from post p " + - "left join post_details pd on p.id = pd.id " + - "where EXISTS ( " + - " select 1 from post_comment where post_id > p.id and version = ?" + - ")" - )) { - statement.setInt(1, counter.incrementAndGet()); - statement.execute(); - } catch (SQLException e) { - fail(e.getMessage()); - } - }); - LOGGER.info("When using {}, throughput is {} statements", - dataSourceProvider(), - counter.get()); - } - - protected int getPostCount() { - return 1000; - } - - protected int getPostCommentCount() { - return 5; - } - - protected int getRunMillis() { - return 60 * 1000; - } - - @Override - protected boolean proxyDataSource() { - return false; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/connection/ConnectionPoolCallTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/connection/ConnectionPoolCallTest.java deleted file mode 100644 index 5291618eb..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/connection/ConnectionPoolCallTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.connection; - -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Slf4jReporter; -import com.codahale.metrics.Timer; -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.junit.Ignore; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Properties; -import java.util.concurrent.TimeUnit; - -/** - * @author Vlad Mihalcea - */ -@Ignore -public class ConnectionPoolCallTest extends DataSourceProviderIntegrationTest { - - private final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - private MetricRegistry metricRegistry = new MetricRegistry(); - - private Timer timer = metricRegistry.timer("callTimer"); - - private Slf4jReporter logReporter = Slf4jReporter - .forRegistry(metricRegistry) - .outputTo(LOGGER) - .build(); - - private int callCount = 1000; - - public ConnectionPoolCallTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); - } - - @Override - protected Class[] entities() { - return new Class[]{}; - } - - @Test - public void testNoPooling() throws SQLException { - LOGGER.info("Test without pooling for {}", dataSourceProvider().database()); - test(dataSourceProvider().dataSource()); - } - - @Test - public void testPooling() throws SQLException { - LOGGER.info("Test with pooling for {}", dataSourceProvider().database()); - test(poolingDataSource()); - } - - private void test(DataSource dataSource) throws SQLException { - for (int i = 0; i < callCount; i++) { - long startNanos = System.nanoTime(); - try (Connection connection = dataSource.getConnection()) { - } - timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - } - logReporter.report(); - } - - protected HikariDataSource poolingDataSource() { - Properties properties = new Properties(); - properties.setProperty("dataSourceClassName", dataSourceProvider().dataSourceClassName().getName()); - properties.put("dataSourceProperties", dataSourceProvider().dataSourceProperties()); - //properties.setProperty("minimumPoolSize", String.valueOf(1)); - properties.setProperty("maximumPoolSize", String.valueOf(3)); - properties.setProperty("connectionTimeout", String.valueOf(5000)); - return new HikariDataSource(new HikariConfig(properties)); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetColumnSizeTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetColumnSizeTest.java deleted file mode 100644 index 18f7ac9ff..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetColumnSizeTest.java +++ /dev/null @@ -1,195 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.fetching; - -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Slf4jReporter; -import com.codahale.metrics.Timer; -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; - -import org.junit.Test; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.fail; - -/** - * ResultSetColumnSizeTest - Test result set column size - * - * @author Vlad Mihalcea - */ -public class ResultSetColumnSizeTest extends DataSourceProviderIntegrationTest { - - public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; - - public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"; - - public static final String INSERT_POST_DETAILS= "insert into post_details (id, created_on, version) values (?, ?, ?)"; - - public static final String SELECT_ALL = - "select * " + - "from post_comment pc " + - "inner join post p on p.id = pc.post_id " + - "inner join post_details pd on p.id = pd.id "; - - public static final String SELECT_ID = - "select pc.version " + - "from post_comment pc " + - "inner join post p on p.id = pc.post_id " + - "inner join post_details pd on p.id = pd.id "; - - - private MetricRegistry metricRegistry = new MetricRegistry(); - - private Timer timer = metricRegistry.timer("callSequence"); - - private Slf4jReporter logReporter = Slf4jReporter - .forRegistry(metricRegistry) - .outputTo(LOGGER) - .build(); - - private BlogEntityProvider entityProvider = new BlogEntityProvider(); - - public ResultSetColumnSizeTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); - } - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Override - public void init() { - super.init(); - doInJDBC(connection -> { - LOGGER.info("{} supports CLOSE_CURSORS_AT_COMMIT {}", - dataSourceProvider().database(), - connection.getMetaData().supportsResultSetHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT) - ); - - LOGGER.info("{} supports HOLD_CURSORS_OVER_COMMIT {}", - dataSourceProvider().database(), - connection.getMetaData().supportsResultSetHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT) - ); - - try ( - PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); - PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); - PreparedStatement postDetailsStatement = connection.prepareStatement(INSERT_POST_DETAILS); - ) { - - if (postStatement.getResultSetHoldability() == ResultSet.CLOSE_CURSORS_AT_COMMIT) { - LOGGER.info("{} default holdability CLOSE_CURSORS_AT_COMMIT", - dataSourceProvider().database() - ); - } else if (postStatement.getResultSetHoldability() == ResultSet.HOLD_CURSORS_OVER_COMMIT) { - LOGGER.info("{} default holdability HOLD_CURSORS_OVER_COMMIT", - dataSourceProvider().database() - ); - } else { - fail(); - } - - int postCount = getPostCount(); - int postCommentCount = getPostCommentCount(); - - int index; - - for (int i = 0; i < postCount; i++) { - if (i > 0 && i % 100 == 0) { - postStatement.executeBatch(); - postDetailsStatement.executeBatch(); - } - - index = 0; - postStatement.setString(++index, String.format("Post no. %1$d", i)); - postStatement.setInt(++index, 0); - postStatement.setLong(++index, i); - postStatement.addBatch(); - - index = 0; - postDetailsStatement.setInt(++index, i); - postDetailsStatement.setTimestamp(++index, new Timestamp(System.currentTimeMillis())); - postDetailsStatement.setInt(++index, 0); - postDetailsStatement.addBatch(); - } - postStatement.executeBatch(); - postDetailsStatement.executeBatch(); - - for (int i = 0; i < postCount; i++) { - for (int j = 0; j < postCommentCount; j++) { - index = 0; - postCommentStatement.setLong(++index, i); - postCommentStatement.setString(++index, String.format("Post comment %1$d", j)); - postCommentStatement.setInt(++index, (int) (Math.random() * 1000)); - postCommentStatement.setLong(++index, (postCommentCount * i) + j); - postCommentStatement.addBatch(); - if (j % 100 == 0) { - postCommentStatement.executeBatch(); - } - } - } - postCommentStatement.executeBatch(); - } catch (SQLException e) { - fail(e.getMessage()); - } - }); - } - - @Test - public void testSelectAll() { - testInternal(SELECT_ALL); - } - - @Test - public void testProjection() { - testInternal(SELECT_ID); - } - - public void testInternal(String sql) { - doInJDBC(connection -> { - for (int i = 0; i < runCount(); i++) { - try (PreparedStatement statement = connection.prepareStatement( - sql - )) { - statement.execute(); - long startNanos = System.nanoTime(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - Object[] values = new Object[resultSet.getMetaData().getColumnCount()]; - for (int j = 0; j < values.length; j++) { - values[j] = resultSet.getObject(j + 1); - } - } - timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - } catch (SQLException e) { - fail(e.getMessage()); - } - } - }); - LOGGER.info("{} Result Set statement {}", dataSourceProvider().database(), sql); - logReporter.report(); - } - - private int runCount() { - return 10; - } - - protected int getPostCount() { - return 100; - } - - protected int getPostCommentCount() { - return 10; - } - - @Override - protected boolean proxyDataSource() { - return false; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetFetchSizeTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetFetchSizeTest.java deleted file mode 100644 index 988f6e056..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetFetchSizeTest.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.fetching; - -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; - -import org.junit.Test; -import org.junit.runners.Parameterized; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.fail; - -/** - * ResultSetFetchSizeTest - Test result set fetch size - * - * @author Vlad Mihalcea - */ -public class ResultSetFetchSizeTest extends DataSourceProviderIntegrationTest { - - public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; - - private BlogEntityProvider entityProvider = new BlogEntityProvider(); - - private final Integer fetchSize; - - public ResultSetFetchSizeTest(DataSourceProvider dataSourceProvider, Integer fetchSize) { - super(dataSourceProvider); - this.fetchSize = fetchSize; - } - - @Parameterized.Parameters - public static Collection parameters() { - List providers = new ArrayList<>(); - for (int i = 0; i < dataSourceProviders.length; i++) { - DataSourceProvider dataSourceProvider = dataSourceProviders[i]; - for (int j = 0; j < fetchSizes.length; j++) { - Integer fetchSize = fetchSizes[j]; - providers.add(new Object[] {dataSourceProvider, fetchSize}); - } - } - return providers; - } - - private static Integer[] fetchSizes = new Integer[] { - //null, 1, 10, 100, 1000, 10000 - 1, 10, 100, 1000, 10000 - }; - - private static DataSourceProvider[] dataSourceProviders = new DataSourceProvider[]{ - new OracleDataSourceProvider(), - new SQLServerDataSourceProvider(), - new PostgreSQLDataSourceProvider(), - new MySQLDataSourceProvider() - }; - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Override - public void init() { - super.init(); - doInJDBC(connection -> { - try ( - PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); - ) { - int postCount = getPostCount(); - - int index; - - for (int i = 0; i < postCount; i++) { - if (i > 0 && i % 100 == 0) { - postStatement.executeBatch(); - } - index = 0; - postStatement.setString(++index, String.format("Post no. %1$d", i)); - postStatement.setInt(++index, 0); - postStatement.setLong(++index, i); - postStatement.addBatch(); - } - postStatement.executeBatch(); - } catch (SQLException e) { - fail(e.getMessage()); - } - }); - } - - @Test - public void testFetchSize() { - long startNanos = System.nanoTime(); - doInJDBC(connection -> { - try (PreparedStatement statement = connection.prepareStatement( - "select * from post" - )) { - if (fetchSize != null) { - statement.setFetchSize(fetchSize); - } - statement.execute(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - resultSet.getLong(1); - } - } catch (SQLException e) { - fail(e.getMessage()); - } - - }); - LOGGER.info("{} fetch size {} took {} millis", - dataSourceProvider().database(), - fetchSize != null ? fetchSize : "N/A", - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); - } - - protected int getPostCount() { - return 10000; - } - - @Override - protected boolean proxyDataSource() { - return false; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetLimitTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetLimitTest.java deleted file mode 100644 index b0dfd9e67..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetLimitTest.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.fetching; - -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; - -import org.hibernate.dialect.pagination.LimitHandler; -import org.hibernate.engine.spi.RowSelection; -import org.hibernate.internal.SessionFactoryImpl; -import org.junit.Test; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * ResultSetLimitTest - Test limiting result set vs fetching and discarding rows - * - * @author Vlad Mihalcea - */ -public class ResultSetLimitTest extends DataSourceProviderIntegrationTest { - public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; - - public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"; - - public static final String SELECT_POST_COMMENT = - "SELECT pc.id AS pc_id, p.title AS p_title " + - "FROM post_comment pc " + - "INNER JOIN post p ON p.id = pc.post_id " + - "ORDER BY pc_id"; - - private BlogEntityProvider entityProvider = new BlogEntityProvider(); - - public ResultSetLimitTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); - } - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Override - public void init() { - super.init(); - doInJDBC(connection -> { - try ( - PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); - PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); - ) { - int postCount = getPostCount(); - int postCommentCount = getPostCommentCount(); - - int index; - - for (int i = 0; i < postCount; i++) { - if (i > 0 && i % 100 == 0) { - postStatement.executeBatch(); - } - index = 0; - postStatement.setString(++index, String.format("Post no. %1$d", i)); - postStatement.setInt(++index, 0); - postStatement.setLong(++index, i); - postStatement.addBatch(); - } - postStatement.executeBatch(); - - for (int i = 0; i < postCount; i++) { - for (int j = 0; j < postCommentCount; j++) { - index = 0; - postCommentStatement.setLong(++index, i); - postCommentStatement.setString(++index, String.format("Post comment %1$d", j)); - postCommentStatement.setInt(++index, (int) (Math.random() * 1000)); - postCommentStatement.setLong(++index, (postCommentCount * i) + j); - postCommentStatement.addBatch(); - if (j % 100 == 0) { - postCommentStatement.executeBatch(); - } - } - } - postCommentStatement.executeBatch(); - } catch (SQLException e) { - fail(e.getMessage()); - } - }); - } - - @Test - public void testNoLimit() { - long startNanos = System.nanoTime(); - doInJDBC(connection -> { - try (PreparedStatement statement = connection.prepareStatement( - SELECT_POST_COMMENT - )) { - statement.execute(); - ResultSet resultSet = statement.getResultSet(); - int count = 0; - while (resultSet.next()) { - resultSet.getLong(1); - count++; - } - assertEquals(getPostCount() * getPostCommentCount(), count); - } catch (SQLException e) { - fail(e.getMessage()); - } - - }); - LOGGER.info("{} Result Set without limit took {} millis", - dataSourceProvider().database(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); - } - - @Test - public void testLimit() { - final RowSelection rowSelection = new RowSelection(); - rowSelection.setMaxRows(getMaxRows()); - LimitHandler limitHandler = ((SessionFactoryImpl) sessionFactory()).getDialect().getLimitHandler(); - String limitStatement = limitHandler.processSql(SELECT_POST_COMMENT, rowSelection); - long startNanos = System.nanoTime(); - doInJDBC(connection -> { - try (PreparedStatement statement = connection.prepareStatement(limitStatement)) { - limitHandler.bindLimitParametersAtEndOfQuery(rowSelection, statement, 1); - statement.setInt(1, getMaxRows()); - statement.execute(); - int count = 0; - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - resultSet.getLong(1); - count++; - } - assertEquals(getMaxRows(), count); - } catch (SQLException e) { - fail(e.getMessage()); - } - - }); - LOGGER.info("{} Result Set with limit took {} millis", - dataSourceProvider().database(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); - } - - @Test - public void testMaxSize() { - long startNanos = System.nanoTime(); - doInJDBC(connection -> { - try (PreparedStatement statement = connection.prepareStatement( - SELECT_POST_COMMENT - )) { - statement.setMaxRows(getMaxRows()); - statement.execute(); - ResultSet resultSet = statement.getResultSet(); - int count = 0; - while (resultSet.next()) { - resultSet.getLong(1); - count++; - } - assertEquals(getMaxRows(), count); - } catch (SQLException e) { - fail(e.getMessage()); - } - - }); - LOGGER.info("{} Result Set maxSize took {} millis", - dataSourceProvider().database(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); - } - - protected int getPostCount() { - //return 100000; - return 100; - } - - protected int getPostCommentCount() { - return 10; - } - - protected int getMaxRows() { - return 100; - } - - - @Override - protected boolean proxyDataSource() { - return false; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/index/PostgresIndexSelectivityTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/index/PostgresIndexSelectivityTest.java deleted file mode 100644 index cf8912228..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/index/PostgresIndexSelectivityTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.index; - -import com.vladmihalcea.book.hpjp.jdbc.index.providers.IndexEntityProvider; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.junit.Test; -import org.postgresql.PGStatement; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Proxy; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.*; - -/** - * PostgresIndexSelectivityTest - Test PostgreSQL index selectivity - * - * @author Vlad Mihalcea - */ -public class PostgresIndexSelectivityTest extends AbstractPostgreSQLIntegrationTest { - - public static final String INSERT_TASK = "insert into Task (id, status) values (?, ?)"; - - private final IndexEntityProvider entityProvider = new IndexEntityProvider(); - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Test - public void testInsert() { - AtomicInteger statementCount = new AtomicInteger(); - long startNanos = System.nanoTime(); - doInJDBC(connection -> { - try (PreparedStatement statement = connection.prepareStatement(INSERT_TASK)) { - int taskCount = getPostCount(); - - for (int i = 0; i < taskCount; i++) { - String task = "DONE"; - if (i > 99000) { - task = "TO_DO"; - } else if (i > 95000) { - task = "FAILED"; - } - statement.setLong(1, i); - statement.setString(2, task); - executeStatement(statement, statementCount); - } - statement.executeBatch(); - } catch (SQLException e) { - fail(e.getMessage()); - } - }); - LOGGER.info("{}.testInsert took {} millis", - getClass().getSimpleName(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); - doInJDBC(connection -> { - try (PreparedStatement statement = connection.prepareStatement( - "select * from task where id = ?" - )) { - - assertFalse(isUseServerPrepare(statement)); - setPrepareThreshold(statement, 1); - statement.setInt(1, 100); - statement.execute(); - assertTrue(isUseServerPrepare(statement)); - } - }); - } - - public boolean isUseServerPrepare(Statement statement) { - if(statement instanceof PGStatement) { - PGStatement pgStatement = (PGStatement) statement; - return pgStatement.isUseServerPrepare(); - } else { - InvocationHandler handler = Proxy.getInvocationHandler(statement); - try { - return (boolean) handler.invoke(statement, PGStatement.class.getMethod("isUseServerPrepare"), null); - } catch (Throwable e) { - throw new IllegalArgumentException(e); - } - } - } - - public void setPrepareThreshold(Statement statement, int threshold) throws SQLException { - if(statement instanceof PGStatement) { - PGStatement pgStatement = (PGStatement) statement; - pgStatement.setPrepareThreshold(threshold); - } else { - InvocationHandler handler = Proxy.getInvocationHandler(statement); - try { - handler.invoke(statement, PGStatement.class.getMethod("setPrepareThreshold", int.class), new Object[]{threshold}); - } catch (Throwable throwable) { - throw new IllegalArgumentException(throwable); - } - } - } - - private void executeStatement(PreparedStatement statement, AtomicInteger statementCount) throws SQLException { - statement.addBatch(); - int count = statementCount.incrementAndGet(); - if(count % getBatchSize() == 0) { - statement.executeBatch(); - } - } - - protected int getPostCount() { - return 1 * 1000; - } - - protected int getBatchSize() { - return 100; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/index/providers/IndexEntityProvider.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/index/providers/IndexEntityProvider.java deleted file mode 100644 index 821eb8d10..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/index/providers/IndexEntityProvider.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.index.providers; - -import com.vladmihalcea.book.hpjp.util.EntityProvider; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; - -/** - * @author Vlad Mihalcea - */ -public class IndexEntityProvider implements EntityProvider { - - @Override - public Class[] entities() { - return new Class[]{ - Task.class - }; - } - - @Entity(name = "Task") - public static class Task { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - - private String status; - - public Task(String status) { - this.status = status; - } - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/OracleConnectionReadyOnlyTransactionTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/OracleConnectionReadyOnlyTransactionTest.java deleted file mode 100644 index 8db0ee882..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/OracleConnectionReadyOnlyTransactionTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction; - -import org.junit.runners.Parameterized; - -import java.sql.CallableStatement; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Collection; -import java.util.Collections; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; - -/** - * OracleConnectionReadyOnlyTransactionTest - Test to verify Oracle driver supports read-only transactions - * - * @author Vlad Mihalcea - */ -public class OracleConnectionReadyOnlyTransactionTest extends ConnectionReadyOnlyTransactionTest { - - public OracleConnectionReadyOnlyTransactionTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); - } - - @Parameterized.Parameters - public static Collection rdbmsDataSourceProvider() { - return Collections.singletonList(new DataSourceProvider[]{new OracleDataSourceProvider()}); - } - - protected void setReadOnly(Connection connection) throws SQLException { - connection.setAutoCommit(false); - try(CallableStatement statement = connection.prepareCall("begin set transaction read only; end;")) { - statement.execute(); - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/SQLServerConnectionReadyOnlyTransactionTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/SQLServerConnectionReadyOnlyTransactionTest.java deleted file mode 100644 index f2d308aa2..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/SQLServerConnectionReadyOnlyTransactionTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction; - -import com.microsoft.sqlserver.jdbc.SQLServerDataSource; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; - -import org.junit.runners.Parameterized; - -import javax.sql.DataSource; -import java.util.Collection; -import java.util.Collections; - -/** - * SQLServerConnectionReadyOnlyTransactionTest - Test to verify SQL Server driver supports read-only transactions - * - * @author Vlad Mihalcea - */ -public class SQLServerConnectionReadyOnlyTransactionTest extends ConnectionReadyOnlyTransactionTest { - - public SQLServerConnectionReadyOnlyTransactionTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); - } - - @Parameterized.Parameters - public static Collection rdbmsDataSourceProvider() { - return Collections.singletonList(new DataSourceProvider[]{new SQLServerDataSourceProvider() { - @Override - public DataSource dataSource() { - SQLServerDataSource dataSource = (SQLServerDataSource) super.dataSource(); - dataSource.setURL(dataSource.getURL() + ";ApplicationIntent=ReadOnly"); - return dataSource; - } - }}); - } - - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/AbstractPredicateLockTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/AbstractPredicateLockTest.java deleted file mode 100644 index 7be7bf9de..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/AbstractPredicateLockTest.java +++ /dev/null @@ -1,265 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.LockMode; -import org.hibernate.LockOptions; -import org.hibernate.Session; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * @author Vlad Mihalcea - */ -public abstract class AbstractPredicateLockTest extends AbstractTest { - - public static final int WAIT_MILLIS = 500; - - private final CountDownLatch aliceLatch = new CountDownLatch(1); - private final CountDownLatch bobLatch = new CountDownLatch(1); - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class - }; - } - - @Override - public void init() { - super.init(); - doInHibernate(session -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - session.persist(post); - - for (long i = 1; i <= 3; i++) { - PostComment comment = new PostComment(); - comment.setId(i); - comment.setReview(String.format("Comment nr. %d", i)); - post.addComment(comment); - } - }); - - } - - @Test - public void testRangeLockPreventsInsert() throws SQLException { - AtomicBoolean prevented = new AtomicBoolean(); - - doInHibernate( session -> { - session.unwrap(Session.class).doWork(this::prepareConnection); - List comments = session.createQuery( - "select c " + - "from PostComment c " + - "where c.post.id = :id", PostComment.class) - .setParameter("id", 1L) - .setLockOptions(new LockOptions(LockMode.PESSIMISTIC_WRITE)) - .getResultList(); - - executeAsync(() -> { - doInHibernate(_session -> { - _session.unwrap(Session.class).doWork(this::prepareConnection); - - Post post = _session.getReference(Post.class, 1L); - - PostComment comment = new PostComment(); - comment.setId((long) comments.size() + 1); - comment.setReview(String.format("Comment nr. %d", comments.size() + 1)); - comment.setPost(post); - - _session.persist(comment); - - aliceLatch.countDown(); - _session.flush(); - LOGGER.info("Insert {} prevented by explicit lock", prevented.get() ? "was" : "was not"); - bobLatch.countDown(); - }); - }); - - awaitOnLatch(aliceLatch); - sleep(WAIT_MILLIS); - LOGGER.info("Alice woke up!"); - prevented.set(true); - } ); - awaitOnLatch(bobLatch); - } - - @Override - protected boolean nativeHibernateSessionFactoryBootstrap() { - return true; - } - - @Test - public void testRangeLockPreventsDelete() throws SQLException { - AtomicBoolean prevented = new AtomicBoolean(); - - doInHibernate( session -> { - session.unwrap(Session.class).doWork(this::prepareConnection); - - List comments = session.createQuery( - "select c " + - "from PostComment c " + - "where c.post.id = :id", PostComment.class) - .setParameter("id", 1L) - .setLockMode(LockModeType.PESSIMISTIC_WRITE) - .getResultList(); - - executeAsync(() -> { - doInHibernate(_session -> { - _session.unwrap(Session.class).doWork(this::prepareConnection); - - aliceLatch.countDown(); - _session.createNativeQuery( - "delete from post_comment where id = :id ") - .setParameter("id", 1L) - .executeUpdate(); - - LOGGER.info("Delete {} prevented by explicit lock", prevented.get() ? "was" : "was not"); - bobLatch.countDown(); - }); - }); - - awaitOnLatch(aliceLatch); - sleep(WAIT_MILLIS); - LOGGER.info("Alice woke up!"); - prevented.set(true); - } ); - awaitOnLatch(bobLatch); - } - - @Test - public void testRangeLockPreventsUpdate() throws SQLException { - AtomicBoolean prevented = new AtomicBoolean(); - - doInHibernate( session -> { - session.unwrap(Session.class).doWork(this::prepareConnection); - - List comments = session.createQuery( - "select c " + - "from PostComment c " + - "where c.post.id = :id", PostComment.class) - .setParameter("id", 1L) - .setLockMode(LockModeType.PESSIMISTIC_WRITE) - .getResultList(); - - executeAsync(() -> { - doInHibernate(_session -> { - _session.unwrap(Session.class).doWork(this::prepareConnection); - - aliceLatch.countDown(); - _session.createQuery( - "update PostComment " + - "set review = :review " + - "where id = :id") - .setParameter("review", "Great") - .setParameter("id", 1L) - .executeUpdate(); - - LOGGER.info("Update {} prevented by explicit lock", prevented.get() ? "was" : "was not"); - bobLatch.countDown(); - }); - }); - - awaitOnLatch(aliceLatch); - sleep(WAIT_MILLIS); - LOGGER.info("Alice woke up!"); - prevented.set(true); - } ); - awaitOnLatch(bobLatch); - } - - protected void prepareConnection(Connection connection) { - - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Post post; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/MySQLPredicateLockTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/MySQLPredicateLockTest.java deleted file mode 100644 index d2094df85..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/MySQLPredicateLockTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking; - -import java.sql.Connection; -import java.sql.SQLException; - -import com.vladmihalcea.book.hpjp.jdbc.transaction.locking.AbstractPredicateLockTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; - -import static org.junit.Assert.fail; - -/** - * @author Vlad Mihalcea - */ -public class MySQLPredicateLockTest extends AbstractPredicateLockTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new MySQLDataSourceProvider(); - } - - protected void prepareConnection(Connection connection) { - try { - connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); - } catch (SQLException e) { - fail(e.getMessage()); - } - executeStatement(connection, "SET GLOBAL innodb_lock_wait_timeout = 1"); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/MySQLTableLockTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/MySQLTableLockTest.java deleted file mode 100644 index c4da7df80..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/MySQLTableLockTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking; - -import java.sql.Connection; - -import com.vladmihalcea.book.hpjp.jdbc.transaction.locking.AbstractTableLockTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; - -/** - * @author Vlad Mihalcea - */ -public class MySQLTableLockTest extends AbstractTableLockTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new MySQLDataSourceProvider(); - } - - @Override - protected String lockEmployeeTableSql() { - return "SELECT * FROM employee WHERE department_id = 1 FOR UPDATE"; - } - - @Override - protected void prepareConnection(Connection connection) { - /*try { - connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); - } catch (SQLException e) { - fail(e.getMessage()); - }*/ - executeStatement(connection, "SET GLOBAL innodb_lock_wait_timeout = 1"); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/OraclePredicateLockTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/OraclePredicateLockTest.java deleted file mode 100644 index db87685d3..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/OraclePredicateLockTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking; - -import com.vladmihalcea.book.hpjp.jdbc.transaction.locking.AbstractPredicateLockTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; - -/** - * @author Vlad Mihalcea - */ -public class OraclePredicateLockTest extends AbstractPredicateLockTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new OracleDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/OracleTableLockTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/OracleTableLockTest.java deleted file mode 100644 index bbc6beef5..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/OracleTableLockTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking; - -import com.vladmihalcea.book.hpjp.jdbc.transaction.locking.AbstractTableLockTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; - -/** - * @author Vlad Mihalcea - */ -public class OracleTableLockTest extends AbstractTableLockTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new OracleDataSourceProvider(); - } - - @Override - protected String lockEmployeeTableSql() { - return "LOCK TABLE employee IN SHARE MODE NOWAIT"; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/PostgreSQLPredicateLockTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/PostgreSQLPredicateLockTest.java deleted file mode 100644 index 267f14b06..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/PostgreSQLPredicateLockTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking; - -import com.vladmihalcea.book.hpjp.jdbc.transaction.locking.AbstractPredicateLockTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; - -/** - * @author Vlad Mihalcea - */ -public class PostgreSQLPredicateLockTest extends AbstractPredicateLockTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new PostgreSQLDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/PostgreSQLTableLockTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/PostgreSQLTableLockTest.java deleted file mode 100644 index 34dfc3866..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/PostgreSQLTableLockTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking; - -import java.sql.Connection; - -import com.vladmihalcea.book.hpjp.jdbc.transaction.locking.AbstractTableLockTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; - -/** - * @author Vlad Mihalcea - */ -public class PostgreSQLTableLockTest extends AbstractTableLockTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new PostgreSQLDataSourceProvider(); - } - - @Override - protected String lockEmployeeTableSql() { - return "LOCK TABLE employee IN SHARE ROW EXCLUSIVE MODE NOWAIT"; - } - - @Override - protected void prepareConnection(Connection connection) { - executeStatement(connection, "SET statement_timeout TO 1000"); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/SQLServerPredicateLockTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/SQLServerPredicateLockTest.java deleted file mode 100644 index 36514c0e0..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/SQLServerPredicateLockTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking; - -import com.vladmihalcea.book.hpjp.jdbc.transaction.locking.AbstractPredicateLockTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; - -/** - * @author Vlad Mihalcea - */ -public class SQLServerPredicateLockTest extends AbstractPredicateLockTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new SQLServerDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/SQLServerTableLockTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/SQLServerTableLockTest.java deleted file mode 100644 index eae75cd29..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/SQLServerTableLockTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking; - -import com.vladmihalcea.book.hpjp.jdbc.transaction.locking.AbstractTableLockTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; - -/** - * @author Vlad Mihalcea - */ -public class SQLServerTableLockTest extends AbstractTableLockTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new SQLServerDataSourceProvider(); - } - - @Override - protected String lockEmployeeTableSql() { - return "SELECT * FROM employee WITH (HOLDLOCK) WHERE department_id = 1"; - } - - protected String insertEmployeeSql() { - return "INSERT INTO employee WITH(NOWAIT) (department_id, name, salary, id) VALUES (?, ?, ?, ?)"; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/PostgreSQLNoAdvisoryLocksTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/PostgreSQLNoAdvisoryLocksTest.java deleted file mode 100644 index 45bb4eadf..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/PostgreSQLNoAdvisoryLocksTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking.advisory; - -import java.sql.Connection; - -import com.vladmihalcea.book.hpjp.jdbc.transaction.locking.advisory.AbstractPostgreSQLAdvisoryLocksTest; - -/** - * @author Vlad Mihalcea - */ -public class PostgreSQLNoAdvisoryLocksTest extends AbstractPostgreSQLAdvisoryLocksTest { - - @Override - protected int acquireLock(Connection connection, int logIndex, int workerId) { - LOGGER.info( "Worker {} writes to log {}", workerId, logIndex ); - return logIndex; - } - - @Override - protected void releaseLock(Connection connection, int logIndex, int workerId) { - - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/AbstractPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/AbstractPhenomenaTest.java deleted file mode 100644 index 21efdf11d..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/AbstractPhenomenaTest.java +++ /dev/null @@ -1,478 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.phenomena; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.exception.ExceptionUtil; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import javax.persistence.*; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * PhenomenaTest - Test to validate what phenomena does a certain isolation level prevents - * - * @author Vlad Mihalcea - */ -@RunWith(Parameterized.class) -public abstract class AbstractPhenomenaTest extends AbstractTest { - - public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; - - public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"; - - public static final String INSERT_POST_DETAILS = "insert into post_details (id, created_by, version) values (?, ?, ?)"; - - public static final String INSERT_DEPARTMENT = "insert into department (name, budget, id) values (?, ?, ?)"; - - public static final String INSERT_EMPLOYEE = "insert into employee (department_id, name, salary, id) values (?, ?, ?, ?)"; - - protected final String isolationLevelName; - - protected final int isolationLevel; - - private final CountDownLatch bobLatch = new CountDownLatch(1); - - private BlogEntityProvider entityProvider = new BlogEntityProvider(); - - protected AbstractPhenomenaTest(String isolationLevelName, int isolationLevel) { - this.isolationLevelName = isolationLevelName; - this.isolationLevel = isolationLevel; - } - - public String getIsolationLevelName() { - return isolationLevelName; - } - - public int getIsolationLevel() { - return isolationLevel; - } - - @Override - protected Class[] entities() { - return entityProvider.entities(); - } - - @Parameterized.Parameters - public static Collection isolationLevels() { - List levels = new ArrayList<>(); - levels.add(new Object[]{"Read Uncommitted", Connection.TRANSACTION_READ_UNCOMMITTED}); - levels.add(new Object[]{"Read Committed", Connection.TRANSACTION_READ_COMMITTED}); - levels.add(new Object[]{"Repeatable Read", Connection.TRANSACTION_REPEATABLE_READ}); - levels.add(new Object[]{"Serializable", Connection.TRANSACTION_SERIALIZABLE}); - return levels; - } - - @Override - public void init() { - super.init(); - doInJDBC(connection -> { - try ( - PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); - PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); - PreparedStatement postDetailsStatement = connection.prepareStatement(INSERT_POST_DETAILS); - ) { - int index = 0; - postStatement.setString(++index, "Transactions"); - postStatement.setInt(++index, 0); - postStatement.setLong(++index, 1); - postStatement.executeUpdate(); - - index = 0; - postDetailsStatement.setInt(++index, 1); - postDetailsStatement.setString(++index, "None"); - postDetailsStatement.setInt(++index, 0); - postDetailsStatement.executeUpdate(); - - for (int i = 0; i < 3; i++) { - index = 0; - postCommentStatement.setLong(++index, 1); - postCommentStatement.setString(++index, String.format("Post comment %1$d", i)); - postCommentStatement.setInt(++index, 0); - postCommentStatement.setLong(++index, i); - postCommentStatement.executeUpdate(); - } - } catch (SQLException e) { - fail(e.getMessage()); - } - }); - } - - @Test - public void testDirtyWrite() { - String firstTitle = "Alice"; - final AtomicBoolean preventedByLocking = new AtomicBoolean(); - - doInJDBC(aliceConnection -> { - if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { - LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); - return; - } - prepareConnection(aliceConnection); - update(aliceConnection, updatePostTitleParamSql(), new Object[]{firstTitle}); - try { - executeSync(() -> { - doInJDBC(bobConnection -> { - prepareConnection(bobConnection); - try { - update(bobConnection, updatePostTitleParamSql(), new Object[]{"Bob"}); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - }); - }); - } catch (Exception e) { - if ( !ExceptionUtil.isConnectionClose( e ) ) { - fail(e.getMessage()); - } - } - }); - - doInJDBC(aliceConnection -> { - String title = selectStringColumn(aliceConnection, selectPostTitleSql()); - LOGGER.info("Isolation level {} {} Dirty Write", isolationLevelName, !title.equals(firstTitle) ? "allows" : "prevents"); - if(preventedByLocking.get()) { - LOGGER.info("Isolation level {} prevents Dirty Write by locking", isolationLevelName); - } - }); - } - - @Test - public void testDirtyRead() { - final AtomicBoolean dirtyRead = new AtomicBoolean(); - final AtomicBoolean preventedByLocking = new AtomicBoolean(); - - doInJDBC(aliceConnection -> { - if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { - LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); - return; - } - prepareConnection(aliceConnection); - try (Statement aliceStatement = aliceConnection.createStatement()) { - aliceStatement.executeUpdate(updatePostTitleSql()); - executeSync(() -> { - doInJDBC(bobConnection -> { - prepareConnection(bobConnection); - try { - String title = selectStringColumn(bobConnection, selectPostTitleSql()); - if ("Transactions".equals(title)) { - LOGGER.info("No Dirty Read, uncommitted data is not viewable"); - } else if ("ACID".equals(title)) { - dirtyRead.set(true); - } else { - fail("Unknown title: " + title); - } - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - }); - }); - } - }); - - LOGGER.info("Isolation level {} {} Dirty Read", isolationLevelName, dirtyRead.get() ? "allows" : "prevents"); - if(preventedByLocking.get()) { - LOGGER.info("Isolation level {} prevents Dirty Read by locking", isolationLevelName); - } - } - - @Test - public void testNonRepeatableRead() { - final AtomicBoolean preventedByLocking = new AtomicBoolean(); - - doInJDBC(aliceConnection -> { - if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { - LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); - return; - } - prepareConnection(aliceConnection); - String firstTitle = selectStringColumn(aliceConnection, selectPostTitleSql()); - try { - executeSync(() -> { - doInJDBC(bobConnection -> { - prepareConnection(bobConnection); - try { - assertEquals(1, update(bobConnection, updatePostTitleSql())); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - }); - }); - } catch (Exception e) { - if ( !ExceptionUtil.isConnectionClose( e ) ) { - fail(e.getMessage()); - } - } - String secondTitle = selectStringColumn(aliceConnection, selectPostTitleSql()); - - LOGGER.info("Isolation level {} {} Non-Repeatable Read", isolationLevelName, !firstTitle.equals(secondTitle) ? "allows" : "prevents"); - if(preventedByLocking.get()) { - LOGGER.info("Isolation level {} prevents Non-Repeatable Read by locking", isolationLevelName); - } - }); - } - - @Test - public void testPhantomRead() { - final AtomicBoolean preventedByLocking = new AtomicBoolean(); - - doInJDBC(aliceConnection -> { - if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { - LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); - return; - } - prepareConnection(aliceConnection); - int commentsCount = count(aliceConnection, countCommentsSql()); - assertEquals(3, commentsCount); - update(aliceConnection, updateCommentsSql()); - try { - executeSync(() -> { - doInJDBC(bobConnection -> { - prepareConnection(bobConnection); - try { - assertEquals(1, update(bobConnection, insertCommentSql())); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - }); - }); - } catch (Exception e) { - if ( !ExceptionUtil.isConnectionClose( e ) ) { - fail(e.getMessage()); - } - } - int secondCommentsCount = count(aliceConnection, countCommentsSql()); - - LOGGER.info("Isolation level {} {} Phantom Reads", isolationLevelName, secondCommentsCount != commentsCount ? "allows" : "prevents"); - if(preventedByLocking.get()) { - LOGGER.info("Isolation level {} prevents Phantom Read by locking", isolationLevelName); - } - }); - } - - @Test - public void testLostUpdate() { - final AtomicBoolean preventedByLocking = new AtomicBoolean(); - final AtomicBoolean preventedByMVCC = new AtomicBoolean(); - try { - doInJDBC(aliceConnection -> { - if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { - LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); - return; - } - prepareConnection(aliceConnection); - String title = selectStringColumn(aliceConnection, selectPostTitleSql()); - executeSync(() -> { - doInJDBC(bobConnection -> { - prepareConnection(bobConnection); - try { - update(bobConnection, updatePostTitleParamSql(), new Object[]{"Bob"}); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - }); - }); - update(aliceConnection, updatePostTitleParamSql(), new Object[]{"Alice"}); - }); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else if ( !ExceptionUtil.isConnectionClose( e ) ) { - throw new IllegalStateException( e ); - } - } - doInJDBC(aliceConnection -> { - String title = selectStringColumn(aliceConnection, selectPostTitleSql()); - LOGGER.info("Isolation level {} {} Lost Update", isolationLevelName, "Alice".equals(title) ? "allows" : "prevents"); - - if (Boolean.TRUE.equals(preventedByLocking.get())) { - LOGGER.info("Isolation level {} prevents Lost Update by locking", isolationLevelName); - } else if (Boolean.TRUE.equals(preventedByMVCC.get())) { - LOGGER.info("Isolation level {} prevents Lost Update by MVCC", isolationLevelName); - } - }); - } - - @Test - public void testReadSkew() { - final AtomicBoolean preventedByLocking = new AtomicBoolean(); - final AtomicBoolean preventedByMVCC = new AtomicBoolean(); - try { - doInJDBC(aliceConnection -> { - if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { - LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); - return; - } - prepareConnection(aliceConnection); - String title = selectStringColumn(aliceConnection, selectPostTitleSql()); - - executeSync(() -> { - doInJDBC(bobConnection -> { - prepareConnection(bobConnection); - try { - update(bobConnection, updatePostTitleParamSql(), new Object[]{"Bob"}); - update(bobConnection, updatePostDetailsAuthorParamSql(), new Object[]{"Bob"}); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - }); - }); - String createdBy = selectStringColumn(aliceConnection, selectPostDetailsAuthorSql()); - LOGGER.info("Isolation level {} {} Read Skew", isolationLevelName, "Bob".equals(createdBy) ? "allows" : "prevents"); - }); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else if ( !ExceptionUtil.isConnectionClose( e ) ) { - throw new IllegalStateException( e ); - } - } - doInJDBC(aliceConnection -> { - String title = selectStringColumn(aliceConnection, selectPostTitleSql()); - - if (Boolean.TRUE.equals(preventedByLocking.get())) { - LOGGER.info("Isolation level {} prevents Read Skew by locking", isolationLevelName); - } else if (Boolean.TRUE.equals(preventedByMVCC.get())) { - LOGGER.info("Isolation level {} prevents Read Skew by MVCC", isolationLevelName); - } - }); - } - - @Test - public void testWriteSkew() { - final AtomicBoolean preventedByLocking = new AtomicBoolean(); - final AtomicBoolean preventedByMVCC = new AtomicBoolean(); - try { - doInJDBC(aliceConnection -> { - if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { - LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); - return; - } - prepareConnection(aliceConnection); - String title = selectStringColumn(aliceConnection, selectPostTitleSql()); - String createdBy = selectStringColumn(aliceConnection, selectPostDetailsAuthorSql()); - - executeSync(() -> { - doInJDBC(bobConnection -> { - prepareConnection(bobConnection); - try { - String bobTitle = selectStringColumn(bobConnection, selectPostTitleSql()); - String bonCreatedBy = selectStringColumn(bobConnection, selectPostDetailsAuthorSql()); - update(bobConnection, updatePostTitleParamSql(), new Object[]{"Bob"}); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - }); - }); - update(aliceConnection, updatePostDetailsAuthorParamSql(), new Object[]{"Alice"}); - }); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - if (Boolean.TRUE.equals(preventedByLocking.get())) { - LOGGER.info("Isolation level {} prevents Write Skew by locking", isolationLevelName); - } else if (Boolean.TRUE.equals(preventedByMVCC.get())) { - LOGGER.info("Isolation level {} prevents Write Skew by MVCC", isolationLevelName); - } else { - LOGGER.info("Isolation level {} allows Write Skew", isolationLevelName); - } - } - - protected void prepareConnection(Connection connection) throws SQLException { - connection.setTransactionIsolation(isolationLevel); - setJdbcTimeout(connection); - } - - protected String selectPostTitleSql() { - return "SELECT title FROM post WHERE id = 1"; - } - - protected String selectPostDetailsAuthorSql() { - return "SELECT created_by FROM post_details WHERE id = 1"; - } - - protected String updatePostTitleSql() { - return "UPDATE post SET title = 'ACID' WHERE id = 1"; - } - - protected String updatePostTitleParamSql() { - return "UPDATE post SET title = ? WHERE id = 1"; - } - - protected String updatePostDetailsAuthorParamSql() { - return "UPDATE post_details SET created_by = ? WHERE id = 1"; - } - - protected String countCommentsSql() { - return "SELECT COUNT(*) FROM post_comment where post_id = 1"; - } - - protected String updateCommentsSql() { - return "UPDATE post_comment SET version = 100 WHERE post_id = 1"; - } - - int nextId = 100; - - protected String insertCommentSql() { - return String.format("INSERT INTO post_comment (post_id, review, version, id) VALUES (1, 'Phantom', 0, %d)", nextId++); - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/CockroachDBPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/CockroachDBPhenomenaTest.java deleted file mode 100644 index ad3b4c02f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/CockroachDBPhenomenaTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.phenomena; - -import java.sql.Connection; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import org.junit.runners.Parameterized; - -import com.vladmihalcea.book.hpjp.jdbc.transaction.phenomena.AbstractPhenomenaTest; -import com.vladmihalcea.book.hpjp.util.providers.CockroachDBDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; - -/** - * @author Vlad Mihalcea - */ -public class CockroachDBPhenomenaTest extends AbstractPhenomenaTest { - - public CockroachDBPhenomenaTest(String isolationLevelName, int isolationLevel) { - super(isolationLevelName, isolationLevel); - } - - @Parameterized.Parameters - public static Collection isolationLevels() { - List levels = new ArrayList<>(); - levels.add(new Object[]{"Serializable", Connection.TRANSACTION_SERIALIZABLE}); - return levels; - } - - @Override - protected DataSourceProvider dataSourceProvider() { - return new CockroachDBDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/MySQLPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/MySQLPhenomenaTest.java deleted file mode 100644 index 2cd3cc319..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/MySQLPhenomenaTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.phenomena; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; - -/** - * MySQLPhenomenaTest - Test to validate MySQL phenomena - * - * @author Vlad Mihalcea - */ -public class MySQLPhenomenaTest extends AbstractPhenomenaTest { - - public MySQLPhenomenaTest(String isolationLevelName, int isolationLevel) { - super(isolationLevelName, isolationLevel); - } - - @Override - protected DataSourceProvider dataSourceProvider() { - return new MySQLDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/PostgreSQLPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/PostgreSQLPhenomenaTest.java deleted file mode 100644 index 4e6d18cbc..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/PostgreSQLPhenomenaTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.phenomena; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; - -/** - * PostgreSQLPhenomenaTest - Test to validate PostgreSQL phenomena - * - * @author Vlad Mihalcea - */ -public class PostgreSQLPhenomenaTest extends AbstractPhenomenaTest { - - public PostgreSQLPhenomenaTest(String isolationLevelName, int isolationLevel) { - super(isolationLevelName, isolationLevel); - } - - @Override - protected DataSourceProvider dataSourceProvider() { - return new PostgreSQLDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/linearizabilty/AbstractLinearizabilityPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/linearizabilty/AbstractLinearizabilityPhenomenaTest.java deleted file mode 100644 index f810b1cb7..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/linearizabilty/AbstractLinearizabilityPhenomenaTest.java +++ /dev/null @@ -1,488 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.phenomena.linearizabilty; - -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.Index; -import javax.persistence.ManyToOne; -import javax.persistence.Table; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import com.vladmihalcea.book.hpjp.jdbc.transaction.phenomena.AbstractPhenomenaTest; -import com.vladmihalcea.book.hpjp.util.exception.ExceptionUtil; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * PhenomenaTest - Test to validate what phenomena does a certain isolation level prevents - * - * @author Vlad Mihalcea - */ -@RunWith(Parameterized.class) -public abstract class AbstractLinearizabilityPhenomenaTest extends AbstractPhenomenaTest { - - protected AbstractLinearizabilityPhenomenaTest(String isolationLevelName, int isolationLevel) { - super(isolationLevelName, isolationLevel); - } - - @Override - protected Class[] entities() { - List> classes = new ArrayList<>(Arrays.asList(super.entities())); - classes.add(Department.class); - classes.add(Employee.class); - return classes.toArray(new Class[]{}); - } - - protected String sumEmployeeSalarySql() { - return "SELECT SUM(salary) FROM employee where department_id = 1"; - } - - protected String allEmployeeSalarySql() { - return "SELECT salary FROM employee where department_id = 1"; - } - - protected String insertEmployeeSql() { - return INSERT_EMPLOYEE; - } - - protected String updateEmployeeSalarySql() { - return "UPDATE employee SET salary = salary * 1.1 WHERE department_id = 1"; - } - - @Override - public void init() { - super.init(); - doInJDBC(connection -> { - try ( - PreparedStatement departmentStatement = connection.prepareStatement(INSERT_DEPARTMENT); - PreparedStatement employeeStatement = connection.prepareStatement(INSERT_EMPLOYEE); - ) { - int index = 0; - departmentStatement.setString(++index, "Hypersistence"); - departmentStatement.setLong(++index, 100_000); - departmentStatement.setLong(++index, 1); - departmentStatement.executeUpdate(); - - for (int i = 0; i < 3; i++) { - index = 0; - employeeStatement.setLong(++index, 1); - employeeStatement.setString(++index, String.format("John Doe %1$d", i)); - employeeStatement.setLong(++index, 30_000); - employeeStatement.setLong(++index, i); - employeeStatement.executeUpdate(); - } - } catch (SQLException e) { - fail(e.getMessage()); - } - }); - } - - @Test - public void testPhantomReadAggregate() { - final AtomicBoolean preventedByLocking = new AtomicBoolean(); - final AtomicBoolean preventedByMVCC = new AtomicBoolean(); - - try { - doInJDBC(aliceConnection -> { - if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { - LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); - return; - } - prepareConnection(aliceConnection); - long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class).longValue(); - assertEquals(90_000, salaryCount); - - try { - executeSync(() -> { - doInJDBC(bobConnection -> { - prepareConnection(bobConnection); - try { - long _salaryCount = selectColumn(bobConnection, sumEmployeeSalarySql(), Number.class).longValue(); - assertEquals(90_000, _salaryCount); - - try ( - PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); - ) { - int employeeId = 4; - int index = 0; - employeeStatement.setLong(++index, 1); - employeeStatement.setString(++index, "Carol"); - employeeStatement.setLong(++index, 9_000); - employeeStatement.setLong(++index, employeeId); - employeeStatement.executeUpdate(); - } - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - }); - }); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - update(aliceConnection, "UPDATE employee SET salary = salary * 1.1 WHERE department_id = 1"); - }); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - doInJDBC(aliceConnection -> { - long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class).longValue(); - if(99_000 != salaryCount) { - LOGGER.info("Isolation level {} allows Phantom Read since the salary count is {} instead of 99000", isolationLevelName, salaryCount); - } - else { - LOGGER.info("Isolation level {} prevents Phantom Read due to {}", isolationLevelName, preventedByLocking.get() ? "locking" : preventedByMVCC.get() ? "MVCC" : "unknown"); - } - }); - } - - @Test - public void testPhantomReadAggregateWithInsert() { - final AtomicBoolean preventedByLocking = new AtomicBoolean(); - final AtomicBoolean preventedByMVCC = new AtomicBoolean(); - - try { - doInJDBC(aliceConnection -> { - if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { - LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); - return; - } - prepareConnection(aliceConnection); - long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class).longValue(); - assertEquals(90_000, salaryCount); - - try { - executeSync(() -> { - doInJDBC(bobConnection -> { - prepareConnection(bobConnection); - try { - long _salaryCount = selectColumn(bobConnection, sumEmployeeSalarySql(), Number.class).longValue(); - assertEquals(90_000, _salaryCount); - - try ( - PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); - ) { - int employeeId = 4; - int index = 0; - employeeStatement.setLong(++index, 1); - employeeStatement.setString(++index, "Carol"); - employeeStatement.setLong(++index, 9_000); - employeeStatement.setLong(++index, employeeId); - employeeStatement.executeUpdate(); - } - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - }); - }); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - try ( - PreparedStatement employeeStatement = aliceConnection.prepareStatement(insertEmployeeSql()); - ) { - int employeeId = 5; - int index = 0; - employeeStatement.setLong(++index, 1); - employeeStatement.setString(++index, "Dave"); - employeeStatement.setLong(++index, 9_000); - employeeStatement.setLong(++index, employeeId); - employeeStatement.executeUpdate(); - } - }); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - doInJDBC(aliceConnection -> { - long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class).longValue(); - if(99_000 != salaryCount) { - LOGGER.info("Isolation level {} allows Phantom Read since the salary count is {} instead of 99000", isolationLevelName, salaryCount); - } - else { - LOGGER.info("Isolation level {} prevents Phantom Read due to {}", isolationLevelName, preventedByLocking.get() ? "locking" : preventedByMVCC.get() ? "MVCC" : "unknown"); - } - }); - } - - @Test - public void testPhantomWriteSelectColumn() { - final AtomicBoolean preventedByLocking = new AtomicBoolean(); - final AtomicBoolean preventedByMVCC = new AtomicBoolean(); - - try { - doInJDBC(aliceConnection -> { - if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { - LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); - return; - } - prepareConnection(aliceConnection); - - List salaries = selectColumnList(aliceConnection, allEmployeeSalarySql(), Number.class); - assertEquals(90_000, salaries.stream().mapToInt(Number::intValue).sum()); - - try { - executeSync(() -> { - doInJDBC(bobConnection -> { - prepareConnection(bobConnection); - try { - List _salaries = selectColumnList(bobConnection, allEmployeeSalarySql(), Number.class); - assertEquals(90_000, _salaries.stream().mapToInt(Number::intValue).sum()); - - try ( - PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); - ) { - int employeeId = 4; - int index = 0; - employeeStatement.setLong(++index, 1); - employeeStatement.setString(++index, "Carol"); - employeeStatement.setLong(++index, 9_000); - employeeStatement.setLong(++index, employeeId); - employeeStatement.executeUpdate(); - } - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - }); - }); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - update(aliceConnection, updateEmployeeSalarySql()); - }); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - doInJDBC(aliceConnection -> { - long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class).longValue(); - if(99_000 != salaryCount) { - LOGGER.info("Isolation level {} allows Phantom Read since the salary count is {} instead of 99000", isolationLevelName, salaryCount); - } - else { - LOGGER.info("Isolation level {} prevents Phantom Read due to {}", isolationLevelName, preventedByLocking.get() ? "locking" : preventedByMVCC.get() ? "MVCC" : "unknown"); - } - }); - } - - @Test - public void testPhantomWriteSelectColumnInOneTx() { - final AtomicBoolean preventedByLocking = new AtomicBoolean(); - final AtomicBoolean preventedByMVCC = new AtomicBoolean(); - - try { - doInJDBC(aliceConnection -> { - if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { - LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); - return; - } - prepareConnection(aliceConnection); - - List salaries = selectColumnList(aliceConnection, allEmployeeSalarySql(), Number.class); - assertEquals(90_000, salaries.stream().mapToInt(Number::intValue).sum()); - - try { - executeSync(() -> { - doInJDBC(bobConnection -> { - prepareConnection(bobConnection); - try { - try ( - PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); - ) { - int employeeId = 4; - int index = 0; - employeeStatement.setLong(++index, 1); - employeeStatement.setString(++index, "Carol"); - employeeStatement.setLong(++index, 9_000); - employeeStatement.setLong(++index, employeeId); - employeeStatement.executeUpdate(); - } - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - }); - }); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - update(aliceConnection, updateEmployeeSalarySql()); - }); - } catch (Exception e) { - if( ExceptionUtil.isLockTimeout( e )) { - preventedByLocking.set( true ); - } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { - preventedByMVCC.set( true ); - } else { - throw new IllegalStateException( e ); - } - } - doInJDBC(aliceConnection -> { - long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class).longValue(); - if(99_000 != salaryCount) { - LOGGER.info("Isolation level {} allows Phantom Read since the salary count is {} instead of 99000", isolationLevelName, salaryCount); - } - else { - LOGGER.info("Isolation level {} prevents Phantom Read due to {}", isolationLevelName, preventedByLocking.get() ? "locking" : preventedByMVCC.get() ? "MVCC" : "unknown"); - } - }); - } - - @Entity(name = "Department") - @Table(name = "department") - public static class Department { - - @Id - private Long id; - - private String name; - - private long budget; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public long getBudget() { - return budget; - } - - public void setBudget(long budget) { - this.budget = budget; - } - } - - @Entity(name = "Employee") - @Table(name = "employee", indexes = @Index(name = "IDX_Employee", columnList = "department_id")) - public static class Employee { - - @Id - private Long id; - - @Column(name = "name") - private String name; - - @ManyToOne(fetch = FetchType.LAZY) - private Department department; - - private long salary; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Department getDepartment() { - return department; - } - - public void setDepartment(Department department) { - this.department = department; - } - - public long getSalary() { - return salary; - } - - public void setSalary(long salary) { - this.salary = salary; - } - } - -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/linearizabilty/OracleLinearizabilityPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/linearizabilty/OracleLinearizabilityPhenomenaTest.java deleted file mode 100644 index 65f9bde4c..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/linearizabilty/OracleLinearizabilityPhenomenaTest.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.phenomena.linearizabilty; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Test; -import org.junit.runners.Parameterized; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; - -import static org.junit.Assert.assertEquals; - -/** - * OraclePhenomenaTest - Test to validate Oracle phenomena - * - * @author Vlad Mihalcea - */ -public class OracleLinearizabilityPhenomenaTest extends AbstractLinearizabilityPhenomenaTest { - - public OracleLinearizabilityPhenomenaTest(String isolationLevelName, int isolationLevel) { - super(isolationLevelName, isolationLevel); - } - - @Parameterized.Parameters - public static Collection isolationLevels() { - List levels = new ArrayList<>(); - levels.add(new Object[]{"Read Committed", Connection.TRANSACTION_READ_COMMITTED}); - levels.add(new Object[]{"Serializable", Connection.TRANSACTION_SERIALIZABLE}); - return levels; - } - - @Override - protected DataSourceProvider dataSourceProvider() { - return new OracleDataSourceProvider(); - } - - @Override - public void init() { - super.init(); - doInJDBC(aliceConnection -> { - executeStatement(aliceConnection, "alter table employee initrans 100"); - }); - } - - @Test - public void testPhantomWriteAggregateNTimes() { - if (isolationLevel != Connection.TRANSACTION_SERIALIZABLE) { - return; - } - - int sleepMillis = 100; - - AtomicInteger ok = new AtomicInteger(); - AtomicInteger fail = new AtomicInteger(); - for (int i = 0; i < 10; i++) { - AtomicReference preventedByLocking = new AtomicReference<>(); - - doInJDBC(aliceConnection -> { - executeStatement(aliceConnection, "delete from employee where id = 4"); - executeStatement(aliceConnection, "update employee set salary = 30000"); - }); - - try { - doInJDBC(aliceConnection -> { - if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { - LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); - return; - } - prepareConnection(aliceConnection); - long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class).longValue(); - assertEquals(90_000, salaryCount); - - try { - executeSync(() -> { - doInJDBC(bobConnection -> { - prepareConnection(bobConnection); - try { - long _salaryCount = selectColumn(bobConnection, sumEmployeeSalarySql(), Number.class).longValue(); - assertEquals(90_000, _salaryCount); - - try ( - PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); - ) { - int employeeId = 4; - int index = 0; - employeeStatement.setLong(++index, 1); - employeeStatement.setString(++index, "Carol"); - employeeStatement.setLong(++index, 9_000); - employeeStatement.setLong(++index, employeeId); - employeeStatement.executeUpdate(); - } - } catch (Exception e) { - LOGGER.info("Exception thrown", e); - preventedByLocking.set(true); - } - }); - }); - } catch (Exception e) { - LOGGER.info("Exception thrown", e); - preventedByLocking.set(true); - } - sleep(sleepMillis); - update(aliceConnection, "UPDATE employee SET salary = salary * 1.1 WHERE department_id = 1 and id < 4"); - }); - } catch (Exception e) { - LOGGER.info("Exception thrown", e); - preventedByLocking.set(true); - } - doInJDBC(aliceConnection -> { - long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class).longValue(); - if(99_000 != salaryCount) { - LOGGER.info("Isolation level {} allows Phantom Write since the salary count is {} instead of 99000", isolationLevelName, salaryCount); - fail.incrementAndGet(); - } - else { - LOGGER.info("Isolation level {} prevents Phantom Write {}", isolationLevelName, preventedByLocking.get() ? "due to locking" : ""); - ok.incrementAndGet(); - } - }); - LOGGER.info("Success: {}, fail: {}", ok.get(), fail.get()); - } - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractCockroachDBIntegrationTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractCockroachDBIntegrationTest.java deleted file mode 100644 index a5c5a0c45..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractCockroachDBIntegrationTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.vladmihalcea.book.hpjp.util; - -import com.vladmihalcea.book.hpjp.util.providers.CockroachDBDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; - -/** - * AbstractCockroachDBIntegrationTest - Abstract CockroachDB IntegrationTest - * - * @author Vlad Mihalcea - */ -public abstract class AbstractCockroachDBIntegrationTest extends AbstractTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new CockroachDBDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractMySQLIntegrationTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractMySQLIntegrationTest.java deleted file mode 100644 index 68daa2c6b..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractMySQLIntegrationTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.vladmihalcea.book.hpjp.util; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; - -/** - * AbstractMySQLIntegrationTest - Abstract MySQL IntegrationTest - * - * @author Vlad Mihalcea - */ -public abstract class AbstractMySQLIntegrationTest extends AbstractTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new MySQLDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractOracleXEIntegrationTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractOracleXEIntegrationTest.java deleted file mode 100644 index 0ca04a66f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractOracleXEIntegrationTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.vladmihalcea.book.hpjp.util; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; - -/** - * AbstractOracleXEIntegrationTest - Abstract Orcale XE IntegrationTest - * - * @author Vlad Mihalcea - */ -public abstract class AbstractOracleXEIntegrationTest extends AbstractTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new OracleDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractPostgreSQLIntegrationTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractPostgreSQLIntegrationTest.java deleted file mode 100644 index 819ec123c..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractPostgreSQLIntegrationTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.vladmihalcea.book.hpjp.util; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; - -/** - * AbstractPostgreSQLIntegrationTest - Abstract PostgreSQL IntegrationTest - * - * @author Vlad Mihalcea - */ -public abstract class AbstractPostgreSQLIntegrationTest extends AbstractTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new PostgreSQLDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractSQLServerIntegrationTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractSQLServerIntegrationTest.java deleted file mode 100644 index e60bed5bd..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractSQLServerIntegrationTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.vladmihalcea.book.hpjp.util; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; - -/** - * AbstractSQLServerIntegrationTest - Abstract SQL Server IntegrationTest - * - * @author Vlad Mihalcea - */ -public abstract class AbstractSQLServerIntegrationTest extends AbstractTest { - - @Override - protected DataSourceProvider dataSourceProvider() { - return new SQLServerDataSourceProvider(); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractTest.java deleted file mode 100644 index 90fd25815..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/AbstractTest.java +++ /dev/null @@ -1,822 +0,0 @@ -package com.vladmihalcea.book.hpjp.util; - -import java.io.Closeable; -import java.io.IOException; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import javax.persistence.EntityManager; -import javax.persistence.EntityManagerFactory; -import javax.persistence.EntityTransaction; -import javax.persistence.spi.PersistenceUnitInfo; -import javax.sql.DataSource; - -import org.hibernate.Interceptor; -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.hibernate.Transaction; -import org.hibernate.boot.MetadataBuilder; -import org.hibernate.boot.MetadataSources; -import org.hibernate.boot.SessionFactoryBuilder; -import org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl; -import org.hibernate.boot.registry.BootstrapServiceRegistry; -import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; -import org.hibernate.boot.registry.StandardServiceRegistry; -import org.hibernate.boot.registry.StandardServiceRegistryBuilder; -import org.hibernate.boot.spi.MetadataImplementor; -import org.hibernate.cfg.AvailableSettings; -import org.hibernate.cfg.Configuration; -import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.H2Dialect; -import org.hibernate.dialect.MySQLDialect; -import org.hibernate.dialect.PostgreSQL81Dialect; -import org.hibernate.integrator.spi.Integrator; -import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; -import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor; -import org.hibernate.jpa.boot.spi.IntegratorProvider; -import org.hibernate.stat.SecondLevelCacheStatistics; -import org.hibernate.type.BasicType; -import org.hibernate.type.Type; -import org.hibernate.usertype.CompositeUserType; -import org.hibernate.usertype.UserType; - -import org.junit.After; -import org.junit.Before; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.vladmihalcea.book.hpjp.util.exception.DataAccessException; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.HsqldbDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.LockType; -import com.vladmihalcea.book.hpjp.util.transaction.ConnectionCallable; -import com.vladmihalcea.book.hpjp.util.transaction.ConnectionVoidCallable; -import com.vladmihalcea.book.hpjp.util.transaction.HibernateTransactionConsumer; -import com.vladmihalcea.book.hpjp.util.transaction.HibernateTransactionFunction; -import com.vladmihalcea.book.hpjp.util.transaction.JPATransactionFunction; -import com.vladmihalcea.book.hpjp.util.transaction.JPATransactionVoidFunction; -import com.vladmihalcea.book.hpjp.util.transaction.VoidCallable; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; - -import static org.junit.Assert.fail; - -public abstract class AbstractTest { - - static { - Thread.currentThread().setName("Alice"); - } - - protected final ExecutorService executorService = Executors.newSingleThreadExecutor(r -> { - Thread bob = new Thread(r); - bob.setName("Bob"); - return bob; - }); - - protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); - - private EntityManagerFactory emf; - - private SessionFactory sf; - - private List closeables = new ArrayList<>(); - - @Before - public void init() { - if(nativeHibernateSessionFactoryBootstrap()) { - sf = newSessionFactory(); - } else { - emf = newEntityManagerFactory(); - } - } - - @After - public void destroy() { - if(nativeHibernateSessionFactoryBootstrap()) { - sf.close(); - } else { - emf.close(); - } - for(Closeable closeable : closeables) { - try { - closeable.close(); - } catch (IOException e) { - LOGGER.error("Failure", e); - } - } - closeables.clear(); - } - - public EntityManagerFactory entityManagerFactory() { - return nativeHibernateSessionFactoryBootstrap() ? sf : emf; - } - - public SessionFactory sessionFactory() { - return nativeHibernateSessionFactoryBootstrap() ? sf : entityManagerFactory().unwrap(SessionFactory.class); - } - protected boolean nativeHibernateSessionFactoryBootstrap() { - return false; - } - - protected abstract Class[] entities(); - - protected List entityClassNames() { - return Arrays.asList(entities()).stream().map(Class::getName).collect(Collectors.toList()); - } - - protected String[] packages() { - return null; - } - - protected String[] resources() { - return null; - } - - protected Interceptor interceptor() { - return null; - } - - private SessionFactory newSessionFactory() { - final BootstrapServiceRegistryBuilder bsrb = new BootstrapServiceRegistryBuilder() - .enableAutoClose(); - - Integrator integrator = integrator(); - if (integrator != null) { - bsrb.applyIntegrator( integrator ); - } - - final BootstrapServiceRegistry bsr = bsrb.build(); - - final StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder(bsr) - .applySettings(properties()) - .build(); - - final MetadataSources metadataSources = new MetadataSources(serviceRegistry); - - for (Class annotatedClass : entities()) { - metadataSources.addAnnotatedClass(annotatedClass); - } - - String[] packages = packages(); - if (packages != null) { - for (String annotatedPackage : packages) { - metadataSources.addPackage(annotatedPackage); - } - } - - String[] resources = resources(); - if (resources != null) { - for (String resource : resources) { - metadataSources.addResource(resource); - } - } - - final MetadataBuilder metadataBuilder = metadataSources.getMetadataBuilder(); - metadataBuilder.enableNewIdentifierGeneratorSupport(true); - metadataBuilder.applyImplicitNamingStrategy(ImplicitNamingStrategyLegacyJpaImpl.INSTANCE); - - MetadataImplementor metadata = (MetadataImplementor) metadataBuilder.build(); - - final SessionFactoryBuilder sfb = metadata.getSessionFactoryBuilder(); - Interceptor interceptor = interceptor(); - if(interceptor != null) { - sfb.applyInterceptor(interceptor); - } - - return sfb.build(); - } - - private SessionFactory newLegacySessionFactory() { - Properties properties = properties(); - Configuration configuration = new Configuration().addProperties(properties); - for(Class entityClass : entities()) { - configuration.addAnnotatedClass(entityClass); - } - String[] packages = packages(); - if(packages != null) { - for(String scannedPackage : packages) { - configuration.addPackage(scannedPackage); - } - } - String[] resources = resources(); - if (resources != null) { - for (String resource : resources) { - configuration.addResource(resource); - } - } - Interceptor interceptor = interceptor(); - if(interceptor != null) { - configuration.setInterceptor(interceptor); - } - - final List additionalTypes = additionalTypes(); - if (additionalTypes != null) { - configuration.registerTypeContributor((typeContributions, serviceRegistry) -> { - additionalTypes.stream().forEach(type -> { - if(type instanceof BasicType) { - typeContributions.contributeType((BasicType) type); - } else if (type instanceof UserType ){ - typeContributions.contributeType((UserType) type); - } else if (type instanceof CompositeUserType) { - typeContributions.contributeType((CompositeUserType) type); - } - }); - }); - } - return configuration.buildSessionFactory( - new StandardServiceRegistryBuilder() - .applySettings(properties) - .build() - ); - } - - protected EntityManagerFactory newEntityManagerFactory() { - PersistenceUnitInfo persistenceUnitInfo = persistenceUnitInfo(getClass().getSimpleName()); - Map configuration = new HashMap<>(); - configuration.put(AvailableSettings.INTERCEPTOR, interceptor()); - Integrator integrator = integrator(); - if (integrator != null) { - configuration.put("hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList(integrator)); - } - - EntityManagerFactoryBuilderImpl entityManagerFactoryBuilder = new EntityManagerFactoryBuilderImpl( - new PersistenceUnitInfoDescriptor(persistenceUnitInfo), configuration - ); - return entityManagerFactoryBuilder.build(); - } - - protected Integrator integrator() { - return null; - } - - protected PersistenceUnitInfoImpl persistenceUnitInfo(String name) { - PersistenceUnitInfoImpl persistenceUnitInfo = new PersistenceUnitInfoImpl( - name, entityClassNames(), properties() - ); - String[] resources = resources(); - if (resources != null) { - persistenceUnitInfo.getMappingFileNames().addAll(Arrays.asList(resources)); - } - return persistenceUnitInfo; - } - - protected Properties properties() { - Properties properties = new Properties(); - properties.put("hibernate.dialect", dataSourceProvider().hibernateDialect()); - //log settings - properties.put("hibernate.hbm2ddl.auto", "create-drop"); - //data source settings - DataSource dataSource = newDataSource(); - if (dataSource != null) { - properties.put("hibernate.connection.datasource", dataSource); - } - //properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); - //properties.put("hibernate.ejb.metamodel.population", "disabled"); - return properties; - } - - protected DataSourceProxyType dataSourceProxyType() { - return DataSourceProxyType.DATA_SOURCE_PROXY; - } - - protected DataSource newDataSource() { - DataSource dataSource = - proxyDataSource() - ? dataSourceProxyType().dataSource(dataSourceProvider().dataSource()) - : dataSourceProvider().dataSource(); - if(connectionPooling()) { - HikariDataSource poolingDataSource = connectionPoolDataSource(dataSource); - closeables.add(poolingDataSource::close); - return poolingDataSource; - } else { - return dataSource; - } - } - - protected boolean proxyDataSource() { - return true; - } - - protected HikariDataSource connectionPoolDataSource(DataSource dataSource) { - return new HikariDataSource(hikariConfig(dataSource)); - } - - protected HikariConfig hikariConfig(DataSource dataSource) { - HikariConfig hikariConfig = new HikariConfig(); - int cpuCores = Runtime.getRuntime().availableProcessors(); - hikariConfig.setMaximumPoolSize(cpuCores * 4); - hikariConfig.setDataSource(dataSource); - return hikariConfig; - } - - protected boolean connectionPooling() { - return false; - } - - protected DataSourceProvider dataSourceProvider() { - return new HsqldbDataSourceProvider(); - } - - protected List additionalTypes() { - return null; - } - - protected T doInHibernate(HibernateTransactionFunction callable) { - T result = null; - Session session = null; - Transaction txn = null; - try { - session = sessionFactory().openSession(); - callable.beforeTransactionCompletion(); - txn = session.beginTransaction(); - - result = callable.apply(session); - if ( !txn.getRollbackOnly() ) { - txn.commit(); - } - else { - try { - txn.rollback(); - } - catch (Exception e) { - LOGGER.error( "Rollback failure", e ); - } - } - } catch (Throwable t) { - if ( txn != null && txn.isActive() ) { - try { - txn.rollback(); - } - catch (Exception e) { - LOGGER.error( "Rollback failure", e ); - } - } - throw t; - } finally { - callable.afterTransactionCompletion(); - if (session != null) { - session.close(); - } - } - return result; - } - - protected void doInHibernate(HibernateTransactionConsumer callable) { - Session session = null; - Transaction txn = null; - try { - session = sessionFactory().openSession(); - callable.beforeTransactionCompletion(); - txn = session.beginTransaction(); - - callable.accept(session); - if ( !txn.getRollbackOnly() ) { - txn.commit(); - } - else { - try { - txn.rollback(); - } - catch (Exception e) { - LOGGER.error( "Rollback failure", e ); - } - } - } catch (Throwable t) { - if ( txn != null && txn.isActive() ) { - try { - txn.rollback(); - } - catch (Exception e) { - LOGGER.error( "Rollback failure", e ); - } - } - throw t; - } finally { - callable.afterTransactionCompletion(); - if (session != null) { - session.close(); - } - } - } - - protected T doInJPA(JPATransactionFunction function) { - T result = null; - EntityManager entityManager = null; - EntityTransaction txn = null; - try { - entityManager = entityManagerFactory().createEntityManager(); - function.beforeTransactionCompletion(); - txn = entityManager.getTransaction(); - txn.begin(); - result = function.apply(entityManager); - if ( !txn.getRollbackOnly() ) { - txn.commit(); - } - else { - try { - txn.rollback(); - } - catch (Exception e) { - LOGGER.error( "Rollback failure", e ); - } - } - } catch (Throwable t) { - if ( txn != null && txn.isActive() ) { - try { - txn.rollback(); - } - catch (Exception e) { - LOGGER.error( "Rollback failure", e ); - } - } - throw t; - } finally { - function.afterTransactionCompletion(); - if (entityManager != null) { - entityManager.close(); - } - } - return result; - } - - protected void doInJPA(JPATransactionVoidFunction function) { - EntityManager entityManager = null; - EntityTransaction txn = null; - try { - entityManager = entityManagerFactory().createEntityManager(); - function.beforeTransactionCompletion(); - txn = entityManager.getTransaction(); - txn.begin(); - function.accept(entityManager); - if ( !txn.getRollbackOnly() ) { - txn.commit(); - } - else { - try { - txn.rollback(); - } - catch (Exception e) { - LOGGER.error( "Rollback failure", e ); - } - } - } catch (Throwable t) { - if ( txn != null && txn.isActive() ) { - try { - txn.rollback(); - } - catch (Exception e) { - LOGGER.error( "Rollback failure", e ); - } - } - throw t; - } finally { - function.afterTransactionCompletion(); - if (entityManager != null) { - entityManager.close(); - } - } - } - - protected T doInJDBC(ConnectionCallable callable) { - AtomicReference result = new AtomicReference<>(); - Session session = null; - Transaction txn = null; - try { - session = sessionFactory().openSession(); - txn = session.beginTransaction(); - session.doWork(connection -> { - result.set(callable.execute(connection)); - }); - if ( !txn.getRollbackOnly() ) { - txn.commit(); - } - else { - try { - txn.rollback(); - } - catch (Exception e) { - LOGGER.error( "Rollback failure", e ); - } - } - } catch (Throwable t) { - if ( txn != null && txn.isActive() ) { - try { - txn.rollback(); - } - catch (Exception e) { - LOGGER.error( "Rollback failure", e ); - } - } - throw t; - } finally { - if (session != null) { - session.close(); - } - } - return result.get(); - } - - protected void doInJDBC(ConnectionVoidCallable callable) { - Session session = null; - Transaction txn = null; - try { - session = sessionFactory().openSession(); - txn = session.beginTransaction(); - session.doWork(callable::execute); - if ( !txn.getRollbackOnly() ) { - txn.commit(); - } - else { - try { - txn.rollback(); - } - catch (Exception e) { - LOGGER.error( "Rollback failure", e ); - } - } - } catch (Throwable t) { - if ( txn != null && txn.isActive() ) { - try { - txn.rollback(); - } - catch (Exception e) { - LOGGER.error( "Rollback failure", e ); - } - } - throw t; - } finally { - if (session != null) { - session.close(); - } - } - } - - protected void executeSync(VoidCallable callable) { - executeSync(Collections.singleton(callable)); - } - - protected void executeSync(Collection callables) { - try { - List> futures = executorService.invokeAll(callables); - for (Future future : futures) { - future.get(); - } - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - } - - protected void executeAsync(Runnable callable, final Runnable completionCallback) { - final Future future = executorService.submit(callable); - new Thread(() -> { - while (!future.isDone()) { - try { - Thread.sleep(100); - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - try { - completionCallback.run(); - } catch (Exception e) { - throw new IllegalStateException(e); - } - }).start(); - } - - protected Future executeAsync(Runnable callable) { - return executorService.submit(callable); - } - - protected void transact(Consumer callback) { - transact(callback, null); - } - - protected void transact(Consumer callback, Consumer before) { - Connection connection = null; - try { - connection = newDataSource().getConnection(); - if (before != null) { - before.accept(connection); - } - connection.setAutoCommit(false); - callback.accept(connection); - connection.commit(); - } catch (Exception e) { - if (connection != null) { - try { - connection.rollback(); - } catch (SQLException ex) { - throw new DataAccessException( e); - } - } - throw (e instanceof DataAccessException ? - (DataAccessException) e : new DataAccessException(e)); - } finally { - if(connection != null) { - try { - connection.close(); - } catch (SQLException e) { - throw new DataAccessException(e); - } - } - } - } - - protected LockType lockType() { - return LockType.LOCKS; - } - - protected void awaitOnLatch(CountDownLatch latch) { - try { - latch.await(); - } catch (InterruptedException e) { - throw new IllegalStateException(e); - } - } - - protected void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - - protected V sleep(int millis, Callable callable) { - V result = null; - try { - if (callable != null) { - result = callable.call(); - } - Thread.sleep(millis); - } catch (Exception e) { - throw new IllegalStateException(e); - } - return result; - } - - protected void awaitTermination(long timeout, TimeUnit unit) { - try { - executorService.awaitTermination(1, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - protected String selectStringColumn(Connection connection, String sql) { - try { - try(Statement statement = connection.createStatement()) { - statement.setQueryTimeout(1); - ResultSet resultSet = statement.executeQuery(sql); - if(!resultSet.next()) { - throw new IllegalArgumentException("There was no row to be selected!"); - } - return resultSet.getString(1); - } - } catch (SQLException e) { - throw new IllegalStateException(e); - } - } - - protected T selectColumn(Connection connection, String sql, Class clazz) { - try { - try(Statement statement = connection.createStatement()) { - statement.setQueryTimeout(1); - ResultSet resultSet = statement.executeQuery(sql); - if(!resultSet.next()) { - throw new IllegalArgumentException("There was no row to be selected!"); - } - return clazz.cast(resultSet.getObject(1)); - } - } catch (SQLException e) { - throw new IllegalStateException(e); - } - } - - protected List selectColumnList(Connection connection, String sql, Class clazz) { - List result = new ArrayList<>(); - try { - try(Statement statement = connection.createStatement()) { - statement.setQueryTimeout(1); - ResultSet resultSet = statement.executeQuery(sql); - while (resultSet.next()) { - result.add(clazz.cast(resultSet.getObject(1))); - } - } - } catch (SQLException e) { - throw new IllegalStateException(e); - } - return result; - } - - protected int update(Connection connection, String sql) { - try { - try(Statement statement = connection.createStatement()) { - statement.setQueryTimeout(1); - return statement.executeUpdate(sql); - } - } catch (SQLException e) { - throw new IllegalStateException(e); - } - } - - protected void executeStatement(Connection connection, String sql) { - try { - try(Statement statement = connection.createStatement()) { - statement.setQueryTimeout(1); - statement.execute(sql); - } - } catch (SQLException e) { - throw new IllegalStateException(e); - } - } - - protected int update(Connection connection, String sql, Object[] params) { - try { - try(PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setQueryTimeout(1); - for (int i = 0; i < params.length; i++) { - statement.setObject(i + 1, params[i]); - } - return statement.executeUpdate(); - } - } catch (SQLException e) { - throw new IllegalStateException(e); - } - } - - protected int count(Connection connection, String sql) { - try { - try(Statement statement = connection.createStatement()) { - statement.setQueryTimeout(1); - ResultSet resultSet = statement.executeQuery(sql); - if(!resultSet.next()) { - throw new IllegalArgumentException("There was no row to be selected!"); - } - return ((Number) resultSet.getObject(1)).intValue(); - } - } catch (SQLException e) { - throw new IllegalStateException(e); - } - } - - /** - * Set JDBC Connection or Statement timeout - * - * @param connection JDBC Connection time out - */ - public void setJdbcTimeout(Connection connection) { - try (Statement st = connection.createStatement()) { - DataSourceProvider dataSourceProvider = dataSourceProvider(); - - switch ( dataSourceProvider.database() ) { - case POSTGRESQL: - st.execute( "SET statement_timeout TO 1000" ); - break; - case MYSQL: - st.execute( "SET SESSION innodb_lock_wait_timeout = 1" ); - break; - case SQLSERVER: - st.execute( "SET LOCK_TIMEOUT 1" ); - break; - default: - try { - connection.setNetworkTimeout( Executors.newSingleThreadExecutor(), 1000 ); - } - catch (Throwable ignore) { - ignore.fillInStackTrace(); - } - } - } - catch (SQLException e) { - fail(e.getMessage()); - } - } - - protected void printCacheRegionStatistics(String region) { - SecondLevelCacheStatistics statistics = sessionFactory().getStatistics().getSecondLevelCacheStatistics(region); - LOGGER.debug("\nRegion: {},\nStatistics: {},\nEntries: {}", region, statistics, statistics.getEntries()); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/CockroachDBDialect.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/CockroachDBDialect.java deleted file mode 100644 index 95eeace4b..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/CockroachDBDialect.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.vladmihalcea.book.hpjp.util; - -import java.sql.Types; - -import org.hibernate.dialect.PostgreSQL82Dialect; - -/** - * @author Vlad Mihalcea - */ -public class CockroachDBDialect extends PostgreSQL82Dialect { - - public CockroachDBDialect() { - super(); - registerColumnType( Types.SMALLINT, "smallint" ); - registerColumnType( Types.TINYINT, "smallint" ); - registerColumnType( Types.INTEGER, "integer" ); - - registerColumnType( Types.FLOAT, "double precision" ); - registerColumnType( Types.DOUBLE, "double precision" ); - - registerColumnType( Types.BLOB, "blob" ); - registerColumnType( Types.OTHER, "interval" ); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/DataSourceProviderIntegrationTest.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/DataSourceProviderIntegrationTest.java deleted file mode 100644 index a323cde57..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/DataSourceProviderIntegrationTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.vladmihalcea.book.hpjp.util; - -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; - -/** - * DataSourceProviderIntegrationTest - Test against some common RDBMS providers - * - * @author Vlad Mihalcea - */ -@RunWith(Parameterized.class) -public abstract class DataSourceProviderIntegrationTest extends AbstractTest { - - private final DataSourceProvider dataSourceProvider; - - public DataSourceProviderIntegrationTest(DataSourceProvider dataSourceProvider) { - this.dataSourceProvider = dataSourceProvider; - } - - @Parameterized.Parameters - public static Collection rdbmsDataSourceProvider() { - List providers = new ArrayList<>(); - providers.add(new DataSourceProvider[]{new OracleDataSourceProvider()}); - providers.add(new DataSourceProvider[]{new SQLServerDataSourceProvider()}); - providers.add(new DataSourceProvider[]{new PostgreSQLDataSourceProvider()}); - providers.add(new DataSourceProvider[]{new MySQLDataSourceProvider()}); - return providers; - } - - @Override - protected DataSourceProvider dataSourceProvider() { - return dataSourceProvider; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/DataSourceProxyType.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/DataSourceProxyType.java deleted file mode 100644 index f47e54de7..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/DataSourceProxyType.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.vladmihalcea.book.hpjp.util; - -import com.p6spy.engine.spy.P6DataSource; -import com.vladmihalcea.book.hpjp.util.logging.InlineQueryLogEntryCreator; -import net.ttddyy.dsproxy.listener.ChainListener; -import net.ttddyy.dsproxy.listener.DataSourceQueryCountListener; -import net.ttddyy.dsproxy.listener.SLF4JQueryLoggingListener; -import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; - -import javax.sql.DataSource; - -/** - * @author Vlad Mihalcea - */ -public enum DataSourceProxyType { - DATA_SOURCE_PROXY { - @Override - DataSource dataSource(DataSource dataSource) { - ChainListener listener = new ChainListener(); - SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); - loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); - listener.addListener(loggingListener); - listener.addListener(new DataSourceQueryCountListener()); - return ProxyDataSourceBuilder - .create(dataSource) - .name(name()) - .listener(listener) - .build(); - } - }, - P6SPY { - @Override - DataSource dataSource(DataSource dataSource) { - return new P6DataSource(dataSource); - } - }; - - abstract DataSource dataSource(DataSource dataSource); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/PersistenceUnitInfoImpl.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/PersistenceUnitInfoImpl.java deleted file mode 100644 index ffa625e76..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/PersistenceUnitInfoImpl.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.vladmihalcea.book.hpjp.util; - -import org.hibernate.jpa.HibernatePersistenceProvider; - -import javax.persistence.SharedCacheMode; -import javax.persistence.ValidationMode; -import javax.persistence.spi.ClassTransformer; -import javax.persistence.spi.PersistenceUnitInfo; -import javax.persistence.spi.PersistenceUnitTransactionType; -import javax.sql.DataSource; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Properties; - -/** - * @author Vlad Mihalcea - */ -public class PersistenceUnitInfoImpl implements PersistenceUnitInfo { - - private final String persistenceUnitName; - - private PersistenceUnitTransactionType transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL; - - private final List managedClassNames; - - private final List mappingFileNames = new ArrayList<>(); - - private final Properties properties; - - private DataSource jtaDataSource; - - private DataSource nonJtaDataSource; - - public PersistenceUnitInfoImpl(String persistenceUnitName, List managedClassNames, Properties properties) { - this.persistenceUnitName = persistenceUnitName; - this.managedClassNames = managedClassNames; - this.properties = properties; - } - - @Override - public String getPersistenceUnitName() { - return persistenceUnitName; - } - - @Override - public String getPersistenceProviderClassName() { - return HibernatePersistenceProvider.class.getName(); - } - - @Override - public PersistenceUnitTransactionType getTransactionType() { - return transactionType; - } - - @Override - public DataSource getJtaDataSource() { - return jtaDataSource; - } - - public PersistenceUnitInfoImpl setJtaDataSource(DataSource jtaDataSource) { - this.jtaDataSource = jtaDataSource; - this.nonJtaDataSource = null; - transactionType = PersistenceUnitTransactionType.JTA; - return this; - } - - @Override - public DataSource getNonJtaDataSource() { - return nonJtaDataSource; - } - - public PersistenceUnitInfoImpl setNonJtaDataSource(DataSource nonJtaDataSource) { - this.nonJtaDataSource = nonJtaDataSource; - this.jtaDataSource = null; - transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL; - return this; - } - - @Override - public List getMappingFileNames() { - return mappingFileNames; - } - - @Override - public List getJarFileUrls() { - return Collections.emptyList(); - } - - @Override - public URL getPersistenceUnitRootUrl() { - return null; - } - - @Override - public List getManagedClassNames() { - return managedClassNames; - } - - @Override - public boolean excludeUnlistedClasses() { - return false; - } - - @Override - public SharedCacheMode getSharedCacheMode() { - return SharedCacheMode.UNSPECIFIED; - } - - @Override - public ValidationMode getValidationMode() { - return ValidationMode.AUTO; - } - - public Properties getProperties() { - return properties; - } - - @Override - public String getPersistenceXMLSchemaVersion() { - return "2.1"; - } - - @Override - public ClassLoader getClassLoader() { - return Thread.currentThread().getContextClassLoader(); - } - - @Override - public void addTransformer(ClassTransformer transformer) { - - } - - @Override - public ClassLoader getNewTempClassLoader() { - return null; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/ReflectionUtils.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/ReflectionUtils.java deleted file mode 100644 index 30835f579..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/ReflectionUtils.java +++ /dev/null @@ -1,312 +0,0 @@ -package com.vladmihalcea.book.hpjp.util; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * ReflectionUtils - Reflection utilities holder. - * - * @author Vlad Mihalcea - * @since 1.0 - */ -public final class ReflectionUtils { - - private static final Logger LOGGER = LoggerFactory.getLogger(ReflectionUtils.class); - - public static final String GETTER_PREFIX = "get"; - public static final String SETTER_PREFIX = "set"; - - private ReflectionUtils() { - throw new UnsupportedOperationException("ReflectionUtils is not instantiable!"); - } - - /** - * Instantiate object - * - * @param className Class for object to instantiate - * @param field type - * @return field value - */ - public static T newInstance(String className) { - try { - Class clazz = Class.forName(className); - return (T) clazz.newInstance(); - } catch (ClassNotFoundException e) { - throw handleException(className, e); - } catch (InstantiationException e) { - throw handleException(className, e); - } catch (IllegalAccessException e) { - throw handleException(className, e); - } - } - - /** - * Get target object field value - * - * @param target target object - * @param fieldName field name - * @param field type - * @return field value - */ - public static T getFieldValue(Object target, String fieldName) { - try { - Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - @SuppressWarnings("unchecked") - T returnValue = (T) field.get(target); - return returnValue; - } catch (NoSuchFieldException e) { - throw handleException(fieldName, e); - } catch (IllegalAccessException e) { - throw handleException(fieldName, e); - } - } - - /** - * Set target object field value - * - * @param target target object - * @param fieldName field name - * @param value field value - */ - public static void setFieldValue(Object target, String fieldName, Object value) { - try { - Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(target, value); - } catch (NoSuchFieldException e) { - throw handleException(fieldName, e); - } catch (IllegalAccessException e) { - throw handleException(fieldName, e); - } - } - - /** - * Get target method - * - * @param target target object - * @param methodName method name - * @param parameterTypes method parameter types - * @return return value - */ - public static Method getMethod(Object target, String methodName, Class... parameterTypes) { - try { - return target.getClass().getMethod(methodName, parameterTypes); - } catch (NoSuchMethodException e) { - throw handleException(methodName, e); - } - } - - /** - * Check if target class has the given method - * - * @param targetClass target class - * @param methodName method name - * @param parameterTypes method parameter types - * @return method availability - */ - public static boolean hasMethod(Class targetClass, String methodName, Class... parameterTypes) { - try { - targetClass.getMethod(methodName, parameterTypes); - return true; - } catch (NoSuchMethodException e) { - return false; - } - } - - /** - * Get setter method - * - * @param target target object - * @param property property - * @param parameterType setter parameter type - * @return setter method - */ - public static Method getSetter(Object target, String property, Class parameterType) { - String setterMethodName = SETTER_PREFIX + property.substring(0, 1).toUpperCase() + property.substring(1); - Method setter = getMethod(target, setterMethodName, parameterType); - setter.setAccessible(true); - return setter; - } - - /** - * Get getter method - * - * @param target target object - * @param property property - * @return setter method - */ - public static Method getGetter(Object target, String property) { - String getterMethodName = GETTER_PREFIX + property.substring(0, 1).toUpperCase() + property.substring(1); - Method getter = getMethod(target, getterMethodName); - getter.setAccessible(true); - return getter; - } - - /** - * Invoke target method - * - * @param method method to invoke - * @param parameters method parameters - * @return return value - */ - public static T invoke(Object target, Method method, Object... parameters) { - try { - method.setAccessible(true); - @SuppressWarnings("unchecked") - T returnValue = (T) method.invoke(target, parameters); - return returnValue; - } catch (InvocationTargetException e) { - throw handleException(method.getName(), e); - } catch (IllegalAccessException e) { - throw handleException(method.getName(), e); - } - } - - /** - * Invoke getter method with the given parameter - * - * @param target target object - * @param property property - */ - public static T invokeGetter(Object target, String property) { - Method setter = getGetter(target, property); - try { - return (T) setter.invoke(target); - } catch (IllegalAccessException e) { - throw handleException(setter.getName(), e); - } catch (InvocationTargetException e) { - throw handleException(setter.getName(), e); - } - } - - /** - * Invoke setter method with the given parameter - * - * @param target target object - * @param property property - * @param parameter setter parameter - */ - public static void invokeSetter(Object target, String property, Object parameter) { - Method setter = getSetter(target, property, parameter.getClass()); - try { - setter.invoke(target, parameter); - } catch (IllegalAccessException e) { - throw handleException(setter.getName(), e); - } catch (InvocationTargetException e) { - throw handleException(setter.getName(), e); - } - } - - /** - * Invoke setter method with the given parameter - * - * @param target target object - * @param property property - * @param parameter setter parameter - */ - public static void invokeSetter(Object target, String property, boolean parameter) { - Method setter = getSetter(target, property, boolean.class); - try { - setter.invoke(target, parameter); - } catch (IllegalAccessException e) { - throw handleException(setter.getName(), e); - } catch (InvocationTargetException e) { - throw handleException(setter.getName(), e); - } - } - - /** - * Invoke setter method with the given parameter - * - * @param target target object - * @param property property - * @param parameter setter parameter - */ - public static void invokeSetter(Object target, String property, int parameter) { - Method setter = getSetter(target, property, int.class); - try { - setter.invoke(target, parameter); - } catch (IllegalAccessException e) { - throw handleException(setter.getName(), e); - } catch (InvocationTargetException e) { - throw handleException(setter.getName(), e); - } - } - - /** - * Handle {@link NoSuchFieldException} by logging it and rethrown it as a {@link IllegalArgumentException} - * - * @param fieldName field name - * @param e exception - * @return wrapped exception - */ - private static IllegalArgumentException handleException(String fieldName, NoSuchFieldException e) { - LOGGER.error("Couldn't find field " + fieldName, e); - return new IllegalArgumentException(e); - } - - /** - * Handle {@link NoSuchMethodException} by logging it and rethrown it as a {@link IllegalArgumentException} - * - * @param methodName method name - * @param e exception - * @return wrapped exception - */ - private static IllegalArgumentException handleException(String methodName, NoSuchMethodException e) { - LOGGER.error("Couldn't find method " + methodName, e); - return new IllegalArgumentException(e); - } - - /** - * Handle {@link IllegalAccessException} by logging it and rethrown it as a {@link IllegalArgumentException} - * - * @param memberName member name - * @param e exception - * @return wrapped exception - */ - private static IllegalArgumentException handleException(String memberName, IllegalAccessException e) { - LOGGER.error("Couldn't access member " + memberName, e); - return new IllegalArgumentException(e); - } - - /** - * Handle {@link InvocationTargetException} by logging it and rethrown it as a {@link IllegalArgumentException} - * - * @param methodName method name - * @param e exception - * @return wrapped exception - */ - private static IllegalArgumentException handleException(String methodName, InvocationTargetException e) { - LOGGER.error("Couldn't invoke method " + methodName, e); - return new IllegalArgumentException(e); - } - - /** - * Handle {@link ClassNotFoundException} by logging it and rethrown it as a {@link IllegalArgumentException} - * - * @param className class name - * @param e exception - * @return wrapped exception - */ - private static IllegalArgumentException handleException(String className, ClassNotFoundException e) { - LOGGER.error("Couldn't find class " + className, e); - return new IllegalArgumentException(e); - } - - /** - * Handle {@link InstantiationException} by logging it and rethrown it as a {@link IllegalArgumentException} - * - * @param className class name - * @param e exception - * @return wrapped exception - */ - private static IllegalArgumentException handleException(String className, InstantiationException e) { - LOGGER.error("Couldn't instantiate class " + className, e); - return new IllegalArgumentException(e); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/exception/ExceptionUtil.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/exception/ExceptionUtil.java deleted file mode 100644 index 57017e7f7..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/exception/ExceptionUtil.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.exception; - -import java.sql.SQLTimeoutException; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import javax.persistence.LockTimeoutException; - -import org.hibernate.PessimisticLockException; -import org.hibernate.exception.GenericJDBCException; -import org.hibernate.exception.JDBCConnectionException; -import org.hibernate.exception.LockAcquisitionException; - -/** - * @author Vlad Mihalcea - */ -public interface ExceptionUtil { - - static List> LOCK_TIMEOUT_EXCEPTIONS = Arrays.asList( - LockAcquisitionException.class, - LockTimeoutException.class, - PessimisticLockException.class, - javax.persistence.PessimisticLockException.class, - SQLTimeoutException.class - ); - - /** - * Get the root cause of a particular {@code Throwable} - * - * @param t exception - * - * @return exception root cause - */ - static Throwable rootCause(Throwable t) { - Throwable cause = t.getCause(); - if ( cause != null && cause != t ) { - return rootCause( cause ); - } - return t; - } - - /** - * Is the given throwable caused by a database lock timeout? - * - * @param e exception - * - * @return is caused by a database lock timeout - */ - static boolean isLockTimeout(Throwable e) { - AtomicReference causeHolder = new AtomicReference<>(e); - do { - final Throwable cause = causeHolder.get(); - if ( LOCK_TIMEOUT_EXCEPTIONS.stream().anyMatch( c -> c.isInstance( cause ) ) || - e.getMessage().contains( "timeout" ) || - e.getMessage().contains( "timed out" ) || - e.getMessage().contains( "time out" ) - ) { - return true; - } else { - if(cause.getCause() == null || cause.getCause() == cause) { - break; - } else { - causeHolder.set( cause.getCause() ); - } - } - } - while ( true ); - return false; - } - - /** - * Is the given throwable caused by a database MVCC anomaly detection? - * - * @param e exception - * - * @return is caused by a database lock MVCC anomaly detection - */ - static boolean isMVCCAnomalyDetection(Throwable e) { - AtomicReference causeHolder = new AtomicReference<>(e); - do { - final Throwable cause = causeHolder.get(); - if ( - cause.getMessage().contains( "ORA-08177: can't serialize access for this transaction" ) //Oracle - || cause.getMessage().toLowerCase().contains( "could not serialize access due to concurrent update" ) //PSQLException - || cause.getMessage().toLowerCase().contains( "ould not serialize access due to read/write dependencies among transactions" ) //PSQLException - || cause.getMessage().toLowerCase().contains( "snapshot isolation transaction aborted due to update conflict" ) //SQLServerException - ) { - return true; - } else { - if(cause.getCause() == null || cause.getCause() == cause) { - break; - } else { - causeHolder.set( cause.getCause() ); - } - } - } - while ( true ); - return false; - } - - /** - * Was the given exception caused by a SQL connection close - * - * @param e exception - * - * @return is caused by a SQL connection close - */ - static boolean isConnectionClose(Exception e) { - Throwable cause = e; - do { - if ( cause.getMessage().toLowerCase().contains( "connection is close" ) - || cause.getMessage().toLowerCase().contains( "closed connection" ) - ) { - return true; - } else { - if(cause.getCause() == null || cause.getCause() == cause) { - break; - } else { - cause = cause.getCause(); - } - } - } - while ( true ); - return false; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/logging/InlineQueryLogEntryCreator.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/logging/InlineQueryLogEntryCreator.java deleted file mode 100644 index ef8de1904..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/logging/InlineQueryLogEntryCreator.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.logging; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; - -import net.ttddyy.dsproxy.ExecutionInfo; -import net.ttddyy.dsproxy.QueryInfo; -import net.ttddyy.dsproxy.listener.DefaultQueryLogEntryCreator; - -/** - * @author Vlad Mihalcea - */ -public class InlineQueryLogEntryCreator extends DefaultQueryLogEntryCreator { - @Override - protected void writeParamsEntry(StringBuilder sb, ExecutionInfo execInfo, List queryInfoList) { - sb.append( "Params:[" ); - for ( QueryInfo queryInfo : queryInfoList ) { - boolean firstArg = true; - for ( Map paramMap : queryInfo.getQueryArgsList() ) { - - if ( !firstArg ) { - sb.append( ", " ); - } - else { - firstArg = false; - } - - SortedMap sortedParamMap = new TreeMap<>( new StringAsIntegerComparator() ); - sortedParamMap.putAll( paramMap ); - - sb.append( "(" ); - boolean firstParam = true; - for ( Map.Entry paramEntry : sortedParamMap.entrySet() ) { - if ( !firstParam ) { - sb.append( ", " ); - } - else { - firstParam = false; - } - Object parameter = paramEntry.getValue(); - if ( parameter != null && parameter.getClass().isArray() ) { - sb.append( arrayToString( parameter ) ); - } - else { - sb.append( parameter ); - } - } - sb.append( ")" ); - } - } - sb.append( "]" ); - } - - private String arrayToString(Object object) { - if ( object.getClass().isArray() ) { - if ( object instanceof byte[] ) { - return Arrays.toString( (byte[]) object ); - } - if ( object instanceof short[] ) { - return Arrays.toString( (short[]) object ); - } - if ( object instanceof char[] ) { - return Arrays.toString( (char[]) object ); - } - if ( object instanceof int[] ) { - return Arrays.toString( (int[]) object ); - } - if ( object instanceof long[] ) { - return Arrays.toString( (long[]) object ); - } - if ( object instanceof float[] ) { - return Arrays.toString( (float[]) object ); - } - if ( object instanceof double[] ) { - return Arrays.toString( (double[]) object ); - } - if ( object instanceof boolean[] ) { - return Arrays.toString( (boolean[]) object ); - } - if ( object instanceof Object[] ) { - return Arrays.toString( (Object[]) object ); - } - } - throw new UnsupportedOperationException( "Arrat type not supported: " + object.getClass() ); - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/CockroachDBDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/CockroachDBDataSourceProvider.java deleted file mode 100644 index 8043be7fb..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/CockroachDBDataSourceProvider.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.providers; - -import java.util.Properties; -import javax.sql.DataSource; - -import org.hibernate.dialect.PostgreSQL82Dialect; -import org.hibernate.dialect.PostgreSQL94Dialect; - -import com.vladmihalcea.book.hpjp.util.CockroachDBDialect; -import org.postgresql.ds.PGSimpleDataSource; - -/** - * @author Vlad Mihalcea - */ -public class CockroachDBDataSourceProvider - implements DataSourceProvider { - - @Override - public String hibernateDialect() { - return CockroachDBDialect.class.getName(); - } - - @Override - public DataSource dataSource() { - PGSimpleDataSource dataSource = new PGSimpleDataSource(); - dataSource.setDatabaseName( "high_performance_java_persistence" ); - dataSource.setServerName( host() ); - dataSource.setPortNumber( port() ); - dataSource.setUser( username() ); - dataSource.setPassword( password() ); - dataSource.setSsl( false ); - return dataSource; - } - - @Override - public Class dataSourceClassName() { - return PGSimpleDataSource.class; - } - - @Override - public Properties dataSourceProperties() { - Properties properties = new Properties(); - properties.setProperty( "databaseName", "high_performance_java_persistence" ); - properties.setProperty( "serverName", host() ); - properties.setProperty( "portNumber", String.valueOf( port() ) ); - properties.setProperty( "user", username() ); - properties.setProperty( "password", password() ); - properties.setProperty( "sslmode", "disabled" ); - return properties; - } - - @Override - public String url() { - return String.format( - "jdbc:postgresql://%s:%d/high_performance_java_persistence?sslmode=disable", - host(), - port() - ); - } - - public String host() { - return "127.0.0.1"; - } - - public int port() { - return 26257; - } - - @Override - public String username() { - return "cockroach"; - } - - @Override - public String password() { - return "admin"; - } - - @Override - public Database database() { - return Database.COCKROACHDB; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/DataSourceProvider.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/DataSourceProvider.java deleted file mode 100644 index f81ee6de0..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/DataSourceProvider.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.providers; - -import java.util.Properties; -import javax.sql.DataSource; - -/** - * @author Vlad Mihalcea - */ -public interface DataSourceProvider { - - enum IdentifierStrategy { - IDENTITY, - SEQUENCE - } - - String hibernateDialect(); - - DataSource dataSource(); - - Class dataSourceClassName(); - - Properties dataSourceProperties(); - - String url(); - - String username(); - - String password(); - - Database database(); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/Database.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/Database.java deleted file mode 100644 index 4fe0dd555..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/Database.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.providers; - -/** - * @author Vlad Mihalcea - */ -public enum Database { - HSQLDB, - POSTGRESQL, - ORACLE, - MYSQL, - SQLSERVER, - COCKROACHDB -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/HsqldbDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/HsqldbDataSourceProvider.java deleted file mode 100644 index 86f4a770f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/HsqldbDataSourceProvider.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.providers; - -import java.util.Arrays; -import java.util.List; -import java.util.Properties; -import javax.sql.DataSource; - -import org.hsqldb.jdbc.JDBCDataSource; - -/** - * @author Vlad Mihalcea - */ -public class HsqldbDataSourceProvider implements DataSourceProvider { - - @Override - public String hibernateDialect() { - return "org.hibernate.dialect.HSQLDialect"; - } - - @Override - public DataSource dataSource() { - JDBCDataSource dataSource = new JDBCDataSource(); - dataSource.setUrl( "jdbc:hsqldb:mem:test" ); - dataSource.setUser( "sa" ); - dataSource.setPassword( "" ); - return dataSource; - } - - @Override - public Class dataSourceClassName() { - return JDBCDataSource.class; - } - - @Override - public Properties dataSourceProperties() { - Properties properties = new Properties(); - properties.setProperty( "url", url() ); - properties.setProperty( "user", username() ); - properties.setProperty( "password", password() ); - return properties; - } - - @Override - public String url() { - return "jdbc:hsqldb:mem:test"; - } - - @Override - public String username() { - return "sa"; - } - - @Override - public String password() { - return ""; - } - - @Override - public Database database() { - return Database.HSQLDB; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/JTDSDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/JTDSDataSourceProvider.java deleted file mode 100644 index e0dd7cbf5..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/JTDSDataSourceProvider.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.providers; - -import java.util.Arrays; -import java.util.List; -import java.util.Properties; -import javax.sql.DataSource; - -import net.sourceforge.jtds.jdbcx.JtdsDataSource; - -/** - * @author Vlad Mihalcea - */ -public class JTDSDataSourceProvider implements DataSourceProvider { - @Override - public String hibernateDialect() { - return "org.hibernate.dialect.SQLServer2012Dialect"; - } - - @Override - public DataSource dataSource() { - JtdsDataSource dataSource = new JtdsDataSource(); - dataSource.setServerName( "localhost" ); - dataSource.setDatabaseName( "high_performance_java_persistence" ); - dataSource.setInstance( "SQLEXPRESS" ); - dataSource.setUser( "sa" ); - dataSource.setPassword( "adm1n" ); - return dataSource; - } - - @Override - public Class dataSourceClassName() { - return JtdsDataSource.class; - } - - @Override - public Properties dataSourceProperties() { - Properties properties = new Properties(); - properties.setProperty( "databaseName", "high_performance_java_persistence" ); - properties.setProperty( "serverName", "localhost" ); - properties.setProperty( "instance", "SQLEXPRESS" ); - properties.setProperty( "user", username() ); - properties.setProperty( "password", password() ); - return properties; - } - - @Override - public String url() { - return null; - } - - @Override - public String username() { - return "sa"; - } - - @Override - public String password() { - return "adm1n"; - } - - @Override - public Database database() { - return Database.SQLSERVER; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/LockType.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/LockType.java deleted file mode 100644 index 4c9a61968..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/LockType.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.providers; - -/** - * @author Vlad Mihalcea - */ -public enum LockType { - LOCKS, - MVLOCKS, - MVCC -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/MySQLDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/MySQLDataSourceProvider.java deleted file mode 100644 index 583d5be41..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/MySQLDataSourceProvider.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.providers; - -import java.util.Arrays; -import java.util.List; -import java.util.Properties; -import javax.sql.DataSource; - -import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; - -/** - * @author Vlad Mihalcea - */ -public class MySQLDataSourceProvider implements DataSourceProvider { - - private boolean rewriteBatchedStatements = true; - - private boolean cachePrepStmts = false; - - private boolean useServerPrepStmts = false; - - private boolean useTimezone = false; - - private boolean useJDBCCompliantTimezoneShift = false; - - private boolean useLegacyDatetimeCode = true; - - public boolean isRewriteBatchedStatements() { - return rewriteBatchedStatements; - } - - public void setRewriteBatchedStatements(boolean rewriteBatchedStatements) { - this.rewriteBatchedStatements = rewriteBatchedStatements; - } - - public boolean isCachePrepStmts() { - return cachePrepStmts; - } - - public void setCachePrepStmts(boolean cachePrepStmts) { - this.cachePrepStmts = cachePrepStmts; - } - - public boolean isUseServerPrepStmts() { - return useServerPrepStmts; - } - - public void setUseServerPrepStmts(boolean useServerPrepStmts) { - this.useServerPrepStmts = useServerPrepStmts; - } - - public boolean isUseTimezone() { - return useTimezone; - } - - public void setUseTimezone(boolean useTimezone) { - this.useTimezone = useTimezone; - } - - public boolean isUseJDBCCompliantTimezoneShift() { - return useJDBCCompliantTimezoneShift; - } - - public void setUseJDBCCompliantTimezoneShift(boolean useJDBCCompliantTimezoneShift) { - this.useJDBCCompliantTimezoneShift = useJDBCCompliantTimezoneShift; - } - - public boolean isUseLegacyDatetimeCode() { - return useLegacyDatetimeCode; - } - - public void setUseLegacyDatetimeCode(boolean useLegacyDatetimeCode) { - this.useLegacyDatetimeCode = useLegacyDatetimeCode; - } - - @Override - public String hibernateDialect() { - return "org.hibernate.dialect.MySQL57Dialect"; - } - - @Override - public DataSource dataSource() { - MysqlDataSource dataSource = new MysqlDataSource(); - dataSource.setURL( "jdbc:mysql://localhost/high_performance_java_persistence?" + - "rewriteBatchedStatements=" + rewriteBatchedStatements + - "&cachePrepStmts=" + cachePrepStmts + - "&useServerPrepStmts=" + useServerPrepStmts + - "&useTimezone=" + useTimezone + - "&useJDBCCompliantTimezoneShift=" + useJDBCCompliantTimezoneShift + - "&useLegacyDatetimeCode=" + useLegacyDatetimeCode - - ); - dataSource.setUser( "mysql" ); - dataSource.setPassword( "admin" ); - return dataSource; - } - - @Override - public Class dataSourceClassName() { - return MysqlDataSource.class; - } - - @Override - public Properties dataSourceProperties() { - Properties properties = new Properties(); - properties.setProperty( "url", url() ); - return properties; - } - - @Override - public String url() { - return "jdbc:mysql://localhost/high_performance_java_persistence?user=mysql&password=admin"; - } - - @Override - public String username() { - return null; - } - - @Override - public String password() { - return null; - } - - @Override - public Database database() { - return Database.MYSQL; - } - - @Override - public String toString() { - return "MySQLDataSourceProvider{" + - "rewriteBatchedStatements=" + rewriteBatchedStatements + - ", cachePrepStmts=" + cachePrepStmts + - ", useServerPrepStmts=" + useServerPrepStmts + - ", useTimezone=" + useTimezone + - ", useJDBCCompliantTimezoneShift=" + useJDBCCompliantTimezoneShift + - ", useLegacyDatetimeCode=" + useLegacyDatetimeCode + - '}'; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/OracleDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/OracleDataSourceProvider.java deleted file mode 100644 index 9a56c365a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/OracleDataSourceProvider.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.providers; - -import java.util.Arrays; -import java.util.List; -import java.util.Properties; -import javax.sql.DataSource; - -import com.vladmihalcea.book.hpjp.util.ReflectionUtils; - -/** - * @author Vlad Mihalcea - */ -public class OracleDataSourceProvider implements DataSourceProvider { - @Override - public String hibernateDialect() { - return "org.hibernate.dialect.Oracle12cDialect"; - } - - @Override - public DataSource dataSource() { - try { - DataSource dataSource = ReflectionUtils.newInstance( "oracle.jdbc.pool.OracleDataSource" ); - ReflectionUtils.invokeSetter( dataSource, "databaseName", "high_performance_java_persistence" ); - ReflectionUtils.invokeSetter( dataSource, "URL", url() ); - ReflectionUtils.invokeSetter( dataSource, "user", "oracle" ); - ReflectionUtils.invokeSetter( dataSource, "password", "admin" ); - return dataSource; - } - catch (Exception e) { - throw new IllegalStateException( e ); - } - } - - @Override - public Class dataSourceClassName() { - try { - return (Class) Class.forName( "oracle.jdbc.pool.OracleDataSource" ); - } - catch (ClassNotFoundException e) { - throw new IllegalArgumentException( e ); - } - } - - @Override - public Properties dataSourceProperties() { - Properties properties = new Properties(); - properties.setProperty( "databaseName", "high_performance_java_persistence" ); - properties.setProperty( "URL", url() ); - properties.setProperty( "user", username() ); - properties.setProperty( "password", password() ); - return properties; - } - - @Override - public String url() { - return "jdbc:oracle:thin:@localhost:1521/xe"; - //return "jdbc:oracle:thin:@localhost:1521/orclpdb1"; - } - - @Override - public String username() { - return "oracle"; - } - - @Override - public String password() { - return "admin"; - } - - @Override - public Database database() { - return Database.ORACLE; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/PostgreSQLDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/PostgreSQLDataSourceProvider.java deleted file mode 100644 index ad37ef4ff..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/PostgreSQLDataSourceProvider.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.providers; - -import java.util.Arrays; -import java.util.List; -import java.util.Properties; -import javax.sql.DataSource; - -import org.hibernate.dialect.PostgreSQL95Dialect; - -import org.postgresql.ds.PGSimpleDataSource; - -/** - * @author Vlad Mihalcea - */ -public class PostgreSQLDataSourceProvider implements DataSourceProvider { - - @Override - public String hibernateDialect() { - return PostgreSQL95Dialect.class.getName(); - } - - @Override - public DataSource dataSource() { - PGSimpleDataSource dataSource = new PGSimpleDataSource(); - dataSource.setDatabaseName( "high_performance_java_persistence" ); - dataSource.setServerName( "localhost" ); - dataSource.setUser( "postgres" ); - dataSource.setPassword( "admin" ); - return dataSource; - } - - @Override - public Class dataSourceClassName() { - return PGSimpleDataSource.class; - } - - @Override - public Properties dataSourceProperties() { - Properties properties = new Properties(); - properties.setProperty( "databaseName", "high_performance_java_persistence" ); - properties.setProperty( "serverName", "localhost" ); - properties.setProperty( "user", username() ); - properties.setProperty( "password", password() ); - return properties; - } - - @Override - public String url() { - return null; - } - - @Override - public String username() { - return "postgres"; - } - - @Override - public String password() { - return "admin"; - } - - @Override - public Database database() { - return Database.POSTGRESQL; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/SQLServerDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/SQLServerDataSourceProvider.java deleted file mode 100644 index 5b37ffe95..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/SQLServerDataSourceProvider.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.providers; - -import java.util.Arrays; -import java.util.List; -import java.util.Properties; -import javax.sql.DataSource; - -import com.microsoft.sqlserver.jdbc.SQLServerDataSource; - -/** - * @author Vlad Mihalcea - */ -public class SQLServerDataSourceProvider implements DataSourceProvider { - @Override - public String hibernateDialect() { - return "org.hibernate.dialect.SQLServer2012Dialect"; - } - - @Override - public DataSource dataSource() { - SQLServerDataSource dataSource = new SQLServerDataSource(); - dataSource.setURL( - "jdbc:sqlserver://localhost;instance=SQLEXPRESS;databaseName=high_performance_java_persistence;user=sa;password=adm1n" ); - return dataSource; - } - - @Override - public Class dataSourceClassName() { - return SQLServerDataSource.class; - } - - @Override - public Properties dataSourceProperties() { - Properties properties = new Properties(); - properties.setProperty( "URL", url() ); - return properties; - } - - @Override - public String url() { - return "jdbc:sqlserver://localhost;instance=SQLEXPRESS;databaseName=high_performance_java_persistence;user=sa;password=adm1n"; - } - - @Override - public String username() { - return null; - } - - @Override - public String password() { - return null; - } - - @Override - public Database database() { - return Database.SQLSERVER; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/TaskEntityProvider.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/TaskEntityProvider.java deleted file mode 100644 index ce1a794c8..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/TaskEntityProvider.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.providers.entity; - -import com.vladmihalcea.book.hpjp.util.EntityProvider; - -import javax.persistence.*; -import java.util.Date; - -/** - * @author Vlad Mihalcea - */ -public class TaskEntityProvider implements EntityProvider { - - public enum StatusType { - TO_D0, - DONE, - FAILED - } - - @Override - public Class[] entities() { - return new Class[]{ - Task.class - }; - } - - @Entity(name = "task") - public static class Task { - - @Id - private Long id; - - @Enumerated(EnumType.STRING) - private StatusType status; - - @Embedded - private Change change; - } - - @Embeddable - public static class Change { - - @Column(name = "changed_on") - private Date changedOn; - - @Column(name = "created_by") - private String changedBy; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/flyway/AbstractHsqldbFlywayConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/flyway/AbstractHsqldbFlywayConfiguration.java deleted file mode 100644 index 8d479941c..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/flyway/AbstractHsqldbFlywayConfiguration.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.spring.config.flyway; - -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import javax.sql.DataSource; -import java.util.Properties; - -/** - * @author Vlad Mihalcea - */ -@Configuration -@PropertySource({"/META-INF/jdbc-hsqldb.properties"}) -public class AbstractHsqldbFlywayConfiguration extends AbstractFlywayConfiguration { - - @Value("${jdbc.dataSourceClassName}") - private String dataSourceClassName; - - @Value("${jdbc.url}") - private String jdbcUrl; - - @Value("${jdbc.username}") - private String jdbcUser; - - @Value("${jdbc.password}") - private String jdbcPassword; - - - @Override - public DataSource actualDataSource() { - Properties driverProperties = new Properties(); - driverProperties.setProperty("url", jdbcUrl); - driverProperties.setProperty("user", jdbcUser); - driverProperties.setProperty("password", jdbcPassword); - - Properties properties = new Properties(); - properties.put("dataSourceClassName", dataSourceClassName); - properties.put("dataSourceProperties", driverProperties); - //properties.setProperty("minimumPoolSize", String.valueOf(1)); - properties.setProperty("maximumPoolSize", String.valueOf(3)); - properties.setProperty("connectionTimeout", String.valueOf(5000)); - return new HikariDataSource(new HikariConfig(properties)); - } - - @Override - protected String databaseType() { - return "hsqldb"; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jpa/AbstractHsqldbJpaConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jpa/AbstractHsqldbJpaConfiguration.java deleted file mode 100644 index bbbf3e53f..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jpa/AbstractHsqldbJpaConfiguration.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.spring.config.jpa; - -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import javax.sql.DataSource; -import java.util.Properties; - -/** - * @author Vlad Mihalcea - */ -@Configuration -@PropertySource({"/META-INF/jdbc-hsqldb.properties"}) -public class AbstractHsqldbJpaConfiguration extends AbstractJpaConfiguration { - - @Value("${jdbc.dataSourceClassName}") - private String dataSourceClassName; - - @Value("${jdbc.url}") - private String jdbcUrl; - - @Value("${jdbc.username}") - private String jdbcUser; - - @Value("${jdbc.password}") - private String jdbcPassword; - - @Override - public DataSource actualDataSource() { - Properties driverProperties = new Properties(); - driverProperties.setProperty("url", jdbcUrl); - driverProperties.setProperty("user", jdbcUser); - driverProperties.setProperty("password", jdbcPassword); - - Properties properties = new Properties(); - properties.put("dataSourceClassName", dataSourceClassName); - properties.put("dataSourceProperties", driverProperties); - //properties.setProperty("minimumPoolSize", String.valueOf(1)); - properties.setProperty("maximumPoolSize", String.valueOf(3)); - properties.setProperty("connectionTimeout", String.valueOf(5000)); - return new HikariDataSource(new HikariConfig(properties)); - } - - @Override - protected String databaseType() { - return "hsqldb"; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jpa/AbstractJpaConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jpa/AbstractJpaConfiguration.java deleted file mode 100644 index f79a6f8d6..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jpa/AbstractJpaConfiguration.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.spring.config.jpa; - -import com.vladmihalcea.book.hpjp.util.DataSourceProxyType; -import com.vladmihalcea.book.hpjp.util.logging.InlineQueryLogEntryCreator; -import net.ttddyy.dsproxy.listener.SLF4JQueryLoggingListener; -import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; -import org.hibernate.jpa.HibernatePersistenceProvider; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.EnableAspectJAutoProxy; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.JpaVendorAdapter; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.persistence.EntityManagerFactory; -import javax.sql.DataSource; -import java.util.Properties; - -/** - * @author Vlad Mihalcea - */ -@EnableTransactionManagement -@EnableAspectJAutoProxy -public abstract class AbstractJpaConfiguration { - - public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); - - @Value("${hibernate.dialect}") - private String hibernateDialect; - - @Bean - public static PropertySourcesPlaceholderConfigurer properties() { - return new PropertySourcesPlaceholderConfigurer(); - } - - @Bean - public abstract DataSource actualDataSource(); - - private DataSource dataSource() { - SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); - loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); - return ProxyDataSourceBuilder - .create(actualDataSource()) - .name(DATA_SOURCE_PROXY_NAME) - .listener(loggingListener) - .build(); - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactory() { - LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - localContainerEntityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); - localContainerEntityManagerFactoryBean.setPersistenceProvider(new HibernatePersistenceProvider()); - localContainerEntityManagerFactoryBean.setDataSource(dataSource()); - localContainerEntityManagerFactoryBean.setPackagesToScan(packagesToScan()); - - JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); - localContainerEntityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); - localContainerEntityManagerFactoryBean.setJpaProperties(additionalProperties()); - return localContainerEntityManagerFactoryBean; - } - - @Bean - public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ - JpaTransactionManager transactionManager = new JpaTransactionManager(); - transactionManager.setEntityManagerFactory(entityManagerFactory); - return transactionManager; - } - - @Bean - public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { - return new TransactionTemplate(transactionManager(entityManagerFactory)); - } - - protected Properties additionalProperties() { - Properties properties = new Properties(); - properties.setProperty("hibernate.dialect", hibernateDialect); - properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); - return properties; - } - - protected String[] packagesToScan() { - return new String[]{ - configurationClass().getPackage().getName() - }; - } - - protected Class configurationClass() { - return this.getClass(); - } - - @Bean - protected abstract String databaseType(); -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jpa/HikariCPPostgreSQLJpaConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jpa/HikariCPPostgreSQLJpaConfiguration.java deleted file mode 100644 index 52046730c..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jpa/HikariCPPostgreSQLJpaConfiguration.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.spring.config.jpa; - -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import javax.sql.DataSource; -import java.util.Properties; - -/** - * @author Vlad Mihalcea - */ -@Configuration -@PropertySource({"/META-INF/jdbc-postgresql.properties"}) -public class HikariCPPostgreSQLJpaConfiguration extends AbstractJpaConfiguration { - - @Value("${jdbc.dataSourceClassName}") - private String dataSourceClassName; - - @Value("${jdbc.username}") - private String jdbcUser; - - @Value("${jdbc.password}") - private String jdbcPassword; - - @Value("${jdbc.database}") - private String jdbcDatabase; - - @Value("${jdbc.host}") - private String jdbcHost; - - @Value("${jdbc.port}") - private String jdbcPort; - - @Override - public DataSource actualDataSource() { - Properties driverProperties = new Properties(); - driverProperties.setProperty("user", jdbcUser); - driverProperties.setProperty("password", jdbcPassword); - driverProperties.setProperty("databaseName", jdbcDatabase); - driverProperties.setProperty("serverName", jdbcHost); - driverProperties.setProperty("portNumber", jdbcPort); - - Properties properties = new Properties(); - properties.put("dataSourceClassName", dataSourceClassName); - properties.put("dataSourceProperties", driverProperties); - //properties.setProperty("minimumPoolSize", String.valueOf(1)); - properties.setProperty("maximumPoolSize", String.valueOf(3)); - properties.setProperty("connectionTimeout", String.valueOf(5000)); - return new HikariDataSource(new HikariConfig(properties)); - } - - @Override - protected String databaseType() { - return "postgresql"; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jpa/PostgreSQLJpaConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jpa/PostgreSQLJpaConfiguration.java deleted file mode 100644 index 65ea81ed0..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jpa/PostgreSQLJpaConfiguration.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.spring.config.jpa; - -import org.postgresql.ds.PGSimpleDataSource; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import javax.sql.DataSource; - -/** - * @author Vlad Mihalcea - */ -@Configuration -@PropertySource({"/META-INF/jdbc-postgresql.properties"}) -public class PostgreSQLJpaConfiguration extends AbstractJpaConfiguration { - - @Value("${jdbc.dataSourceClassName}") - private String dataSourceClassName; - - @Value("${jdbc.username}") - private String jdbcUser; - - @Value("${jdbc.password}") - private String jdbcPassword; - - @Value("${jdbc.database}") - private String jdbcDatabase; - - @Value("${jdbc.host}") - private String jdbcHost; - - @Value("${jdbc.port}") - private String jdbcPort; - - @Override - public DataSource actualDataSource() { - PGSimpleDataSource dataSource = new PGSimpleDataSource(); - dataSource.setDatabaseName(jdbcDatabase); - dataSource.setUser(jdbcUser); - dataSource.setPassword(jdbcPassword); - dataSource.setServerName(jdbcHost); - dataSource.setPortNumber(Integer.valueOf(jdbcPort)); - return dataSource; - } - - @Override - protected String databaseType() { - return "postgresql"; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jta/AbstractJtaTransactionManagerConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jta/AbstractJtaTransactionManagerConfiguration.java deleted file mode 100644 index 374022ca6..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jta/AbstractJtaTransactionManagerConfiguration.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.spring.config.jta; - -import bitronix.tm.BitronixTransactionManager; -import bitronix.tm.TransactionManagerServices; -import com.vladmihalcea.book.hpjp.util.DataSourceProxyType; -import net.ttddyy.dsproxy.listener.SLF4JQueryLoggingListener; -import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; -import org.hibernate.engine.transaction.jta.platform.internal.BitronixJtaPlatform; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.DependsOn; -import org.springframework.context.annotation.EnableAspectJAutoProxy; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.orm.jpa.JpaVendorAdapter; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.transaction.jta.JtaTransactionManager; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.sql.DataSource; -import java.util.Properties; - -import com.vladmihalcea.book.hpjp.util.logging.InlineQueryLogEntryCreator; - -/** - * @author Vlad Mihalcea - */ -@EnableTransactionManagement -@EnableAspectJAutoProxy -public abstract class AbstractJtaTransactionManagerConfiguration { - - public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); - - @Value("${btm.config.journal:null}") - private String btmJournal; - - @Value("${hibernate.dialect}") - private String hibernateDialect; - - @Bean - public static PropertySourcesPlaceholderConfigurer properties() { - return new PropertySourcesPlaceholderConfigurer(); - } - - @Bean - public bitronix.tm.Configuration btmConfig() { - bitronix.tm.Configuration configuration = TransactionManagerServices.getConfiguration(); - configuration.setServerId("spring-btm"); - configuration.setWarnAboutZeroResourceTransaction(true); - configuration.setJournal(btmJournal); - return configuration; - } - - @Bean - public abstract DataSource actualDataSource(); - - @DependsOn(value = {"btmConfig", "actualDataSource"}) - public DataSource dataSource() { - SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); - loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); - return ProxyDataSourceBuilder - .create(actualDataSource()) - .name(DATA_SOURCE_PROXY_NAME) - .listener(loggingListener) - .build(); - } - - @Bean - @DependsOn("btmConfig") - public LocalContainerEntityManagerFactoryBean entityManagerFactory() { - LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - localContainerEntityManagerFactoryBean.setJtaDataSource(dataSource()); - localContainerEntityManagerFactoryBean.setPackagesToScan(packagesToScan()); - - JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); - localContainerEntityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); - localContainerEntityManagerFactoryBean.setJpaProperties(additionalProperties()); - return localContainerEntityManagerFactoryBean; - } - - protected String[] packagesToScan() { - return new String[]{ - configurationClass().getPackage().getName() - }; - } - - protected abstract Class configurationClass(); - - @Bean(destroyMethod = "shutdown") - @DependsOn(value = "btmConfig") - public BitronixTransactionManager jtaTransactionManager() { - return TransactionManagerServices.getTransactionManager(); - } - - @Bean - public JtaTransactionManager transactionManager() { - BitronixTransactionManager bitronixTransactionManager = jtaTransactionManager(); - JtaTransactionManager transactionManager = new JtaTransactionManager(); - transactionManager.setTransactionManager(bitronixTransactionManager); - transactionManager.setUserTransaction(bitronixTransactionManager); - transactionManager.setAllowCustomIsolationLevels(true); - return transactionManager; - } - - @Bean - public TransactionTemplate transactionTemplate() { - return new TransactionTemplate(transactionManager()); - } - - protected Properties additionalProperties() { - Properties properties = new Properties(); - - properties.setProperty("hibernate.transaction.jta.platform", BitronixJtaPlatform.class.getName()); - properties.setProperty("hibernate.dialect", hibernateDialect); - properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); - - return properties; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jta/HsqldbJtaTransactionManagerConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jta/HsqldbJtaTransactionManagerConfiguration.java deleted file mode 100644 index 2e4d8ba3a..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jta/HsqldbJtaTransactionManagerConfiguration.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.spring.config.jta; - -import bitronix.tm.resource.jdbc.PoolingDataSource; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import javax.sql.DataSource; -import java.util.Properties; - -/** - * @author Vlad Mihalcea - */ -@PropertySource({"/META-INF/jta-hsqldb.properties"}) -@Configuration -public abstract class HsqldbJtaTransactionManagerConfiguration extends AbstractJtaTransactionManagerConfiguration{ - - @Value("${jdbc.dataSourceClassName}") - private String dataSourceClassName; - - @Value("${jdbc.username}") - private String jdbcUser; - - @Value("${jdbc.password}") - private String jdbcPassword; - - @Value("${jdbc.url}") - private String jdbcUrl; - public DataSource actualDataSource() { - PoolingDataSource poolingDataSource = new PoolingDataSource(); - poolingDataSource.setClassName(dataSourceClassName); - poolingDataSource.setUniqueName(getClass().getName()); - poolingDataSource.setMinPoolSize(0); - poolingDataSource.setMaxPoolSize(5); - poolingDataSource.setAllowLocalTransactions(true); - poolingDataSource.setDriverProperties(new Properties()); - poolingDataSource.getDriverProperties().put("user", jdbcUser); - poolingDataSource.getDriverProperties().put("password", jdbcPassword); - poolingDataSource.getDriverProperties().put("url", jdbcUrl); - return poolingDataSource; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jta/PostgreSQLJtaTransactionManagerConfiguration.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jta/PostgreSQLJtaTransactionManagerConfiguration.java deleted file mode 100644 index 98dff5e18..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/jta/PostgreSQLJtaTransactionManagerConfiguration.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.spring.config.jta; - -import bitronix.tm.resource.jdbc.PoolingDataSource; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import javax.sql.DataSource; -import java.util.Properties; - -/** - * @author Vlad Mihalcea - */ -@PropertySource({"/META-INF/jta-postgresql.properties"}) -@Configuration -public abstract class PostgreSQLJtaTransactionManagerConfiguration extends AbstractJtaTransactionManagerConfiguration{ - - @Value("${jdbc.dataSourceClassName}") - private String dataSourceClassName; - - @Value("${btm.config.journal:disk}") - private String btmJournal; - - @Value("${jdbc.username}") - private String jdbcUser; - - @Value("${jdbc.password}") - private String jdbcPassword; - - @Value("${jdbc.database}") - private String jdbcDatabase; - - @Value("${jdbc.host}") - private String jdbcHost; - - @Value("${jdbc.port}") - private String jdbcPort; - - @Value("${hibernate.dialect}") - private String hibernateDialect; - - public DataSource actualDataSource() { - PoolingDataSource poolingDataSource = new PoolingDataSource(); - poolingDataSource.setClassName(dataSourceClassName); - poolingDataSource.setUniqueName(getClass().getName()); - poolingDataSource.setMinPoolSize(0); - poolingDataSource.setMaxPoolSize(5); - poolingDataSource.setAllowLocalTransactions(true); - poolingDataSource.setDriverProperties(new Properties()); - poolingDataSource.getDriverProperties().put("user", jdbcUser); - poolingDataSource.getDriverProperties().put("password", jdbcPassword); - poolingDataSource.getDriverProperties().put("databaseName", jdbcDatabase); - poolingDataSource.getDriverProperties().put("serverName", jdbcHost); - poolingDataSource.getDriverProperties().put("portNumber", jdbcPort); - return poolingDataSource; - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/ConnectionVoidCallable.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/ConnectionVoidCallable.java deleted file mode 100644 index 5d7b4184b..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/ConnectionVoidCallable.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.transaction; - -import java.sql.Connection; -import java.sql.SQLException; - -/** - * @author Vlad Mihalcea - */ -@FunctionalInterface -public interface ConnectionVoidCallable { - void execute(Connection connection) throws SQLException; -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/JPATransactionFunction.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/JPATransactionFunction.java deleted file mode 100644 index c572748d1..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/JPATransactionFunction.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.transaction; - -import java.util.function.Function; -import javax.persistence.EntityManager; - -/** - * @author Vlad Mihalcea - */ -@FunctionalInterface -public interface JPATransactionFunction extends Function { - default void beforeTransactionCompletion() { - - } - - default void afterTransactionCompletion() { - - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/JPATransactionVoidFunction.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/JPATransactionVoidFunction.java deleted file mode 100644 index 8640da197..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/JPATransactionVoidFunction.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.transaction; - -import java.util.function.Consumer; -import javax.persistence.EntityManager; - -/** - * @author Vlad Mihalcea - */ -@FunctionalInterface -public interface JPATransactionVoidFunction extends Consumer { - default void beforeTransactionCompletion() { - - } - - default void afterTransactionCompletion() { - - } -} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/VoidCallable.java b/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/VoidCallable.java deleted file mode 100644 index 4e9a9def2..000000000 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/VoidCallable.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.vladmihalcea.book.hpjp.util.transaction; - -import java.util.concurrent.Callable; - -/** - * @author Vlad Mihalcea - */ -@FunctionalInterface -public interface VoidCallable extends Callable { - - void execute(); - - default Void call() throws Exception { - execute(); - return null; - } -} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/access/property/FluentApiPropertyAccessTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/access/property/FluentApiPropertyAccessTest.java new file mode 100644 index 000000000..b6eb96214 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/access/property/FluentApiPropertyAccessTest.java @@ -0,0 +1,182 @@ +package com.vladmihalcea.hpjp.hibernate.access.property; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class FluentApiPropertyAccessTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post = new Post() + .id(1L) + .title("High-Performance Java Persistence") + .addComment(new PostComment() + .review("Awesome book") + .createdOn(Timestamp.from( + LocalDateTime.now().minusDays(1).toInstant(ZoneOffset.UTC)) + ) + ) + .addComment(new PostComment() + .review("High-Performance Rocks!") + .createdOn(Timestamp.from( + LocalDateTime.now().minusDays(2).toInstant(ZoneOffset.UTC)) + ) + ) + .addComment(new PostComment() + .review("Database essentials to the rescue!") + .createdOn(Timestamp.from( + LocalDateTime.now().minusDays(3).toInstant(ZoneOffset.UTC)) + ) + ); + entityManager.persist(post); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(3, post.getComments().size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + private Long id; + + private String title; + + private List comments = new ArrayList<>(); + + public Post() {} + + public Post(String title) { + this.title = title; + } + + public Post id(Long id) { + this.id = id; + return this; + } + + @Id + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Post title(String title) { + this.title = title; + return this; + } + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "post") + public List getComments() { + return comments; + } + + public void setComments(List comments) { + this.comments = comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment.post(this)); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + private Long id; + + private String review; + + private Date createdOn; + + private Post post; + + @Id + @GeneratedValue + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public PostComment review(String review) { + this.review = review; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public PostComment createdOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + @ManyToOne(fetch = FetchType.LAZY) + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public PostComment post(Post post) { + this.post = post; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/AllAssociationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/AllAssociationTest.java new file mode 100644 index 000000000..548dd9e60 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/AllAssociationTest.java @@ -0,0 +1,254 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class AllAssociationTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + PostComment.class, + Tag.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(1L); + post.title = "Postit"; + + PostComment comment1 = new PostComment(); + comment1.id = 1L; + comment1.review = "Good"; + + PostComment comment2 = new PostComment(); + comment2.id = 2L; + comment2.review = "Excellent"; + + post.addComment(comment1); + post.addComment(comment2); + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + LOGGER.info("No alias"); + List posts = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.title = :title + """, Post.class) + .setParameter("title", "Postit") + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Post() {} + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true, fetch = FetchType.LAZY) + private PostDetails details; + + @ManyToMany + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public PostDetails getDetails() { + return details; + } + + public List getTags() { + return tags; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void addDetails(PostDetails details) { + this.details = details; + details.setPost(this); + } + + public void removeDetails() { + this.details.setPost(null); + this.details = null; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + public PostDetails() { + createdOn = new Date(); + } + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public PostComment() {} + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + private Long id; + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyExtraColumnsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyExtraColumnsTest.java new file mode 100644 index 000000000..d885023d7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyExtraColumnsTest.java @@ -0,0 +1,342 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import org.hibernate.Session; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NaturalIdCache; + +import org.junit.Test; + +import com.vladmihalcea.hpjp.util.AbstractTest; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalManyToManyExtraColumnsTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Tag.class, + PostTag.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Tag misc = new Tag().setName("Misc"); + Tag jdbc = new Tag().setName("JDBC"); + Tag hibernate = new Tag().setName("Hibernate"); + Tag jooq = new Tag().setName("jOOQ"); + + entityManager.persist(misc); + entityManager.persist(jdbc); + entityManager.persist(hibernate); + entityManager.persist(jooq); + }); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + Tag misc = session.bySimpleNaturalId(Tag.class).load("Misc"); + Tag jdbc = session.bySimpleNaturalId(Tag.class).load("JDBC"); + Tag hibernate = session.bySimpleNaturalId(Tag.class).load("Hibernate"); + Tag jooq = session.bySimpleNaturalId(Tag.class).load("jOOQ"); + + Post hpjp1 = new Post() + .setTitle("High-Performance Java Persistence 1st edition"); + hpjp1.setId(1L); + + hpjp1.addTag(jdbc); + hpjp1.addTag(hibernate); + hpjp1.addTag(jooq); + hpjp1.addTag(misc); + + entityManager.persist(hpjp1); + + Post hpjp2 = new Post() + .setTitle("High-Performance Java Persistence 2nd edition"); + hpjp2.setId(2L); + + hpjp2.addTag(jdbc); + hpjp2.addTag(hibernate); + hpjp2.addTag(jooq); + + entityManager.persist(hpjp2); + }); + + doInJPA(entityManager -> { + Tag misc = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Tag.class) + .load("Misc"); + + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.tags pt + join fetch pt.tag + where p.id = :postId + """, Post.class) + .setParameter("postId", 1L) + .getSingleResult(); + + post.removeTag(misc); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getTags() { + return tags; + } + + public Post addTag(Tag tag) { + PostTag postTag = new PostTag(this, tag); + tags.add(postTag); + tag.getPosts().add(postTag); + return this; + } + + public Post removeTag(Tag tag) { + for (Iterator iterator = tags.iterator(); iterator.hasNext(); ) { + PostTag postTag = iterator.next(); + if (postTag.getPost().equals(this) && + postTag.getTag().equals(tag)) { + iterator.remove(); + postTag.getTag().getPosts().remove(postTag); + postTag.setPost(null); + postTag.setTag(null); + } + } + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Post)) return false; + return id != null && id.equals(((Post) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } + + @Embeddable + public static class PostTagId implements Serializable { + + @Column(name = "post_id") + private Long postId; + + @Column(name = "tag_id") + private Long tagId; + + private PostTagId() {} + + public PostTagId(Long postId, Long tagId) { + this.postId = postId; + this.tagId = tagId; + } + + public Long getPostId() { + return postId; + } + + public Long getTagId() { + return tagId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTagId that = (PostTagId) o; + return Objects.equals(this.postId, that.getPostId()) && + Objects.equals(this.tagId, that.getTagId()); + } + + @Override + public int hashCode() { + return Objects.hash(this.postId, this.tagId); + } + } + + @Entity(name = "PostTag") + @Table(name = "post_tag") + public static class PostTag { + + @EmbeddedId + private PostTagId id; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("postId") + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("tagId") + private Tag tag; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + private PostTag() {} + + public PostTag(Post post, Tag tag) { + this.post = post; + this.tag = tag; + this.id = new PostTagId(post.getId(), tag.getId()); + } + + public PostTagId getId() { + return id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Tag getTag() { + return tag; + } + + public void setTag(Tag tag) { + this.tag = tag; + } + + public Date getCreatedOn() { + return createdOn; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTag that = (PostTag) o; + return Objects.equals(this.id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(this.id); + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + @NaturalIdCache + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @OneToMany( + mappedBy = "tag", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List posts = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + + public List getPosts() { + return posts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityEmbeddableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityEmbeddableTest.java new file mode 100644 index 000000000..53a28fae6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityEmbeddableTest.java @@ -0,0 +1,324 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import org.hibernate.Session; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NaturalIdCache; + +import org.junit.Test; + +import com.vladmihalcea.hpjp.util.AbstractTest; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalManyToManyLinkEntityEmbeddableTest + extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Tag.class, + PostTag.class + }; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + return properties; + } + + @Test + public void testLifecycle() { + + doInJPA(entityManager -> { + Tag misc = new Tag("Misc"); + Tag jdbc = new Tag("JDBC"); + Tag hibernate = new Tag("Hibernate"); + Tag jooq = new Tag("jOOQ"); + + entityManager.persist( misc ); + entityManager.persist( jdbc ); + entityManager.persist( hibernate ); + entityManager.persist( jooq ); + }); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap( Session.class ); + + Tag misc = session.bySimpleNaturalId(Tag.class).load( "Misc" ); + Tag jdbc = session.bySimpleNaturalId(Tag.class).load( "JDBC" ); + Tag hibernate = session.bySimpleNaturalId(Tag.class).load( "Hibernate" ); + Tag jooq = session.bySimpleNaturalId(Tag.class).load( "jOOQ" ); + + Post hpjp1 = new Post("High-Performance Java Persistence 1st edition"); + hpjp1.setId(1L); + + hpjp1.addTag(jdbc); + hpjp1.addTag(hibernate); + hpjp1.addTag(jooq); + hpjp1.addTag(misc); + + entityManager.persist(hpjp1); + + Post hpjp2 = new Post("High-Performance Java Persistence 2nd edition"); + hpjp2.setId(2L); + + hpjp2.addTag(jdbc); + hpjp2.addTag(hibernate); + hpjp2.addTag(jooq); + + entityManager.persist(hpjp2); + }); + + doInJPA(entityManager -> { + Tag misc = entityManager.unwrap( Session.class ) + .bySimpleNaturalId(Tag.class) + .load( "Misc" ); + + Post post = entityManager.createQuery( + "select p " + + "from Post p " + + "join fetch p.tags pt " + + "join fetch pt.id.tag " + + "where p.id = :postId", Post.class) + .setParameter( "postId", 1L ) + .getSingleResult(); + + post.removeTag( misc ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "id.post", cascade = CascadeType.ALL, orphanRemoval = true) + private List tags = new ArrayList<>(); + + public Post() { + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public List getTags() { + return tags; + } + + public void addTag(Tag tag) { + PostTag postTag = new PostTag(this, tag); + tags.add(postTag); + tag.getPosts().add(postTag); + } + + public void removeTag(Tag tag) { + for (Iterator iterator = tags.iterator(); iterator.hasNext(); ) { + PostTag postTag = iterator.next(); + if (postTag.getId().getTag().equals(tag)) { + iterator.remove(); + postTag.getId().getTag().getPosts().remove(postTag); + break; + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Post post = (Post) o; + return Objects.equals(title, post.title); + } + + @Override + public int hashCode() { + return Objects.hash(title); + } + } + + @Embeddable + public static class PostTagId implements Serializable { + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + private Tag tag; + + private PostTagId() {} + + public PostTagId(Post post, Tag tag) { + this.post = post; + this.tag = tag; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Tag getTag() { + return tag; + } + + public void setTag(Tag tag) { + this.tag = tag; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTagId that = (PostTagId) o; + return Objects.equals(post, that.post) && + Objects.equals(tag, that.tag); + } + + @Override + public int hashCode() { + return Objects.hash(post, tag); + } + } + + @Entity(name = "PostTag") + @Table(name = "post_tag") + public static class PostTag { + + @EmbeddedId + private PostTagId id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + private PostTag() {} + + public PostTag(Post post, Tag tag) { + this.id = new PostTagId(post, tag); + } + + public PostTagId getId() { + return id; + } + + public Date getCreatedOn() { + return createdOn; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTag that = (PostTag) o; + return Objects.equals(this.id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(this.id); + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + @NaturalIdCache + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @OneToMany( + mappedBy = "id.tag", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List posts = new ArrayList<>(); + + public Tag() { + } + + public Tag(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getPosts() { + return posts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityIdColumnsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityIdColumnsTest.java new file mode 100644 index 000000000..365f9a2e9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityIdColumnsTest.java @@ -0,0 +1,256 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalManyToManyLinkEntityIdColumnsTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Tag.class, + PostTag.class + }; + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post1 = new Post("JPA with Hibernate"); + Post post2 = new Post("Native Hibernate"); + + Tag tag1 = new Tag("Java"); + tag1.setId(1L); + Tag tag2 = new Tag("Hibernate"); + tag2.setId(2L); + + entityManager.persist(post1); + entityManager.persist(post2); + + entityManager.persist(tag1); + entityManager.persist(tag2); + + post1.addTag(tag1); + post1.addTag(tag2); + + post2.addTag(tag1); + + entityManager.flush(); + + LOGGER.info("Remove"); + post1.removeTag(tag1); + }); + } + + @Test + public void testShuffle() { + final Long postId = doInJPA(entityManager -> { + Post post1 = new Post("JPA with Hibernate"); + Post post2 = new Post("Native Hibernate"); + + Tag tag1 = new Tag("Java"); + tag1.setId(1L); + Tag tag2 = new Tag("Hibernate"); + tag2.setId(2L); + + entityManager.persist(post1); + entityManager.persist(post2); + + entityManager.persist(tag1); + entityManager.persist(tag2); + + post1.addTag(tag1); + post1.addTag(tag2); + + post2.addTag(tag1); + + entityManager.flush(); + + return post1.getId(); + }); + doInJPA(entityManager -> { + LOGGER.info("Shuffle"); + Post post1 = entityManager.find(Post.class, postId); + Tag tag1 = entityManager.find(Tag.class, 1L); + + PostTag postTag = entityManager.find(PostTag.class, new PostTag(post1, tag1)); + + post1.getTags().sort((postTag1, postTag2) -> + postTag2.getTag().getId().compareTo(postTag1.getTag().getId()) + ); + }); + } + + @Entity(name = "Post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List tags = new ArrayList<>(); + + public Post() { + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public List getTags() { + return tags; + } + + public void addTag(Tag tag) { + PostTag postTag = new PostTag(this, tag); + tags.add(postTag); + tag.getPosts().add(postTag); + } + + public void removeTag(Tag tag) { + for (Iterator iterator = tags.iterator(); iterator.hasNext(); ) { + PostTag postTag = iterator.next(); + if (postTag.getPost().equals(this) && + postTag.getTag().equals(tag)) { + iterator.remove(); + postTag.getTag().getPosts().remove(postTag); + //Fails with this. Throws StaleStateException. + //postTag.setPost(null); + //postTag.setTag(null); + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Post post = (Post) o; + return Objects.equals(title, post.title); + } + + @Override + public int hashCode() { + return Objects.hash(title); + } + } + + @Entity(name = "PostTag") @Table(name = "post_tag") + public static class PostTag implements Serializable { + + @Id + @ManyToOne + private Post post; + + @Id + @ManyToOne + private Tag tag; + + private PostTag() {} + + public PostTag(Post post, Tag tag) { + this.post = post; + this.tag = tag; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Tag getTag() { + return tag; + } + + public void setTag(Tag tag) { + this.tag = tag; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTag that = (PostTag) o; + return Objects.equals(post, that.post) && + Objects.equals(tag, that.tag); + } + + @Override + public int hashCode() { + return Objects.hash(post, tag); + } + } + + @Entity(name = "Tag") + public static class Tag { + + @Id + private Long id; + + private String name; + + @OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true) + private List posts = new ArrayList<>(); + + public Tag() { + } + + public Tag(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getPosts() { + return posts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityMapKeyJoinColumnTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityMapKeyJoinColumnTest.java new file mode 100644 index 000000000..20731f5c4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityMapKeyJoinColumnTest.java @@ -0,0 +1,315 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import java.io.Serializable; +import java.util.Date; +import java.util.Objects; +import java.util.Properties; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import org.hibernate.Session; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NaturalIdCache; + +import org.junit.Test; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import java.util.HashMap; +import java.util.Map; +import jakarta.persistence.MapKeyJoinColumn; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalManyToManyLinkEntityMapKeyJoinColumnTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Tag.class, + PostTag.class + }; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + return properties; + } + + @Test + public void testLifecycle() { + + doInJPA(entityManager -> { + Tag misc = new Tag("Misc"); + Tag jdbc = new Tag("JDBC"); + Tag hibernate = new Tag("Hibernate"); + Tag jooq = new Tag("jOOQ"); + + entityManager.persist( misc ); + entityManager.persist( jdbc ); + entityManager.persist( hibernate ); + entityManager.persist( jooq ); + }); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap( Session.class ); + + Tag misc = session.bySimpleNaturalId(Tag.class).load( "Misc" ); + Tag jdbc = session.bySimpleNaturalId(Tag.class).load( "JDBC" ); + Tag hibernate = session.bySimpleNaturalId(Tag.class).load( "Hibernate" ); + Tag jooq = session.bySimpleNaturalId(Tag.class).load( "jOOQ" ); + + Post hpjp1 = new Post("High-Performance Java Persistence 1st edition"); + hpjp1.setId(1L); + + hpjp1.addTag(jdbc); + hpjp1.addTag(hibernate); + hpjp1.addTag(jooq); + hpjp1.addTag(misc); + + entityManager.persist(hpjp1); + + Post hpjp2 = new Post("High-Performance Java Persistence 2nd edition"); + hpjp2.setId(2L); + + hpjp2.addTag(jdbc); + hpjp2.addTag(hibernate); + hpjp2.addTag(jooq); + + entityManager.persist(hpjp2); + }); + + doInJPA(entityManager -> { + Tag misc = entityManager.unwrap( Session.class ) + .bySimpleNaturalId(Tag.class) + .load( "Misc" ); + + Post post = entityManager.createQuery( + "select p " + + "from Post p " + + "join fetch p.tags pt " + + "join fetch pt.id.tag " + + "where p.id = :postId", Post.class) + .setParameter( "postId", 1L ) + .getSingleResult(); + + post.removeTag( misc ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "id.post", cascade = CascadeType.ALL, orphanRemoval = true) + @MapKeyJoinColumn(name="tag_id") + private Map tags = new HashMap<>(); + + public Post() { + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Map getTags() { + return tags; + } + + public void addTag(Tag tag) { + PostTag postTag = new PostTag(this, tag); + tags.put(tag, postTag); + tag.getPosts().put(this, postTag); + } + + public void removeTag(Tag tag) { + PostTag postTag = tags.remove(tag); + postTag.getId().getTag().getPosts().remove(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Post post = (Post) o; + return Objects.equals(title, post.title); + } + + @Override + public int hashCode() { + return Objects.hash(title); + } + } + + @Embeddable + public static class PostTagId + implements Serializable { + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + private Tag tag; + + private PostTagId() {} + + public PostTagId(Post post, Tag tag) { + this.post = post; + this.tag = tag; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Tag getTag() { + return tag; + } + + public void setTag(Tag tag) { + this.tag = tag; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTagId that = (PostTagId) o; + return Objects.equals(post, that.post) && + Objects.equals(tag, that.tag); + } + + @Override + public int hashCode() { + return Objects.hash(post, tag); + } + } + + @Entity(name = "PostTag") + @Table(name = "post_tag") + public static class PostTag { + + @EmbeddedId + private PostTagId id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + private PostTag() {} + + public PostTag(Post post, Tag tag) { + this.id = new PostTagId(post, tag); + } + + public PostTagId getId() { + return id; + } + + public Date getCreatedOn() { + return createdOn; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTag that = (PostTag) o; + return Objects.equals(this.id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(this.id); + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + @NaturalIdCache + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @OneToMany(mappedBy = "id.tag", cascade = CascadeType.ALL, orphanRemoval = true) + @MapKeyJoinColumn(name="post_id") + private Map posts = new HashMap<>(); + + public Tag() { + } + + public Tag(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getPosts() { + return posts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityOrderColumnTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityOrderColumnTest.java new file mode 100644 index 000000000..950d982c5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityOrderColumnTest.java @@ -0,0 +1,294 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.util.*; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalManyToManyLinkEntityOrderColumnTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Tag.class, + PostTag.class + }; + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post1 = new Post("JPA with Hibernate"); + Post post2 = new Post("Native Hibernate"); + + Tag tag1 = new Tag("Java"); + Tag tag2 = new Tag("Hibernate"); + + entityManager.persist(post1); + entityManager.persist(post2); + + entityManager.persist(tag1); + entityManager.persist(tag2); + + post1.addTag(tag1); + post1.addTag(tag2); + + post2.addTag(tag1); + + entityManager.flush(); + + LOGGER.info("Remove"); + post1.removeTag(tag1); + }); + } + + @Test + public void testShuffle() { + final Long postId = doInJPA(entityManager -> { + Post post1 = new Post("JPA with Hibernate"); + Post post2 = new Post("Native Hibernate"); + + Tag tag1 = new Tag("Java"); + Tag tag2 = new Tag("Hibernate"); + + entityManager.persist(post1); + entityManager.persist(post2); + + entityManager.persist(tag1); + entityManager.persist(tag2); + + post1.addTag(tag1); + post1.addTag(tag2); + + post2.addTag(tag1); + + entityManager.flush(); + + return post1.getId(); + }); + doInJPA(entityManager -> { + LOGGER.info("Shuffle"); + Post post1 = entityManager.find(Post.class, postId); + post1.getTags().sort( + Comparator.comparing((PostTag postTag) -> postTag.getId().getTagId()) + .reversed() + ); + }); + } + + @Entity(name = "Post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + @OrderColumn(name = "entry") + private List tags = new ArrayList<>(); + + public Post() { + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public List getTags() { + return tags; + } + + public void addTag(Tag tag) { + PostTag postTag = new PostTag(this, tag); + tags.add(postTag); + tag.getPosts().add(postTag); + } + + public void removeTag(Tag tag) { + for (Iterator iterator = tags.iterator(); iterator.hasNext(); ) { + PostTag postTag = iterator.next(); + if (postTag.getPost().equals(this) && + postTag.getTag().equals(tag)) { + iterator.remove(); + postTag.getTag().getPosts().remove(postTag); + postTag.setPost(null); + postTag.setTag(null); + break; + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Post post = (Post) o; + return Objects.equals(title, post.title); + } + + @Override + public int hashCode() { + return Objects.hash(title); + } + } + + @Embeddable + public static class PostTagId implements Serializable { + + private Long postId; + + private Long tagId; + + public PostTagId() { + } + + public PostTagId(Long postId, Long tagId) { + this.postId = postId; + this.tagId = tagId; + } + + public Long getPostId() { + return postId; + } + + public Long getTagId() { + return tagId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTagId that = (PostTagId) o; + return Objects.equals(postId, that.getPostId()) && + Objects.equals(tagId, that.getTagId()); + } + + @Override + public int hashCode() { + return Objects.hash(postId, tagId); + } + } + + @Entity(name = "PostTag") + public static class PostTag { + + @EmbeddedId + private PostTagId id; + + @ManyToOne + @MapsId("postId") + private Post post; + + @ManyToOne + @MapsId("tagId") + private Tag tag; + + public PostTag() { + } + + public PostTag(Post post, Tag tag) { + this.post = post; + this.tag = tag; + this.id = new PostTagId(post.getId(), tag.getId()); + } + + public PostTagId getId() { + return id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Tag getTag() { + return tag; + } + + public void setTag(Tag tag) { + this.tag = tag; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTag that = (PostTag) o; + return Objects.equals(this.id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(this.id); + } + } + + @Entity(name = "Tag") + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true) + private List posts = new ArrayList<>(); + + public Tag() { + } + + public Tag(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getPosts() { + return posts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityTest.java new file mode 100644 index 000000000..31f542710 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyLinkEntityTest.java @@ -0,0 +1,315 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.util.*; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalManyToManyLinkEntityTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Tag.class, + PostTag.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Tag().setName("JPA") + ); + + entityManager.persist( + new Tag().setName("Hibernate") + ); + }); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + entityManager.persist( + new Post() + .setId(1L) + .setTitle("JPA with Hibernate") + .addTag(session.bySimpleNaturalId(Tag.class).getReference("JPA")) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + ); + + entityManager.persist( + new Post() + .setId(2L) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + ); + }); + } + + @Test + public void testRemoveTagReference() { + doInJPA(entityManager -> { + Post post1 = entityManager.createQuery(""" + select p + from Post p + join fetch p.tags + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + Session session = entityManager.unwrap(Session.class); + + post1.removeTag(session.bySimpleNaturalId(Tag.class).getReference("JPA")); + }); + } + + @Test + public void testRemovePostEntity() { + doInJPA(entityManager -> { + LOGGER.info("Remove"); + Post post1 = entityManager.getReference(Post.class, 1L); + + entityManager.remove(post1); + }); + } + + @Test + public void testShuffle() { + doInJPA(entityManager -> { + LOGGER.info("Shuffle"); + Post post1 = entityManager.find(Post.class, 1L); + + post1.getTags().sort( + Comparator.comparing((PostTag postTag) -> postTag.getId().getTagId()) + .reversed() + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getTags() { + return tags; + } + + public Post addTag(Tag tag) { + PostTag postTag = new PostTag(this, tag); + tags.add(postTag); + tag.getPosts().add(postTag); + return this; + } + + public Post removeTag(Tag tag) { + for (Iterator iterator = tags.iterator(); iterator.hasNext(); ) { + PostTag postTag = iterator.next(); + if (postTag.getPost().equals(this) && + postTag.getTag().equals(tag)) { + iterator.remove(); + postTag.getTag().getPosts().remove(postTag); + postTag.setPost(null); + postTag.setTag(null); + } + } + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Post)) return false; + return id != null && id.equals(((Post) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } + + @Embeddable + public static class PostTagId implements Serializable { + + private Long postId; + + private Long tagId; + + private PostTagId() {} + + public PostTagId(Long postId, Long tagId) { + this.postId = postId; + this.tagId = tagId; + } + + public Long getPostId() { + return postId; + } + + public Long getTagId() { + return tagId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTagId that = (PostTagId) o; + return Objects.equals(postId, that.getPostId()) && + Objects.equals(tagId, that.getTagId()); + } + + @Override + public int hashCode() { + return Objects.hash(postId, tagId); + } + } + + @Entity(name = "PostTag") + @Table(name = "post_tag") + public static class PostTag { + + @EmbeddedId + private PostTagId id; + + @ManyToOne + @MapsId("postId") + private Post post; + + @ManyToOne + @MapsId("tagId") + private Tag tag; + + private PostTag() {} + + public PostTag(Post post, Tag tag) { + this.post = post; + this.tag = tag; + this.id = new PostTagId(post.getId(), tag.getId()); + } + + public PostTagId getId() { + return id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Tag getTag() { + return tag; + } + + public void setTag(Tag tag) { + this.tag = tag; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTag that = (PostTag) o; + return Objects.equals(this.id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(this.id); + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true) + private List posts = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + + public List getPosts() { + return posts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyListTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyListTest.java new file mode 100644 index 000000000..3ff0628c1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyListTest.java @@ -0,0 +1,264 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.annotations.NaturalId; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalManyToManyListTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + Tag.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post1 = new Post("JPA with Hibernate"); + Post post2 = new Post("Native Hibernate"); + + Tag tag1 = new Tag("Java"); + Tag tag2 = new Tag("Hibernate"); + + post1.addTag(tag1); + post1.addTag(tag2); + + post2.addTag(tag1); + + entityManager.persist(post1); + entityManager.persist(post2); + + entityManager.flush(); + + post1.removeTag(tag1); + }); + } + + @Test + public void testRemovePost() { + final Long postId = doInJPA(entityManager -> { + Post post1 = new Post("JPA with Hibernate"); + Post post2 = new Post("Native Hibernate"); + + Tag tag1 = new Tag("Java"); + Tag tag2 = new Tag("Hibernate"); + + post1.addTag(tag1); + post1.addTag(tag2); + + post2.addTag(tag1); + + entityManager.persist(post1); + entityManager.persist(post2); + + return post1.id; + }); + doInJPA(entityManager -> { + LOGGER.info("Remove Post"); + Post post1 = entityManager.find(Post.class, postId); + + entityManager.remove(post1); + }); + } + + @Test + @Ignore + public void testRemoveTag() { + final Long tagId = doInJPA(entityManager -> { + Post post1 = new Post("JPA with Hibernate"); + Post post2 = new Post("Native Hibernate"); + + Tag tag1 = new Tag("Java"); + Tag tag2 = new Tag("Hibernate"); + + post1.addTag(tag1); + post1.addTag(tag2); + + post2.addTag(tag1); + + entityManager.persist(post1); + entityManager.persist(post2); + + return tag1.id; + }); + doInJPA(entityManager -> { + LOGGER.info("Remove Tag"); + Tag tag1 = entityManager.find(Tag.class, tagId); + + entityManager.remove(tag1); + }); + } + + @Test + public void testShuffle() { + final Long postId = doInJPA(entityManager -> { + Post post1 = new Post("JPA with Hibernate"); + Post post2 = new Post("Native Hibernate"); + + Tag tag1 = new Tag("Java"); + Tag tag2 = new Tag("Hibernate"); + + post1.addTag(tag1); + post1.addTag(tag2); + + post2.addTag(tag1); + + entityManager.persist(post1); + entityManager.persist(post2); + + return post1.id; + }); + doInJPA(entityManager -> { + LOGGER.info("Shuffle"); + Tag tag1 = new Tag("Java"); + Post post1 = entityManager.createQuery(""" + select p + from Post p + join fetch p.tags + where p.id = :id + """, Post.class) + .setParameter("id", postId) + .getSingleResult(); + + post1.removeTag(tag1); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + public Post() {} + + public Post(String title) { + this.title = title; + } + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getTags() { + return tags; + } + + public void addTag(Tag tag) { + tags.add(tag); + tag.getPosts().add(this); + } + + public void removeTag(Tag tag) { + tags.remove(tag); + tag.getPosts().remove(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Post)) return false; + return id != null && id.equals(((Post) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @ManyToMany(mappedBy = "tags") + private List posts = new ArrayList<>(); + + public Tag() {} + + public Tag(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getPosts() { + return posts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyToManyOrderColumnTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyOrderColumnTest.java similarity index 97% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyToManyOrderColumnTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyOrderColumnTest.java index 646a856d6..9eda5f78d 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyToManyOrderColumnTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyOrderColumnTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.*; /** diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManySetTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManySetTest.java new file mode 100644 index 000000000..21968aa67 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManySetTest.java @@ -0,0 +1,203 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalManyToManySetTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Tag.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Tag().setName("JPA") + ); + + entityManager.persist( + new Tag().setName("Hibernate") + ); + }); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + entityManager.persist( + new Post() + .setId(1L) + .setTitle("JPA with Hibernate") + .addTag(session.bySimpleNaturalId(Tag.class).getReference("JPA")) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + ); + + entityManager.persist( + new Post() + .setId(2L) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + ); + }); + } + + @Test + public void testRemoveTagReference() { + doInJPA(entityManager -> { + Post post1 = entityManager.createQuery(""" + select p + from Post p + join fetch p.tags + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + Session session = entityManager.unwrap(Session.class); + + post1.getTags().remove(session.bySimpleNaturalId(Tag.class).getReference("JPA")); + }); + } + + @Test + public void testRemovePostEntity() { + doInJPA(entityManager -> { + LOGGER.info("Remove"); + Post post1 = entityManager.getReference(Post.class, 1L); + + entityManager.remove(post1); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private Set tags = new HashSet<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Set getTags() { + return tags; + } + + public Post addTag(Tag tag) { + tags.add(tag); + tag.getPosts().add(this); + return this; + } + + public Post removeTag(Tag tag) { + tags.remove(tag); + tag.getPosts().remove(this); + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Post)) return false; + return id != null && id.equals(((Post) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @ManyToMany(mappedBy = "tags") + private Set posts = new HashSet<>(); + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + + public Set getPosts() { + return posts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyToManyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyToManyTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyTest.java index 09b7f20fb..6041e1bc3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalManyToManyTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalManyToManyTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.*; /** @@ -149,8 +149,12 @@ public void removeTag(Tag tag) { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) + return true; + + if (!(o instanceof Post)) + return false; + Post post = (Post) o; return Objects.equals(title, post.title); } @@ -202,8 +206,12 @@ public List getPosts() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) + return true; + + if (!(o instanceof Tag)) + return false; + Tag tag = (Tag) o; return Objects.equals(name, tag.name); } diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToManyJoinColumnTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyJoinColumnTest.java similarity index 95% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToManyJoinColumnTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyJoinColumnTest.java index 7eda0e0e3..21f2126ef 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToManyJoinColumnTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyJoinColumnTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyMergeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyMergeTest.java new file mode 100644 index 000000000..e765971a8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyMergeTest.java @@ -0,0 +1,363 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.jpa.AvailableHints; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalOneToManyMergeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + + doInJPA(entityManager -> { + entityManager + .find(Post.class, 1L) + .addComment(new PostComment().setReview("JDBC section is a must-read!")) + .addComment(new PostComment().setReview("The book size is larger than usual.")) + .addComment(new PostComment().setReview("Just half-way through.")) + .addComment(new PostComment().setReview("The book has over 450 pages.")); + }); + } + + @Test + @Ignore + public void testCollectionOverwrite() { + List comments = fetchPostComments(1L); + + modifyComments(comments); + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + entityManager.detach(post); + post.setComments(comments); + entityManager.merge(post); + }); + + verifyResults(); + } + + @Test + public void testCollectionOverwriteFix() { + List comments = fetchPostComments(1L); + + modifyComments(comments); + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + entityManager.detach(post); + + post.getComments().clear(); + for (PostComment comment : comments) { + post.addComment(comment); + } + + entityManager.merge(post); + }); + + verifyResults(); + } + + @Test + public void testCollectionMerge() { + + List comments = fetchPostComments(1L); + + modifyComments(comments); + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + List removedComments = new ArrayList<>(post.getComments()); + removedComments.removeAll(comments); + + for(PostComment removedComment : removedComments) { + post.removeComment(removedComment); + } + + List newComments = new ArrayList<>(comments); + newComments.removeAll(post.getComments()); + + comments.removeAll(newComments); + + for(PostComment existingComment : comments) { + existingComment.setPost(post); + PostComment mergedComment = entityManager.merge(existingComment); + post.getComments().set(post.getComments().indexOf(mergedComment), mergedComment); + } + + for(PostComment newComment : newComments) { + post.addComment(newComment); + } + }); + + verifyResults(); + } + + @Test + public void testPostMerge() { + Post post = fetchPostWithComments(1L); + + modifyPostComments(post); + + doInJPA(entityManager -> { + entityManager.merge(post); + }); + + verifyResults(); + } + + public List fetchPostComments(Long postId) { + return doInJPA(entityManager -> { + return entityManager.createQuery(""" + select pc + from PostComment pc + join pc.post p + where p.id = :postId + order by pc.id + """, PostComment.class) + .setParameter("postId", postId) + .getResultList(); + }); + } + + public Post fetchPostWithComments(Long postId) { + return doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :postId + """, Post.class) + .setHint(AvailableHints.HINT_READ_ONLY, true) + .setParameter("postId", postId) + .getSingleResult(); + }); + } + + private void modifyComments(List comments) { + comments.get(0).setReview("The JDBC part is a must-have!"); + + comments.remove(2); + + comments.add( + new PostComment() + .setReview( + "The last part is about jOOQ and " + + "how to get the most of your relational database." + ) + ); + } + + private void modifyPostComments(Post post) { + post.getComments().get(0).setReview("The JDBC part is a must-have!"); + + post.removeComment(post.getComments().get(2)); + + post.addComment( + new PostComment() + .setReview( + "The last part is about jOOQ and " + + "how to get the most of your relational database." + ) + ); + } + + private void verifyResults() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments c + where p.id = :id + order by c.id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + assertEquals(4, post.getComments().size()); + + assertEquals( + "The JDBC part is a must-have!", + post.getComments().get(0).getReview() + ); + + assertEquals( + "The book size is larger than usual.", + post.getComments().get(1).getReview() + ); + + assertEquals( + "The book has over 450 pages.", + post.getComments().get(2).getReview() + ); + + assertEquals( + "The last part is about jOOQ and how to get the most of your relational database.", + post.getComments().get(3).getReview() + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + private Post setComments(List comments) { + this.comments = comments; + return this; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostComment)) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyOrphanRemovalWithoutCascadeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyOrphanRemovalWithoutCascadeTest.java new file mode 100644 index 000000000..c6c16639b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyOrphanRemovalWithoutCascadeTest.java @@ -0,0 +1,174 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalOneToManyOrphanRemovalWithoutCascadeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Test + public void testOrphanRemoval() { + + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + + PostComment comment1 = new PostComment(); + comment1.setReview("Best book on JPA and Hibernate!"); + post.addComment(comment1); + + PostComment comment2 = new PostComment(); + comment2.setReview("A must-read for every Java developer!"); + post.addComment(comment2); + + entityManager.persist(post); + entityManager.persist(comment1); + entityManager.persist(comment2); + }); + + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, QueryCountHolder.getGrandTotal().getSelect()); + + post.getComments().remove(0); + assertEquals(2, QueryCountHolder.getGrandTotal().getSelect()); + }); + + assertEquals(1, QueryCountHolder.getGrandTotal().getDelete()); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertEquals(1, post.getComments().size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", orphanRemoval = true, cascade = CascadeType.ALL) + //@OneToMany(mappedBy = "post", orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Post() { + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostComment)) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManySetTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManySetTest.java new file mode 100644 index 000000000..de404dd0a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManySetTest.java @@ -0,0 +1,208 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalOneToManySetTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setReview("A must-read for every Java developer!") + ) + ); + }); + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + assertEquals(2, post.getComments().size()); + }); + } + + @Test + public void testRemove() { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + PostComment comment = post.getComments().iterator().next(); + + post.removeComment(comment); + }); + } + + @Test + public void testRemoveParent() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + entityManager.remove(post); + }); + } + + @Test + public void testOrphanRemoval() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, QueryCountHolder.getGrandTotal().getSelect()); + + post.removeComment(post.getComments().iterator().next()); + assertEquals(2, QueryCountHolder.getGrandTotal().getSelect()); + }); + + assertEquals(1, QueryCountHolder.getGrandTotal().getDelete()); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertEquals(1, post.getComments().size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private Set comments = new HashSet<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Set getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostComment)) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyTest.java new file mode 100644 index 000000000..23f4921cc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyTest.java @@ -0,0 +1,206 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalOneToManyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setReview("A must-read for every Java developer!") + ) + ); + }); + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + assertEquals(2, post.getComments().size()); + }); + } + + @Test + public void testRemove() { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + PostComment comment = post.getComments().get(0); + + post.removeComment(comment); + }); + } + + @Test + public void testRemoveParent() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + entityManager.remove(post); + }); + } + + @Test + public void testOrphanRemoval() { + QueryCountHolder.clear(); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, QueryCountHolder.getGrandTotal().getSelect()); + + post.getComments().remove(0); + assertEquals(2, QueryCountHolder.getGrandTotal().getSelect()); + }); + + assertEquals(1, QueryCountHolder.getGrandTotal().getDelete()); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertEquals(1, post.getComments().size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostComment)) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToManyUniquenessTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyUniquenessTest.java similarity index 97% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToManyUniquenessTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyUniquenessTest.java index f2b34c8fc..bfe7fc4e9 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToManyUniquenessTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToManyUniquenessTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.Date; import java.util.List; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneMapsIdStringTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneMapsIdStringTest.java new file mode 100644 index 000000000..a21605eb4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneMapsIdStringTest.java @@ -0,0 +1,125 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalOneToOneMapsIdStringTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + }; + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post = new Post("First post"); + post.setId("ABC12"); + + PostDetails details = new PostDetails(); + post.setDetails(details); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetching Post"); + Post post = entityManager.find(Post.class, "ABC12"); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetching Post"); + PostDetails details = entityManager.find(PostDetails.class, "ABC12"); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @Column(name = "post_id") + private String id; + + private String title; + + @OneToOne(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private PostDetails details; + + public Post() {} + + public Post(String title) { + this.title = title; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public PostDetails getDetails() { + return details; + } + + public void setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + //@Column(name = "post_id") + private String id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "post_id") + private Post post; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToOneMapsIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneMapsIdTest.java similarity index 80% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToOneMapsIdTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneMapsIdTest.java index fb8beaef3..5aa577685 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToOneMapsIdTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneMapsIdTest.java @@ -1,12 +1,16 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Date; import java.util.List; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + /** * @author Vlad Mihalcea */ @@ -26,19 +30,23 @@ public void testLifecycle() { Post post = new Post("First post"); PostDetails details = new PostDetails("John Doe"); post.setDetails(details); + entityManager.persist(post); }); + SQLStatementCountValidator.reset(); + doInJPA(entityManager -> { LOGGER.info("Fetching Post"); Post post = entityManager.find(Post.class, 1L); - /*Post post = entityManager.createQuery( - "select p " + - "from Post p " + - "where p.id = :id", Post.class) - .setParameter("id", 1L) - .getSingleResult();*/ + + assertNotNull(post); + post.setDetails(null); }); + + //The association is mandatory so it can't be deleted + SQLStatementCountValidator.assertSelectCount(2); + SQLStatementCountValidator.assertDeleteCount(0); } @Test @@ -101,8 +109,15 @@ public PostDetails getDetails() { } public void setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } this.details = details; - details.setPost(this); } } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneNPlusOneBytecodeEnhancementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneNPlusOneBytecodeEnhancementTest.java new file mode 100644 index 000000000..123e495da --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneNPlusOneBytecodeEnhancementTest.java @@ -0,0 +1,38 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; +import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner; +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@RunWith(BytecodeEnhancerRunner.class) +@EnhancementOptions( + lazyLoading = true +) +public class BidirectionalOneToOneNPlusOneBytecodeEnhancementTest extends BidirectionalOneToOneNPlusOneTest { + + @Test + public void testNoNPlusOne() { + SQLStatementCountValidator.reset(); + + List posts = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + where p.title like 'Post nr.%' + """, Post.class) + .getResultList(); + }); + + assertEquals(100, posts.size()); + SQLStatementCountValidator.assertSelectCount(1); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneNPlusOneNonOptionalTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneNPlusOneNonOptionalTest.java new file mode 100644 index 000000000..6238ffe2c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneNPlusOneNonOptionalTest.java @@ -0,0 +1,158 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalOneToOneNPlusOneNonOptionalTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + for (int i = 1; i <= 100; i++) { + Post post = new Post().setTitle(String.format("Post nr. %d", i)); + post.setDetails(new PostDetails().setCreatedBy("Vlad Mihalcea")); + + entityManager.persist(post); + } + }); + } + + @Test + public void testNoNPlusOne() { + SQLStatementCountValidator.reset(); + + List posts = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + where p.title like 'Post nr.%' + """, Post.class) + .getResultList(); + }); + + assertEquals(100, posts.size()); + SQLStatementCountValidator.assertSelectCount(1); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + fetch = FetchType.LAZY, + optional = false + ) + private PostDetails details; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneNPlusOneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneNPlusOneTest.java new file mode 100644 index 000000000..b78ad0e45 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneNPlusOneTest.java @@ -0,0 +1,285 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; +import com.vladmihalcea.hpjp.util.AbstractTest; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import jakarta.persistence.*; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalOneToOneNPlusOneTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + PostSummary.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator( + List.of( + PostDTO.class + ) + ) + ) + ); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + for (int i = 1; i <= 100; i++) { + Post post = new Post().setTitle(String.format("Post nr. %d", i)); + post.setDetails(new PostDetails().setCreatedBy("Vlad Mihalcea")); + + entityManager.persist(post); + } + }); + } + + @Test + @Ignore + public void testNPlusOne() { + SQLStatementCountValidator.reset(); + + List posts = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + where p.title like 'Post nr.%' + """, Post.class) + .getResultList(); + }); + + assertEquals(100, posts.size()); + SQLStatementCountValidator.assertSelectCount(1); + } + + @Test + public void testWithoutNPlusOne() { + SQLStatementCountValidator.reset(); + + List posts = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from PostSummary p + where p.title like 'Post nr.%' + """, PostSummary.class) + .getResultList(); + }); + + assertEquals(100, posts.size()); + SQLStatementCountValidator.assertSelectCount(1); + } + + @Test + public void testFetchPostAndDetailsProjection() { + doInJPA(entityManager -> { + for (int i = 1; i <= 100; i++) { + entityManager.persist( + new Post().setTitle(String.format("Post nr. %d", 100 + i)) + ); + } + }); + + SQLStatementCountValidator.reset(); + + List posts = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select new PostDTO(p.id, p.title, pd.createdOn, pd.createdBy) + from PostSummary p + left join PostDetails pd on p.id = pd.id + where p.title like :titleToken + """, PostDTO.class) + .setParameter("titleToken", "Post nr.%") + .getResultList(); + }); + + assertEquals(200, posts.size()); + SQLStatementCountValidator.assertSelectCount(1); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + fetch = FetchType.LAZY + ) + private PostDetails details; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + } + + @Entity(name = "PostSummary") + @Table(name = "post") + public static class PostSummary { + + @Id + @GeneratedValue + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public PostSummary setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public PostSummary setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + } + + public static class PostDTO { + + private Long id; + + private String title; + + private Date createdOn; + + private String createdBy; + + public PostDTO(Long id, String title, Date createdOn, String createdBy) { + this.id = id; + this.title = title; + this.createdOn = createdOn; + this.createdBy = createdBy; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public Date getCreatedOn() { + return createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneOptionalFalseEagerFetchingDefaultTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneOptionalFalseEagerFetchingDefaultTest.java new file mode 100644 index 000000000..212273170 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneOptionalFalseEagerFetchingDefaultTest.java @@ -0,0 +1,161 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalOneToOneOptionalFalseEagerFetchingDefaultTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + for (int i = 1; i <= 100; i++) { + entityManager.persist( + new Post() + .setTitle(String.format("Post nr. %d", i)) + .setDetails( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + ) + ); + } + }); + } + + @Test + public void testNPlusOne() { + SQLStatementCountValidator.reset(); + + List posts = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + where p.title like 'Post nr.%' + """, Post.class) + .getResultList(); + }); + + assertEquals(100, posts.size()); + SQLStatementCountValidator.assertSelectCount(101); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + fetch = FetchType.LAZY, + optional = false + ) + private PostDetails details; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + @GeneratedValue + private Long id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneOptionalFalseLazyFetchingMapsIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneOptionalFalseLazyFetchingMapsIdTest.java new file mode 100644 index 000000000..14b870eae --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneOptionalFalseLazyFetchingMapsIdTest.java @@ -0,0 +1,162 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalOneToOneOptionalFalseLazyFetchingMapsIdTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + for (int i = 1; i <= 100; i++) { + entityManager.persist( + new Post() + .setTitle(String.format("Post nr. %d", i)) + .setDetails( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + ) + ); + } + }); + } + + @Test + public void testNPlusOne() { + SQLStatementCountValidator.reset(); + + List posts = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + where p.title like 'Post nr.%' + """, Post.class) + .getResultList(); + }); + + assertEquals(100, posts.size()); + SQLStatementCountValidator.assertSelectCount(1); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + fetch = FetchType.LAZY, + optional = false + ) + private PostDetails details; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneOptionalTrueEagerFetchingDefaultTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneOptionalTrueEagerFetchingDefaultTest.java new file mode 100644 index 000000000..5196ea7f2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneOptionalTrueEagerFetchingDefaultTest.java @@ -0,0 +1,160 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalOneToOneOptionalTrueEagerFetchingDefaultTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + for (int i = 1; i <= 100; i++) { + entityManager.persist( + new Post() + .setTitle(String.format("Post nr. %d", i)) + .setDetails( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + ) + ); + } + }); + } + + @Test + public void testNPlusOne() { + SQLStatementCountValidator.reset(); + + List posts = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + where p.title like 'Post nr.%' + """, Post.class) + .getResultList(); + }); + + assertEquals(100, posts.size()); + SQLStatementCountValidator.assertSelectCount(101); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + fetch = FetchType.LAZY + ) + private PostDetails details; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + @GeneratedValue + private Long id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneOptionalTrueEagerFetchingMapsIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneOptionalTrueEagerFetchingMapsIdTest.java new file mode 100644 index 000000000..4f1b135bd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneOptionalTrueEagerFetchingMapsIdTest.java @@ -0,0 +1,161 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BidirectionalOneToOneOptionalTrueEagerFetchingMapsIdTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + for (int i = 1; i <= 100; i++) { + entityManager.persist( + new Post() + .setTitle(String.format("Post nr. %d", i)) + .setDetails( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + ) + ); + } + }); + } + + @Test + public void testNPlusOne() { + SQLStatementCountValidator.reset(); + + List posts = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + where p.title like 'Post nr.%' + """, Post.class) + .getResultList(); + }); + + assertEquals(100, posts.size()); + SQLStatementCountValidator.assertSelectCount(101); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + fetch = FetchType.LAZY + ) + private PostDetails details; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToOneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneTest.java similarity index 79% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToOneTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneTest.java index f58e2eaba..0c9a14e02 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/BidirectionalOneToOneTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/BidirectionalOneToOneTest.java @@ -1,9 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Date; import static org.junit.Assert.assertNotNull; @@ -21,12 +22,18 @@ protected Class[] entities() { }; } + @Override + protected Database database() { + return Database.POSTGRESQL; + } + @Test public void testLifecycle() { doInJPA(entityManager -> { Post post = new Post("First post"); PostDetails details = new PostDetails("John Doe"); post.setDetails(details); + entityManager.persist(post); }); @@ -34,6 +41,8 @@ public void testLifecycle() { LOGGER.info("Fetching Post"); Post post = entityManager.find(Post.class, 1L); assertNotNull(post); + + post.setDetails(null); }); } @@ -47,7 +56,11 @@ public static class Post { private String title; - @OneToOne(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false) + @OneToOne( + mappedBy = "post", + fetch = FetchType.LAZY, + cascade = CascadeType.ALL + ) private PostDetails details; public Post() {} @@ -77,8 +90,15 @@ public PostDetails getDetails() { } public void setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } this.details = details; - details.setPost(this); } } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionArrayTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionArrayTest.java new file mode 100644 index 000000000..17b89bd29 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionArrayTest.java @@ -0,0 +1,92 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.Arrays; + +/** + * @author Vlad Mihalcea + */ +public class ElementCollectionArrayTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + post.setComments(new String[] { + "My first review", + "My second review", + "My third review", + }); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + LOGGER.info("Remove tail"); + post.setComments(Arrays.copyOf(post.getComments(), 2)); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + LOGGER.info("Remove head"); + post.setComments(Arrays.copyOfRange(post.getComments(), 1, 2)); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @ElementCollection + @OrderColumn(name = "position") + private String[] comments; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public String[] getComments() { + return comments; + } + + public void setComments(String[] comments) { + this.comments = comments; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionListMergeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionListMergeTest.java new file mode 100644 index 000000000..fc0c7f2c3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionListMergeTest.java @@ -0,0 +1,375 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.beans.BeanUtils; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.util.*; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ElementCollectionListMergeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostCategory.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + PostCategory category1 = new PostCategory() + .setCategory("Post"); + PostCategory category2 = new PostCategory() + .setCategory("Archive"); + entityManager.persist(category1); + entityManager.persist(category2); + + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addCategory(category1) + .addCategory(category2) + .addComment(new Comment().setComment("firstComment")) + ); + }); + } + + @Test + @Ignore("Test is failing!") + public void testMerge() { + + PostDTO postDTO = getPostDTO(); + + Post _post = doInJPA(entityManager -> { + //second find and copy from dto + Post post = entityManager.find(Post.class, 1L); + BeanUtils.copyProperties(postDTO, post); + + // find posts by category + List posts = entityManager.createQuery(""" + select p + from Post p + join p.categories c + where c.id = :categoryId + """, Post.class) + .setParameter("categoryId", post.categories.iterator().next().id) + .getResultList(); + + return entityManager.find(Post.class, 1L); + }); + + doInJPA(entityManager -> { + // update post + BeanUtils.copyProperties(postDTO, _post); + update(entityManager, _post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertEquals(2, post.getTags().size()); + assertEquals(3, post.getComments().size()); + assertEquals(2, post.getCategories().size()); + }); + } + + private PostDTO getPostDTO() { + Post post = doInJPA(entityManager -> { + return entityManager.find(Post.class, 1L); + }); + + PostDTO postDTO = new PostDTO(); + postDTO.id = post.id; + postDTO.title = post.title; + postDTO.categories = post.categories; + postDTO.tags = post.tags; + postDTO.comments = new HashSet<>(); + postDTO + .addComment(new Comment().setComment("Best book on JPA and Hibernate!").setAuthor("Alice")) + .addComment(new Comment().setComment("A must-read for every Java developer!").setAuthor("Bob")) + .addComment(new Comment().setComment("A great reference book").setAuthor("Carol")) + .addTag(new Tag().setName("JPA").setAuthor("Alice")) + .addTag(new Tag().setName("Hibernate").setAuthor("Alice")); + return postDTO; + } + + private Object update(EntityManager entityManager, Object object) { + return entityManager.merge(object); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "post_comment", + joinColumns = @JoinColumn(name = "post_id") + ) + private List comments = new ArrayList<>(); + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "post_tag", + joinColumns = @JoinColumn(name = "post_id") + ) + private Set tags = new HashSet<>(); + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "post_categories", + joinColumns = @JoinColumn(name = "category_id"), + inverseJoinColumns = @JoinColumn(name = "post_id") + ) + private Set categories = new HashSet<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public void setComments(List comments) { + this.comments = comments; + } + + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public Set getCategories() { + return categories; + } + + public void setCategories(Set categories) { + this.categories = categories; + } + + public Post addComment(Comment comment) { + comments.add(comment); + return this; + } + + public Post addTag(Tag tag) { + tags.add(tag); + return this; + } + + public Post addCategory(PostCategory category) { + categories.add(category); + return this; + } + } + + public static class PostDTO { + + private Long id; + + private String title; + + private Set comments = new HashSet<>(); + + private Set tags = new HashSet<>(); + + private Set categories = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Set getComments() { + return comments; + } + + public void setComments(Set comments) { + this.comments = comments; + } + + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public Set getCategories() { + return categories; + } + + public void setCategories(Set categories) { + this.categories = categories; + } + + public PostDTO addComment(Comment comment) { + comments.add(comment); + return this; + } + + public PostDTO addTag(Tag tag) { + tags.add(tag); + return this; + } + + public PostDTO addCategory(PostCategory category) { + categories.add(category); + return this; + } + } + + @Entity(name = "PostCategory") + @Table(name = "post_category") + public static class PostCategory { + + @Id + @GeneratedValue + private Long id; + + private String category; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCategory() { + return category; + } + + public PostCategory setCategory(String category) { + this.category = category; + return this; + } + } + + @Embeddable + public static class Comment implements Serializable { + + private String comment; + + private String author; + + public String getComment() { + return comment; + } + + public Comment setComment(String comment) { + this.comment = comment; + return this; + } + + public String getAuthor() { + return author; + } + + public Comment setAuthor(String author) { + this.author = author; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Comment)) return false; + Comment comment1 = (Comment) o; + return comment.equals(comment1.comment) && + Objects.equals(author, comment1.author); + } + + @Override + public int hashCode() { + return Objects.hash(comment, author); + } + } + + @Embeddable + public static class Tag implements Serializable { + + private String name; + + private String author; + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + + public String getAuthor() { + return author; + } + + public Tag setAuthor(String author) { + this.author = author; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Tag)) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name) && + Objects.equals(author, tag.author); + } + + @Override + public int hashCode() { + return Objects.hash(name, author); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionListOrderColumnTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionListOrderColumnTest.java new file mode 100644 index 000000000..5432dcd8b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionListOrderColumnTest.java @@ -0,0 +1,86 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class ElementCollectionListOrderColumnTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + post.getComments().add("My first review"); + post.getComments().add("My second review"); + post.getComments().add("My third review"); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + LOGGER.info("Remove tail"); + post.getComments().remove(2); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + LOGGER.info("Remove head"); + post.getComments().remove(0); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @ElementCollection + @OrderColumn(name = "position") + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionListTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionListTest.java new file mode 100644 index 000000000..265175a1a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionListTest.java @@ -0,0 +1,144 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class ElementCollectionListTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment("Best book on JPA and Hibernate!") + .addComment("A must-read for every Java developer!") + .addComment("A great reference book") + ); + }); + } + + @Test + public void testRemoveTail() { + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + List comments = post.getComments(); + comments.remove(comments.size() - 1); + }); + } + + @Test + public void testFetchAndRemoveTail() { + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + post.getComments().remove(post.getComments().size() - 1); + }); + } + + @Test + public void testFetchAndRemoveHead() { + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + post.getComments().remove(0); + }); + } + + @Test + public void testFetchAndRemove() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + List comments = post.getComments(); + comments.remove(comments.iterator().next()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @ElementCollection + @JoinTable(name = "post_comments") + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(String comment) { + comments.add(comment); + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/ElementCollectionNestedTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionNestedTest.java similarity index 91% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/ElementCollectionNestedTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionNestedTest.java index dc0c81311..1fda95b66 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/ElementCollectionNestedTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionNestedTest.java @@ -1,10 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionSetMergeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionSetMergeTest.java new file mode 100644 index 000000000..4e867a9fd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionSetMergeTest.java @@ -0,0 +1,454 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; +import org.springframework.beans.BeanUtils; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ElementCollectionSetMergeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostCategory.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + PostCategory category1 = new PostCategory() + .setCategory("Post"); + PostCategory category2 = new PostCategory() + .setCategory("Archive"); + entityManager.persist(category1); + entityManager.persist(category2); + + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addCategory(category1) + .addCategory(category2) + .addComment(new Comment().setComment("firstComment")) + ); + }); + } + + @Test + public void testMerge() { + + PostDTO postDTO = getPostDTO(); + + doInJPA(entityManager -> { + + EntityManager entityManager1 = entityManagerFactory().createEntityManager(); + //second find and copy from dto + Post post = entityManager1.find(Post.class, 1L); + BeanUtils.copyProperties(postDTO, post); + + entityManager1.close(); + + // find posts by category + List posts = entityManager.createQuery(""" + select p + from Post p + join p.categories c + where c.id = :categoryId + """, Post.class) + .setParameter("categoryId", post.categories.iterator().next().id) + .getResultList(); + + // update post + post = entityManager.find(Post.class, 1L); + BeanUtils.copyProperties(postDTO, post); + Object mergedEntity = update(entityManager, post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertEquals(2, post.getTags().size()); + assertEquals(3, post.getComments().size()); + assertEquals(2, post.getCategories().size()); + }); + } + + @Test + public void testMergeDetach() { + + PostDTO postDTO = getPostDTO(); + + doInJPA(entityManager -> { + + //second find and copy from dto + Post post = entityManager.find(Post.class, 1L); + entityManager.detach(post); + BeanUtils.copyProperties(postDTO, post); + + // find posts by category + List posts = entityManager.createQuery(""" + select p + from Post p + join p.categories c + where c.id = :categoryId + """, Post.class) + .setParameter("categoryId", post.categories.iterator().next().id) + .getResultList(); + + // update post + post = entityManager.find(Post.class, 1L); + BeanUtils.copyProperties(postDTO, post); + Object mergedEntity = update(entityManager, post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertEquals(2, post.getTags().size()); + assertEquals(3, post.getComments().size()); + assertEquals(2, post.getCategories().size()); + }); + } + + @Test + public void testMergeWorkingReorder() { + + PostDTO postDTO = getPostDTO(); + + + doInJPA(entityManager -> { + + //second find and copy from dto + Post post = entityManager.find(Post.class, 1L); + + // find posts by category + List posts = entityManager.createQuery(""" + select p + from Post p + join p.categories c + where c.id = :categoryId + """, Post.class) + .setParameter("categoryId", post.categories.iterator().next().id) + .getResultList(); + + BeanUtils.copyProperties(postDTO, post); + + // update post + post = entityManager.find(Post.class, 1L); + BeanUtils.copyProperties(postDTO, post); + Object mergedEntity = update(entityManager, post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertEquals(2, post.getTags().size()); + assertEquals(3, post.getComments().size()); + assertEquals(2, post.getCategories().size()); + }); + } + + private PostDTO getPostDTO() { + Post post = doInJPA(entityManager -> { + return entityManager.find(Post.class, 1L); + }); + + PostDTO postDTO = new PostDTO(); + postDTO.id = post.id; + postDTO.title = post.title; + postDTO.categories = post.categories; + postDTO.tags = post.tags; + postDTO.comments = new HashSet<>(); + postDTO.addComment(new Comment().setComment("Best book on JPA and Hibernate!").setAuthor("Alice")) + .addComment(new Comment().setComment("A must-read for every Java developer!").setAuthor("Bob")) + .addComment(new Comment().setComment("A great reference book").setAuthor("Carol")) + .addTag(new Tag().setName("JPA").setAuthor("Alice")) + .addTag(new Tag().setName("Hibernate").setAuthor("Alice")); + return postDTO; + } + + private Object update(EntityManager entityManager, Object object) { + Object mergedEntity = entityManager.merge(object); + entityManager.detach(object); + Object merged2 = entityManager.merge(mergedEntity); + return merged2; + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "post_comment", + joinColumns = @JoinColumn(name = "post_id") + ) + private Set comments = new HashSet<>(); + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "post_tag", + joinColumns = @JoinColumn(name = "post_id") + ) + private Set tags = new HashSet<>(); + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "post_categories", + joinColumns = @JoinColumn(name = "category_id"), + inverseJoinColumns = @JoinColumn(name = "post_id") + ) + private Set categories = new HashSet<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Set getComments() { + return comments; + } + + public void setComments(Set comments) { + this.comments = comments; + } + + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public Set getCategories() { + return categories; + } + + public void setCategories(Set categories) { + this.categories = categories; + } + + public Post addComment(Comment comment) { + comments.add(comment); + return this; + } + + public Post addTag(Tag tag) { + tags.add(tag); + return this; + } + + public Post addCategory(PostCategory category) { + categories.add(category); + return this; + } + } + + public static class PostDTO { + + private Long id; + + private String title; + + private Set comments = new HashSet<>(); + + private Set tags = new HashSet<>(); + + private Set categories = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Set getComments() { + return comments; + } + + public void setComments(Set comments) { + this.comments = comments; + } + + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public Set getCategories() { + return categories; + } + + public void setCategories(Set categories) { + this.categories = categories; + } + + public PostDTO addComment(Comment comment) { + comments.add(comment); + return this; + } + + public PostDTO addTag(Tag tag) { + tags.add(tag); + return this; + } + + public PostDTO addCategory(PostCategory category) { + categories.add(category); + return this; + } + } + + @Entity(name = "PostCategory") + @Table(name = "post_category") + public static class PostCategory { + + @Id + @GeneratedValue + private Long id; + + private String category; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCategory() { + return category; + } + + public PostCategory setCategory(String category) { + this.category = category; + return this; + } + } + + @Embeddable + public static class Comment implements Serializable { + + private String comment; + + private String author; + + public String getComment() { + return comment; + } + + public Comment setComment(String comment) { + this.comment = comment; + return this; + } + + public String getAuthor() { + return author; + } + + public Comment setAuthor(String author) { + this.author = author; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Comment)) return false; + Comment comment1 = (Comment) o; + return Objects.equals(comment, comment1.comment) && + Objects.equals(author, comment1.author); + } + + @Override + public int hashCode() { + return Objects.hash(comment, author); + } + } + + @Embeddable + public static class Tag implements Serializable { + + private String name; + + private String author; + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + + public String getAuthor() { + return author; + } + + public Tag setAuthor(String author) { + this.author = author; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Tag)) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name) && + Objects.equals(author, tag.author); + } + + @Override + public int hashCode() { + return Objects.hash(name, author); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionSetTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionSetTest.java new file mode 100644 index 000000000..2d0638971 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionSetTest.java @@ -0,0 +1,111 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author Vlad Mihalcea + */ +public class ElementCollectionSetTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment("Best book on JPA and Hibernate!") + .addComment("A must-read for every Java developer!") + .addComment("A great reference book") + ); + }); + } + + @Test + public void testFetchAndRemove() { + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + post.getComments().remove(post.getComments().iterator().next()); + }); + } + + @Test + public void testRemove() { + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + Set comments = post.getComments(); + comments.remove(comments.iterator().next()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @ElementCollection + @JoinTable(name = "post_comments") + private Set comments = new HashSet<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Set getComments() { + return comments; + } + + public Post addComment(String comment) { + comments.add(comment); + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionSortedSetMergeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionSortedSetMergeTest.java new file mode 100644 index 000000000..c88379e2a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionSortedSetMergeTest.java @@ -0,0 +1,383 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.annotations.SortComparator; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.beans.BeanUtils; + +import java.io.Serializable; +import java.util.*; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ElementCollectionSortedSetMergeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostCategory.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + PostCategory category1 = new PostCategory() + .setCategory("Post"); + PostCategory category2 = new PostCategory() + .setCategory("Archive"); + entityManager.persist(category1); + entityManager.persist(category2); + + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addCategory(category1) + .addCategory(category2) + .addComment(new Comment().setComment("firstComment")) + ); + }); + } + + @Test + @Ignore("Test is failing!") + public void testMerge() { + PostDTO postDTO = getPostDTO(); + + doInJPA(entityManager -> { + + //second find and copy from dto + Post post = entityManager.find(Post.class, 1L); + BeanUtils.copyProperties(postDTO, post); + + // find posts by category + List posts = entityManager.createQuery(""" + select p + from Post p + join p.categories c + where c.id = :categoryId + """, Post.class) + .setParameter("categoryId", post.categories.iterator().next().id) + .getResultList(); + + // update post + post = entityManager.find(Post.class, 1L); + BeanUtils.copyProperties(postDTO, post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertEquals(2, post.getTags().size()); + assertEquals(3, post.getComments().size()); + assertEquals(2, post.getCategories().size()); + }); + } + + private PostDTO getPostDTO() { + Post post = doInJPA(entityManager -> { + return entityManager.find(Post.class, 1L); + }); + + PostDTO postDTO = new PostDTO(); + postDTO.id = post.id; + postDTO.title = post.title; + postDTO.categories = post.categories; + postDTO.tags = post.tags; + postDTO.addComment(new Comment().setComment("Best book on JPA and Hibernate!").setAuthor("Alice")) + .addComment(new Comment().setComment("A must-read for every Java developer!").setAuthor("Bob")) + .addComment(new Comment().setComment("A great reference book").setAuthor("Carol")) + .addTag(new Tag().setName("JPA").setAuthor("Alice")) + .addTag(new Tag().setName("Hibernate").setAuthor("Alice")); + return postDTO; + } + + private Object update(EntityManager entityManager, Object object) { + return entityManager.merge(object); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "post_comment", + joinColumns = @JoinColumn(name = "post_id") + ) + @SortComparator(CommentComparator.class) + private SortedSet comments = new TreeSet<>(); + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "post_tag", + joinColumns = @JoinColumn(name = "post_id") + ) + private Set tags = new HashSet<>(); + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "post_categories", + joinColumns = @JoinColumn(name = "category_id"), + inverseJoinColumns = @JoinColumn(name = "post_id") + ) + private Set categories = new HashSet<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Set getComments() { + return comments; + } + + public void setComments(SortedSet comments) { + this.comments = comments; + } + + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public Set getCategories() { + return categories; + } + + public void setCategories(Set categories) { + this.categories = categories; + } + + public Post addComment(Comment comment) { + comments.add(comment); + return this; + } + + public Post addTag(Tag tag) { + tags.add(tag); + return this; + } + + public Post addCategory(PostCategory category) { + categories.add(category); + return this; + } + } + + public static class PostDTO { + + private Long id; + + private String title; + + private SortedSet comments = new TreeSet<>(); + + private Set tags = new HashSet<>(); + + private Set categories = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public SortedSet getComments() { + return comments; + } + + public void setComments(SortedSet comments) { + this.comments = comments; + } + + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public Set getCategories() { + return categories; + } + + public void setCategories(Set categories) { + this.categories = categories; + } + + public PostDTO addComment(Comment comment) { + comments.add(comment); + return this; + } + + public PostDTO addTag(Tag tag) { + tags.add(tag); + return this; + } + + public PostDTO addCategory(PostCategory category) { + categories.add(category); + return this; + } + } + + @Entity(name = "PostCategory") + @Table(name = "post_category") + public static class PostCategory { + + @Id + @GeneratedValue + private Long id; + + private String category; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCategory() { + return category; + } + + public PostCategory setCategory(String category) { + this.category = category; + return this; + } + } + + @Embeddable + public static class Comment implements Serializable, Comparable { + + private String comment; + + private String author; + + public String getComment() { + return comment; + } + + public Comment setComment(String comment) { + this.comment = comment; + return this; + } + + public String getAuthor() { + return author; + } + + public Comment setAuthor(String author) { + this.author = author; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Comment)) return false; + Comment comment1 = (Comment) o; + return Objects.equals(comment, comment1.comment) && + Objects.equals(author, comment1.author); + } + + @Override + public int hashCode() { + return Objects.hash(comment, author); + } + + @Override + public int compareTo(Object o) { + return 1; + } + } + + @Embeddable + public static class Tag implements Serializable { + + private String name; + + private String author; + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + + public String getAuthor() { + return author; + } + + public Tag setAuthor(String author) { + this.author = author; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Tag)) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name) && + Objects.equals(author, tag.author); + } + + @Override + public int hashCode() { + return Objects.hash(name, author); + } + } + + public static class CommentComparator implements Comparator { + @Override + public int compare(ElementCollectionSortedSetMergeTest.Comment o1, ElementCollectionSortedSetMergeTest.Comment o2) { + return 0; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/ElementCollectionWithCollectionTableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionWithCollectionTableTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/ElementCollectionWithCollectionTableTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionWithCollectionTableTest.java index bc2403405..243912059 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/ElementCollectionWithCollectionTableTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ElementCollectionWithCollectionTableTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/LazyToOneFalseTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/LazyToOneFalseTest.java new file mode 100644 index 000000000..90be2a879 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/LazyToOneFalseTest.java @@ -0,0 +1,116 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class LazyToOneFalseTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Test + public void testLazyLoadingNoProxy() { + final Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence, 1st Part"); + + doInJPA(entityManager -> { + entityManager.persist(post); + + entityManager.persist( + new PostComment() + .setId(1L) + .setReview("Amazing!") + .setPost(post) + ); + }); + + PostComment comment = doInJPA(entityManager -> { + return entityManager.find(PostComment.class, 1L); + }); + + assertNotNull(comment.getPost()); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + //@LazyToOne(LazyToOneOption.FALSE) + @JoinColumn(name = "post_id") + private Post post; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/LazyToOneNoProxyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/LazyToOneNoProxyTest.java new file mode 100644 index 000000000..112046e06 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/LazyToOneNoProxyTest.java @@ -0,0 +1,169 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.LazyToOne; +import org.hibernate.annotations.LazyToOneOption; +import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.persistence.*; +import java.util.Date; + +import static junit.framework.TestCase.fail; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +@RunWith(BytecodeEnhancerRunner.class) +public class LazyToOneNoProxyTest extends AbstractTest { + + //Needed as otherwise we get a No unique field [LOGGER] error + private final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostDetails.class + }; + } + + @Test + public void testLazyLoadingNoProxy() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence, 1st Part") + .setDetails( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + ) + ); + }); + + Post post = doInJPA(entityManager -> { + return entityManager.find(Post.class, 1L); + }); + + try { + assertNotNull(post.getDetails().getCreatedOn()); + + fail("Should throw LazyInitializationException"); + } catch (Exception expected) { + LOGGER.info("The @OneToOne association was fetched lazily", expected); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToOne( + mappedBy = "post", + fetch = FetchType.LAZY, + cascade = CascadeType.ALL + ) + @LazyToOne(LazyToOneOption.NO_PROXY) + private PostDetails details; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/LazyToOneProxyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/LazyToOneProxyTest.java new file mode 100644 index 000000000..f130a890d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/LazyToOneProxyTest.java @@ -0,0 +1,133 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.LazyToOne; +import org.hibernate.annotations.LazyToOneOption; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Date; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class LazyToOneProxyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostDetails.class + }; + } + + @Test + public void testLazyLoadingNoProxy() { + final Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence, 1st Part"); + + doInJPA(entityManager -> { + entityManager.persist(post); + + entityManager.persist( + new PostDetails() + .setPost(post) + .setCreatedBy("Vlad Mihalcea") + ); + }); + + PostDetails details = doInJPA(entityManager -> { + return entityManager.find(PostDetails.class, post.getId()); + }); + + assertNotNull(details.getPost()); + LOGGER.info("PostDetail Proxy class: {}", details.getPost().getClass()); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @LazyToOne(LazyToOneOption.PROXY) + @MapsId + @JoinColumn(name = "id") + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ManyToOneJoinColumnNonPKTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ManyToOneJoinColumnNonPKTest.java new file mode 100644 index 000000000..8ee0919b9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ManyToOneJoinColumnNonPKTest.java @@ -0,0 +1,188 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import java.io.Serializable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import org.hibernate.annotations.NaturalId; + +import org.junit.Test; + +import com.vladmihalcea.hpjp.util.AbstractTest; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ManyToOneJoinColumnNonPKTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Book.class, + Publication.class, + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setTitle( "High-Performance Java Persistence" ); + book.setAuthor( "Vlad Mihalcea" ); + book.setIsbn( "978-9730228236" ); + entityManager.persist(book); + + Publication amazonUs = new Publication(); + amazonUs.setPublisher( "amazon.com" ); + amazonUs.setBook( book ); + amazonUs.setPriceCents( 4599 ); + amazonUs.setCurrency( "$" ); + entityManager.persist( amazonUs ); + + Publication amazonUk = new Publication(); + amazonUk.setPublisher( "amazon.co.uk" ); + amazonUk.setBook( book ); + amazonUk.setPriceCents( 3545 ); + amazonUk.setCurrency( "&" ); + entityManager.persist( amazonUk ); + }); + doInJPA(entityManager -> { + Publication publication = entityManager.createQuery( + "select p " + + "from Publication p " + + "join fetch p.book b " + + "where " + + " b.isbn = :isbn and " + + " p.currency = :currency", Publication.class) + .setParameter( "isbn", "978-9730228236" ) + .setParameter( "currency", "&" ) + .getSingleResult(); + + assertEquals( + "amazon.co.uk", + publication.getPublisher() + ); + + assertEquals( + "High-Performance Java Persistence", + publication.getBook().getTitle() + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book implements Serializable { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String author; + + @NaturalId + private String isbn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + } + + @Entity(name = "Publication") + @Table(name = "publication") + public static class Publication { + + @Id + @GeneratedValue + private Long id; + + private String publisher; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "isbn", referencedColumnName = "isbn") + private Book book; + + @Column(name = "price_in_cents", nullable = false) + private Integer priceCents; + + private String currency; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getPublisher() { + return publisher; + } + + public void setPublisher(String publisher) { + this.publisher = publisher; + } + + public Integer getPriceCents() { + return priceCents; + } + + public void setPriceCents(Integer priceCents) { + this.priceCents = priceCents; + } + + public Book getBook() { + return book; + } + + public void setBook(Book book) { + this.book = book; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ManyToOneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ManyToOneTest.java new file mode 100644 index 000000000..88d846b6f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/ManyToOneTest.java @@ -0,0 +1,322 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ManyToOneTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("First post"); + entityManager.persist(post); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + entityManager.persist( + new PostComment() + .setId(1L) + .setReview("My first review") + .setPost(post) + ); + }); + + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + + comment.setPost(null); + }); + + doInJPA(entityManager -> { + PostComment comment = entityManager.getReference(PostComment.class, 1L); + + entityManager.remove(comment); + }); + } + + @Test + public void testThreePostComments() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("First post"); + entityManager.persist(post); + }); + doInJPA(entityManager -> { + Post post = entityManager.getReference(Post.class, 1L); + + entityManager.persist( + new PostComment() + .setId(1L) + .setReview("My first review") + .setPost(post) + ); + + entityManager.persist( + new PostComment() + .setId(2L) + .setReview("My second review") + .setPost(post) + ); + + entityManager.persist( + new PostComment() + .setId(3L) + .setReview("My third review") + .setPost(post) + ); + }); + + doInJPA(entityManager -> { + PostComment comment1 = entityManager.getReference(PostComment.class, 2L); + + entityManager.remove(comment1); + }); + + doInJPA(entityManager -> { + List comments = entityManager.createQuery( + "select pc " + + "from PostComment pc " + + "where pc.post.id = :postId", PostComment.class) + .setParameter("postId", 1L) + .getResultList(); + + assertEquals(2, comments.size()); + }); + } + + @Test + public void testPersistAndQuery() { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + PostComment comment = new PostComment(); + comment.setId(1L); + comment.setReview("Amazing book!"); + comment.setPost(post); + + doInJPA(entityManager -> { + entityManager.persist(post); + entityManager.persist(comment); + }); + + doInJPA(entityManager -> { + PostComment postComment = entityManager + .createQuery( + "select pc " + + "from PostComment pc " + + "join fetch pc.post " + + "where pc.id = :id", PostComment.class) + .setParameter("id", comment.getId()) + .getSingleResult(); + + assertEquals("High-Performance Java Persistence", postComment.getPost().getTitle()); + assertEquals("Amazing book!", postComment.getReview()); + + }); + } + + @Test + public void testPersistWithFind() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + entityManager.persist( + new PostComment() + .setId(1L) + .setReview("Amazing book!") + .setPost(post) + ); + }); + } + + @Test + public void testPersistWithProxy() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.getReference(Post.class, 1L); + + entityManager.persist( + new PostComment() + .setId(1L) + .setReview("Amazing book!") + .setPost(post) + ); + }); + } + + @Test + public void testFetchEntityWithFind() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.getReference(Post.class, 1L); + + entityManager.persist( + new PostComment() + .setId(1L) + .setReview("Amazing book!") + .setPost(post) + ); + }); + + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + + LOGGER.info( + "The post '{}' got the following comment '{}'", + comment.getPost().getTitle(), + comment.getReview() + ); + }); + } + + @Test + public void testFetchEntityWithJoinFetch() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.getReference(Post.class, 1L); + + entityManager.persist( + new PostComment() + .setId(1L) + .setReview("Amazing book!") + .setPost(post) + ); + }); + + doInJPA(entityManager -> { + PostComment comment = entityManager.createQuery(""" + select pc + from PostComment pc + join fetch pc.post + where pc.id = :id + """, PostComment.class) + .setParameter("id", 1L) + .getSingleResult(); + + LOGGER.info( + "The post '{}' got the following comment '{}'", + comment.getPost().getTitle(), + comment.getReview() + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/MultiLevelOneToManyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/MultiLevelOneToManyTest.java new file mode 100644 index 000000000..402529e84 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/MultiLevelOneToManyTest.java @@ -0,0 +1,224 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MultiLevelOneToManyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + PostCommentTag.class, + }; + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post = new Post("First post"); + + PostComment comment1 = new PostComment("My first review"); + PostComment comment2 = new PostComment("My second review"); + PostComment comment3 = new PostComment("My third review"); + + post.addComment( + comment1 + ); + post.addComment( + comment2 + ); + post.addComment( + comment3 + ); + + comment1.addTag(new PostCommentTag("Java")); + + comment2.addTag(new PostCommentTag("Java")); + comment2.addTag(new PostCommentTag("JPA")); + + comment3.addTag(new PostCommentTag("Java")); + comment3.addTag(new PostCommentTag("JPA")); + comment3.addTag(new PostCommentTag("Hibernate")); + + entityManager.persist(post); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertEquals(1, post.getComments().get(0).getTags().size()); + assertEquals(2, post.getComments().get(1).getTags().size()); + assertEquals(3, post.getComments().get(2).getTags().size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Post() { + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true) + private List tags = new ArrayList<>(); + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public List getTags() { + return tags; + } + + public void addTag(PostCommentTag tag) { + tags.add(tag); + tag.setComment(this); + } + + public void removeComment(PostCommentTag tag) { + tags.remove(tag); + tag.setComment(null); + } + } + + @Entity(name = "PostCommentTag") + @Table(name = "post_comment_tag") + public static class PostCommentTag { + + @Id + @GeneratedValue + private Long id; + + private String tag; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_comment_id") + private PostComment comment; + + public PostCommentTag() { + } + + public PostCommentTag(String tag) { + this.tag = tag; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + public PostComment getComment() { + return comment; + } + + public void setComment(PostComment comment) { + this.comment = comment; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneIdTest.java new file mode 100644 index 000000000..234f7c135 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneIdTest.java @@ -0,0 +1,149 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.util.Date; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class OneToOneIdTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + }; + } + + @Test + @Ignore + public void testLifecycle() { + Post _post = doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("First post"); + + PostDetails details = new PostDetails(); + details.setCreatedBy("John Doe"); + + post.setDetails(details); + entityManager.persist(post); + + return post; + }); + + _post.setTitle("Second post"); + _post.getDetails().setCreatedBy("Vlad Mihalcea"); + + doInJPA(entityManager -> { + Post post = entityManager.merge(_post); + }); + + doInJPA(entityManager -> { + PostDetails id = new PostDetails(); + id.setPost(_post); + + PostDetails details = entityManager.find(PostDetails.class, id); + assertEquals("Vlad Mihalcea", details.getCreatedBy()); + assertEquals("Second post", details.getPost().getTitle()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post implements Serializable { + + @Id + private Long id; + + private String title; + + @OneToOne(mappedBy = "post", cascade = CascadeType.ALL) + private PostDetails details; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public PostDetails getDetails() { + return details; + } + + public void setDetails(PostDetails details) { + this.details = details; + this.details.setPost(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Post)) return false; + return id != null && id.equals(((Post) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails implements Serializable { + + @Id + @OneToOne + private Post post; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "created_by") + private String createdBy; + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneInsertableUpdatableFalseTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneInsertableUpdatableFalseTest.java new file mode 100644 index 000000000..da123b878 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneInsertableUpdatableFalseTest.java @@ -0,0 +1,132 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Date; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class OneToOneInsertableUpdatableFalseTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + }; + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post = new Post("First post"); + entityManager.persist(post); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + PostDetails details = new PostDetails("John Doe"); + details.setPost(post); + entityManager.persist(details); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + PostDetails details = entityManager.find(PostDetails.class, post.getId()); + assertNotNull(details); + + entityManager.flush(); + details.setPost(null); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + public Post() {} + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + @Column(name = "post_id") + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + @OneToOne + @JoinColumn(name = "post_id", insertable = false, updatable = false) + private Post post; + + public PostDetails() {} + + public PostDetails(String createdBy) { + createdOn = new Date(); + this.createdBy = createdBy; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Date getCreatedOn() { + return createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + if (post != null) { + this.id = post.getId(); + } + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneJoinColumnWithoutMapsIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneJoinColumnWithoutMapsIdTest.java new file mode 100644 index 000000000..c0f6857c0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneJoinColumnWithoutMapsIdTest.java @@ -0,0 +1,139 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.Date; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class OneToOneJoinColumnWithoutMapsIdTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post().setTitle("First post") + ); + }); + doInJPA(entityManager -> { + entityManager.persist( + new PostDetails() + .setCreatedBy("John Doe") + .setPost(entityManager.getReference(Post.class, 1L)) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + PostDetails details = entityManager.find(PostDetails.class, post.getId()); + + assertEquals(details.getId(), post.getId()); + assertEquals(details.getPost().getId(), post.getId()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "id") + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + if (post != null) { + this.id = post.getId(); + } + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneMapsIdJoinColumnTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneMapsIdJoinColumnTest.java new file mode 100644 index 000000000..1b09bdd76 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneMapsIdJoinColumnTest.java @@ -0,0 +1,135 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Date; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class OneToOneMapsIdJoinColumnTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post = new Post("First post"); + entityManager.persist(post); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + PostDetails details = new PostDetails("John Doe"); + details.setPost(post); + entityManager.persist(details); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + PostDetails details = entityManager.find(PostDetails.class, post.getId()); + assertNotNull(details); + + entityManager.flush(); + details.setPost(null); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + public Post() {} + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Post post; + + public PostDetails() {} + + public PostDetails(String createdBy) { + createdOn = new Date(); + this.createdBy = createdBy; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Date getCreatedOn() { + return createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/OneToOneMapsIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneMapsIdTest.java similarity index 88% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/OneToOneMapsIdTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneMapsIdTest.java index 9fb658424..3a59b45fe 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/OneToOneMapsIdTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/OneToOneMapsIdTest.java @@ -1,9 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Date; import static org.junit.Assert.assertNotNull; @@ -11,7 +12,7 @@ /** * @author Vlad Mihalcea */ -public class OneToOneMapsIdTest extends AbstractMySQLIntegrationTest { +public class OneToOneMapsIdTest extends AbstractTest { @Override protected Class[] entities() { @@ -21,6 +22,11 @@ protected Class[] entities() { }; } + @Override + protected Database database() { + return Database.MYSQL; + } + @Test public void testLifecycle() { doInJPA(entityManager -> { diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/TwoMapsIdsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/TwoMapsIdsTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/TwoMapsIdsTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/TwoMapsIdsTest.java index 145d3c7d1..776daa0c4 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/TwoMapsIdsTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/TwoMapsIdsTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import static org.junit.Assert.assertEquals; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalManyAsOneToManyExtraColumnsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalManyAsOneToManyExtraColumnsTest.java new file mode 100644 index 000000000..6f5cc5042 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalManyAsOneToManyExtraColumnsTest.java @@ -0,0 +1,334 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import org.hibernate.Session; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NaturalIdCache; + +import org.junit.Test; + +import com.vladmihalcea.hpjp.util.AbstractTest; + +/** + * @author Vlad Mihalcea + */ +public class UnidirectionalManyAsOneToManyExtraColumnsTest + extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Tag.class, + PostTag.class + }; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + return properties; + } + + @Test + public void testLifecycle() { + + doInJPA(entityManager -> { + Tag misc = new Tag("Misc"); + Tag jdbc = new Tag("JDBC"); + Tag hibernate = new Tag("Hibernate"); + Tag jooq = new Tag("jOOQ"); + + entityManager.persist( misc ); + entityManager.persist( jdbc ); + entityManager.persist( hibernate ); + entityManager.persist( jooq ); + }); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap( Session.class ); + + Tag misc = session.bySimpleNaturalId(Tag.class).load( "Misc" ); + Tag jdbc = session.bySimpleNaturalId(Tag.class).load( "JDBC" ); + Tag hibernate = session.bySimpleNaturalId(Tag.class).load( "Hibernate" ); + Tag jooq = session.bySimpleNaturalId(Tag.class).load( "jOOQ" ); + + Post hpjp1 = new Post("High-Performance Java Persistence 1st edition"); + hpjp1.setId(1L); + + hpjp1.addTag(jdbc); + hpjp1.addTag(hibernate); + hpjp1.addTag(jooq); + hpjp1.addTag(misc); + + entityManager.persist(hpjp1); + + Post hpjp2 = new Post("High-Performance Java Persistence 2nd edition"); + hpjp2.setId(2L); + + hpjp2.addTag(jdbc); + hpjp2.addTag(hibernate); + hpjp2.addTag(jooq); + + entityManager.persist(hpjp2); + }); + + doInJPA(entityManager -> { + Tag misc = entityManager.unwrap( Session.class ) + .bySimpleNaturalId(Tag.class) + .load( "Misc" ); + + Post post = entityManager.createQuery( + "select p " + + "from Post p " + + "join fetch p.tags pt " + + "join fetch pt.tag " + + "where p.id = :postId", Post.class) + .setParameter( "postId", 1L ) + .getSingleResult(); + + post.removeTag( misc ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List tags = new ArrayList<>(); + + public Post() { + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public List getTags() { + return tags; + } + + public void addTag(Tag tag) { + PostTag postTag = new PostTag(this, tag); + tags.add(postTag); + } + + public void removeTag(Tag tag) { + for (Iterator iterator = tags.iterator(); iterator.hasNext(); ) { + PostTag postTag = iterator.next(); + if (postTag.getPost().equals(this) && + postTag.getTag().equals(tag)) { + iterator.remove(); + postTag.setPost(null); + postTag.setTag(null); + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Post post = (Post) o; + return Objects.equals(title, post.title); + } + + @Override + public int hashCode() { + return Objects.hash(title); + } + } + + @Embeddable + public static class PostTagId + implements Serializable { + + @Column(name = "post_id") + private Long postId; + + @Column(name = "tag_id") + private Long tagId; + + private PostTagId() {} + + public PostTagId(Long postId, Long tagId) { + this.postId = postId; + this.tagId = tagId; + } + + public Long getPostId() { + return postId; + } + + public Long getTagId() { + return tagId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTagId that = (PostTagId) o; + return Objects.equals(postId, that.getPostId()) && + Objects.equals(tagId, that.getTagId()); + } + + @Override + public int hashCode() { + return Objects.hash(postId, tagId); + } + } + + @Entity(name = "PostTag") + @Table(name = "post_tag") + public static class PostTag { + + @EmbeddedId + private PostTagId id; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("postId") + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("tagId") + private Tag tag; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + private PostTag() {} + + public PostTag(Post post, Tag tag) { + this.post = post; + this.tag = tag; + this.id = new PostTagId(post.getId(), tag.getId()); + } + + public PostTagId getId() { + return id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Tag getTag() { + return tag; + } + + public void setTag(Tag tag) { + this.tag = tag; + } + + public Date getCreatedOn() { + return createdOn; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTag that = (PostTag) o; + return Objects.equals(post, that.post) && + Objects.equals(tag, that.tag); + } + + @Override + public int hashCode() { + return Objects.hash(post, tag); + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + @NaturalIdCache + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + public Tag() { + } + + public Tag(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalManyToManySetTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalManyToManySetTest.java new file mode 100644 index 000000000..e1d735718 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalManyToManySetTest.java @@ -0,0 +1,163 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Vlad Mihalcea + */ +public class UnidirectionalManyToManySetTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Tag.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Tag().setName("JPA") + ); + + entityManager.persist( + new Tag().setName("Hibernate") + ); + }); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + entityManager.persist( + new Post() + .setId(1L) + .setTitle("JPA with Hibernate") + .addTag(session.bySimpleNaturalId(Tag.class).getReference("JPA")) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + ); + + entityManager.persist( + new Post() + .setId(2L) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + ); + }); + } + + @Test + public void testRemoveTagReference() { + doInJPA(entityManager -> { + Post post1 = entityManager.createQuery(""" + select p + from Post p + join fetch p.tags + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + Session session = entityManager.unwrap(Session.class); + + post1.getTags().remove(session.bySimpleNaturalId(Tag.class).getReference("JPA")); + }); + } + + @Test + public void testRemovePostEntity() { + doInJPA(entityManager -> { + LOGGER.info("Remove"); + Post post1 = entityManager.getReference(Post.class, 1L); + + entityManager.remove(post1); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private Set tags = new HashSet<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Set getTags() { + return tags; + } + + public Post addTag(Tag tag) { + tags.add(tag); + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalManyToManySortedSetTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalManyToManySortedSetTest.java new file mode 100644 index 000000000..bee079a7d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalManyToManySortedSetTest.java @@ -0,0 +1,173 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.SortNatural; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.*; + +/** + * @author Vlad Mihalcea + */ +public class UnidirectionalManyToManySortedSetTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Tag.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Tag().setName("JPA") + ); + + entityManager.persist( + new Tag().setName("Hibernate") + ); + }); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + entityManager.persist( + new Post() + .setId(1L) + .setTitle("JPA with Hibernate") + .addTag(session.bySimpleNaturalId(Tag.class).getReference("JPA")) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + ); + + entityManager.persist( + new Post() + .setId(2L) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + ); + }); + } + + @Test + public void testRemoveTagReference() { + doInJPA(entityManager -> { + Post post1 = entityManager.createQuery(""" + select p + from Post p + join fetch p.tags + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + Session session = entityManager.unwrap(Session.class); + + post1.getTags().remove(session.bySimpleNaturalId(Tag.class).getReference("JPA")); + }); + } + + @Test + public void testRemovePostEntity() { + doInJPA(entityManager -> { + LOGGER.info("Remove"); + Post post1 = entityManager.getReference(Post.class, 1L); + + entityManager.remove(post1); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + @SortNatural + private SortedSet tags = new TreeSet<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Set getTags() { + return tags; + } + + public Post addTag(Tag tag) { + tags.add(tag); + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag implements Comparable { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + + @Override + public int compareTo(Tag o) { + String otherName = o.name; + if(otherName == null) { + return this.name != null ? 1 : 0; + } + return this.name != null ? name.compareTo(o.name) : -1; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalManyToManyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalManyToManyTest.java new file mode 100644 index 000000000..a94e7167e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalManyToManyTest.java @@ -0,0 +1,163 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class UnidirectionalManyToManyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + Tag.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Tag().setName("JPA") + ); + + entityManager.persist( + new Tag().setName("Hibernate") + ); + }); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + entityManager.persist( + new Post() + .setId(1L) + .setTitle("JPA with Hibernate") + .addTag(session.bySimpleNaturalId(Tag.class).getReference("JPA")) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + ); + + entityManager.persist( + new Post() + .setId(2L) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + ); + }); + } + + @Test + public void testRemoveTagReference() { + doInJPA(entityManager -> { + Post post1 = entityManager.createQuery(""" + select p + from Post p + join fetch p.tags + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + Session session = entityManager.unwrap(Session.class); + + post1.getTags().remove(session.bySimpleNaturalId(Tag.class).getReference("JPA")); + }); + } + + @Test + public void testRemovePostEntity() { + doInJPA(entityManager -> { + LOGGER.info("Remove"); + Post post1 = entityManager.getReference(Post.class, 1L); + + entityManager.remove(post1); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getTags() { + return tags; + } + + public Post addTag(Tag tag) { + tags.add(tag); + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnAndOrderColumnTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnAndOrderColumnTest.java similarity index 88% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnAndOrderColumnTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnAndOrderColumnTest.java index a693b9cdf..2e9ccd63d 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnAndOrderColumnTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnAndOrderColumnTest.java @@ -1,19 +1,19 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; import java.util.ArrayList; import java.util.List; -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.OneToMany; -import javax.persistence.OrderColumn; -import javax.persistence.Table; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderColumn; +import jakarta.persistence.Table; import org.junit.Test; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnNotNullTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnNotNullTest.java similarity index 95% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnNotNullTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnNotNullTest.java index 076c469b8..08ba796cf 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnNotNullTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnNotNullTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnSetTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnSetTest.java similarity index 93% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnSetTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnSetTest.java index 67c0e495b..42de3199a 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnSetTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnSetTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.nio.ByteBuffer; import java.util.*; @@ -123,7 +123,7 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PostComment comment = (PostComment) o; - return Objects.equals(slug, comment.slug); + return Objects.equals(slug, comment.getSlug()); } @Override diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnTest.java new file mode 100644 index 000000000..7b0bd1dd4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyJoinColumnTest.java @@ -0,0 +1,160 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class UnidirectionalOneToManyJoinColumnTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + private boolean POST_ID_SET_NOT_NULL = false; + + @Override + protected void afterInit() { + if (POST_ID_SET_NOT_NULL) { + executeStatement("ALTER TABLE post_comment ALTER COLUMN post_id SET NOT NULL"); + } + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setReview("A must-read for every Java developer!") + ) + .addComment( + new PostComment() + .setReview("A great reference book") + ) + ); + }); + } + + @Test + public void testRemoveTail() { + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + post.getComments().remove(post.getComments().size() - 1); + }); + } + + @Test + public void testRemoveHead() { + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + post.getComments().remove(0); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "post_id") + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManySetIdEqualsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManySetIdEqualsTest.java similarity index 83% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManySetIdEqualsTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManySetIdEqualsTest.java index 9634ccc33..2a9e01965 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToManySetIdEqualsTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManySetIdEqualsTest.java @@ -1,21 +1,18 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import java.util.ArrayList; import java.util.HashSet; -import java.util.List; import java.util.Set; -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.OneToMany; -import javax.persistence.Table; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; import org.junit.Test; -import com.vladmihalcea.book.hpjp.hibernate.equality.ProperIdEqualityTest; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; /** * @author Vlad Mihalcea @@ -131,11 +128,11 @@ public void setReview(String review) { public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof PostComment )) return false; - return id != null && id.equals(((PostComment) o).id); + return id != null && id.equals(((PostComment) o).getId()); } @Override public int hashCode() { - return 31; + return getClass().hashCode(); } } } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManySetTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManySetTest.java new file mode 100644 index 000000000..b53fb18a2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManySetTest.java @@ -0,0 +1,129 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.*; + +/** + * @author Vlad Mihalcea + */ +public class UnidirectionalOneToManySetTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + }; + } + + @Test + public void testLifecycle() { + doInJPA(entityManager -> { + Post post = new Post("First post"); + + post.getComments().add(new PostComment("My first review")); + post.getComments().add(new PostComment("My second review")); + post.getComments().add(new PostComment("My third review")); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.createQuery( + "select p " + + "from Post p " + + "join fetch p.comments " + + "where p.id = :postId", Post.class) + .setParameter("postId", 1L) + .getSingleResult(); + + post.getComments().remove(post.getComments().iterator().next()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + public Post() {} + + public Post(String title) { + this.title = title; + } + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + private Set comments = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Set getComments() { + return comments; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostComment)) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyTest.java new file mode 100644 index 000000000..24425f2b5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToManyTest.java @@ -0,0 +1,154 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class UnidirectionalOneToManyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setReview("A must-read for every Java developer!") + ) + .addComment( + new PostComment() + .setReview("A great reference book") + ) + ); + }); + } + + @Test + public void testRemoveTail() { + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + post.getComments().remove(post.getComments().size() - 1); + }); + } + + @Test + public void testRemoveHead() { + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + post.getComments().remove(0); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToOneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToOneTest.java similarity index 87% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToOneTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToOneTest.java index 93a5a998d..37fa61dc7 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/association/UnidirectionalOneToOneTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOneToOneTest.java @@ -1,9 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.association; +package com.vladmihalcea.hpjp.hibernate.association; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Date; import static org.junit.Assert.assertNotNull; @@ -11,7 +13,7 @@ /** * @author Vlad Mihalcea */ -public class UnidirectionalOneToOneTest extends AbstractMySQLIntegrationTest { +public class UnidirectionalOneToOneTest extends AbstractTest { @Override protected Class[] entities() { @@ -21,6 +23,11 @@ protected Class[] entities() { }; } + @Override + protected Database database() { + return Database.POSTGRESQL; + } + @Test public void testLifecycle() { doInJPA(entityManager -> { @@ -97,7 +104,6 @@ public static class PostDetails { private String createdBy; @OneToOne - @JoinColumn(name = "post_id", unique = true) private Post post; public PostDetails() {} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOrderedOneToManyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOrderedOneToManyTest.java new file mode 100644 index 000000000..c6381b7e1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/association/UnidirectionalOrderedOneToManyTest.java @@ -0,0 +1,155 @@ +package com.vladmihalcea.hpjp.hibernate.association; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class UnidirectionalOrderedOneToManyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setReview("A must-read for every Java developer!") + ) + .addComment( + new PostComment() + .setReview("A great reference book") + ) + ); + }); + } + + @Test + public void testRemoveTail() { + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + post.getComments().remove(post.getComments().size() - 1); + }); + } + + @Test + public void testRemoveHead() { + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + post.getComments().remove(0); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @OrderColumn(name = "entry") + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/envers/EnversAuditedDefaultStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/envers/EnversAuditedDefaultStrategyTest.java new file mode 100644 index 000000000..691d9c9a0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/envers/EnversAuditedDefaultStrategyTest.java @@ -0,0 +1,113 @@ +package com.vladmihalcea.hpjp.hibernate.audit.envers; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.envers.AuditReaderFactory; +import org.hibernate.envers.Audited; +import org.hibernate.envers.query.AuditEntity; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class EnversAuditedDefaultStrategyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence 1st edition"); + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Java Persistence 2nd edition"); + }); + + doInJPA(entityManager -> { + entityManager.remove( + entityManager.getReference(Post.class, 1L) + ); + }); + + doInJPA(entityManager -> { + List posts = AuditReaderFactory.get(entityManager) + .createQuery() + .forRevisionsOfEntity(Post.class, true, true) + .add(AuditEntity.id().eq(1L)) + .getResultList(); + + assertEquals(3, posts.size()); + + for (int i = 0; i < posts.size(); i++) { + LOGGER.info("Revision {} of Post entity: {}", i + 1, posts.get(i)); + } + }); + + List revisions = doInJPA(entityManager -> { + return AuditReaderFactory.get(entityManager).getRevisions( + Post.class, 1L + ); + }); + + doInJPA(entityManager -> { + Post post = (Post) AuditReaderFactory.get(entityManager) + .createQuery() + .forEntitiesAtRevision(Post.class, revisions.get(0)) + .getSingleResult(); + + assertEquals("High-Performance Java Persistence 1st edition", post.getTitle()); + }); + + } + + @Entity(name = "Post") + @Table(name = "post") + @Audited + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public String toString() { + return "Post{" + + "id=" + id + + ", title='" + title + '\'' + + '}'; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/envers/EnversAuditedValidityStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/envers/EnversAuditedValidityStrategyTest.java new file mode 100644 index 000000000..5cf62241a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/envers/EnversAuditedValidityStrategyTest.java @@ -0,0 +1,20 @@ +package com.vladmihalcea.hpjp.hibernate.audit.envers; + +import java.util.Properties; + +import org.hibernate.envers.configuration.EnversSettings; +import org.hibernate.envers.strategy.internal.ValidityAuditStrategy; + +/** + * @author Vlad Mihalcea + */ +public class EnversAuditedValidityStrategyTest extends EnversAuditedDefaultStrategyTest { + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty( + EnversSettings.AUDIT_STRATEGY, + ValidityAuditStrategy.class.getName() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/envers/EnversBatchInsertTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/envers/EnversBatchInsertTest.java new file mode 100644 index 000000000..2028bd9eb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/envers/EnversBatchInsertTest.java @@ -0,0 +1,184 @@ +package com.vladmihalcea.hpjp.hibernate.audit.envers; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.NaturalId; +import org.hibernate.envers.AuditReaderFactory; +import org.hibernate.envers.Audited; +import org.hibernate.envers.configuration.EnversSettings; +import org.hibernate.envers.query.AuditEntity; +import org.hibernate.envers.strategy.internal.ValidityAuditStrategy; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class EnversBatchInsertTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Tag.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty( + EnversSettings.AUDIT_STRATEGY, + ValidityAuditStrategy.class.getName() + ); + properties.put("hibernate.jdbc.batch_size", "5"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + /** + * See issue https://hibernate.atlassian.net/browse/HHH-14664} + */ + @Test + public void test_HHH_14664() { + doInJPA(entityManager -> { + for (long i = 1; i <= 10; i++) { + Post post = new Post(); + post.setId(i); + post.setTitle( + String.format( + "High-Performance Java Persistence edition - %d", i + ) + ); + Tag tag = new Tag(); + tag.setName(String.format("Tag %d", i)); + post.addTag(tag); + entityManager.persist(post); + } + entityManager.flush(); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Java Persistence 2nd edition"); + }); + + doInJPA(entityManager -> { + List posts = AuditReaderFactory.get(entityManager) + .createQuery() + .forRevisionsOfEntity(Post.class, true, true) + .add(AuditEntity.id().eq(1L)) + .getResultList(); + + assertEquals(2, posts.size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @Audited + public static class Post { + + @Id + private Long id; + + private String title; + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + @Audited + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public String toString() { + return "Post{" + + "id=" + id + + ", title='" + title + '\'' + + '}'; + } + + public void addTag(Tag tag) { + tags.add(tag); + tag.getPosts().add(this); + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + @Audited + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @ManyToMany(mappedBy = "tags") + private List posts = new ArrayList<>(); + + public Tag() {} + + public Tag(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getPosts() { + return posts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/LoadEventListenerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/LoadEventListenerTest.java new file mode 100644 index 000000000..2e1b3437f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/LoadEventListenerTest.java @@ -0,0 +1,275 @@ +package com.vladmihalcea.hpjp.hibernate.audit.hibernate; + +import com.vladmihalcea.hpjp.hibernate.audit.hibernate.listener.EventListenerIntegrator; +import com.vladmihalcea.hpjp.hibernate.audit.hibernate.model.*; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.transaction.JPATransactionFunction; +import org.hibernate.integrator.spi.Integrator; +import org.junit.Test; + +import jakarta.persistence.EntityManager; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class LoadEventListenerTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + PostCommentDetails.class, + LoadEventLogEntry.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected Integrator integrator() { + return EventListenerIntegrator.INSTANCE; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + PostComment comment1 = new PostComment() + .setId(1L) + .setReview("Good") + .setPost(post); + + PostCommentDetails details1 = new PostCommentDetails() + .setComment(comment1) + .setVotes(10); + + PostComment comment2 = new PostComment() + .setId(2L) + .setReview("Excellent") + .setPost(post); + + PostCommentDetails details2 = new PostCommentDetails() + .setComment(comment2) + .setVotes(10); + + entityManager.persist(post); + entityManager.persist(comment1); + entityManager.persist(comment2); + entityManager.persist(details1); + entityManager.persist(details2); + }); + } + + @Test + public void testFindPost() { + LoggedUser.logIn("vlad@vladmihalcea.com"); + + assertTrue(getLoadEventLogEntries().isEmpty()); + + Post post = doInJPA(entityManager -> { + return entityManager.find(Post.class, 1L); + }); + + assertEquals("High-Performance Java Persistence", post.getTitle()); + + List logEntries = getLoadEventLogEntries(); + assertEquals(1, logEntries.size()); + + LoadEventLogEntry eventLogEntry = logEntries.get(0); + assertPostEntityLogEntry(post, eventLogEntry); + } + + @Test + public void testFindPostComment() { + LoggedUser.logIn("vlad@vladmihalcea.com"); + + assertTrue(getLoadEventLogEntries().isEmpty()); + + PostComment postComment = doInJPA(entityManager -> { + return entityManager.find(PostComment.class, 1L); + }); + + assertEquals("Good", postComment.getReview()); + + List logEntries = getLoadEventLogEntries(); + assertEquals(1, logEntries.size()); + + LoadEventLogEntry eventLogEntry = logEntries.get(0); + assertPostCommentEntityLogEntry(postComment, eventLogEntry); + } + + @Test + public void testQueryJoinFetch() { + LoggedUser.logIn("vlad@vladmihalcea.com"); + + PostCommentDetails postCommentDetails = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select pcd + from PostCommentDetails pcd + join fetch pcd.comment pc + join fetch pc.post p + where pcd.id = :id + """, PostCommentDetails.class) + .setParameter("id", 2L) + .getSingleResult(); + }); + + Map logEntryMap = getLoadEventLogEntryMap(); + assertEquals(3, logEntryMap.size()); + + assertPostCommentDetailsEntityLogEntry( + postCommentDetails, + logEntryMap.get(PostCommentDetails.class.getName()) + ); + + assertPostCommentEntityLogEntry( + postCommentDetails.getComment(), + logEntryMap.get(PostComment.class.getName()) + ); + + assertPostEntityLogEntry( + postCommentDetails.getComment().getPost(), + logEntryMap.get(Post.class.getName()) + ); + } + + @Test + public void testLazyLoading() { + LoggedUser.logIn("vlad@vladmihalcea.com"); + + doInJPA(entityManager -> { + PostCommentDetails postCommentDetails = entityManager.find(PostCommentDetails.class, 2L); + + Map logEntryMap = getLoadEventLogEntryMap( + getLoadEventLogEntries(entityManager) + ); + assertEquals(1, logEntryMap.size()); + + assertPostCommentDetailsEntityLogEntry( + postCommentDetails, + logEntryMap.get(PostCommentDetails.class.getName()) + ); + + PostComment postComment = postCommentDetails.getComment(); + assertEquals("Excellent", postComment.getReview()); + + logEntryMap = getLoadEventLogEntryMap( + getLoadEventLogEntries(entityManager) + ); + assertEquals(2, logEntryMap.size()); + + assertPostCommentEntityLogEntry( + postComment, + logEntryMap.get(PostComment.class.getName()) + ); + + Post post = postComment.getPost(); + assertEquals("High-Performance Java Persistence", post.getTitle()); + + logEntryMap = getLoadEventLogEntryMap( + getLoadEventLogEntries(entityManager) + ); + assertEquals(3, logEntryMap.size()); + + assertPostEntityLogEntry( + post, + logEntryMap.get(Post.class.getName()) + ); + }); + } + + public List getLoadEventLogEntries() { + return doInJPA((JPATransactionFunction>) this::getLoadEventLogEntries); + } + + public List getLoadEventLogEntries(EntityManager entityManager) { + return entityManager.createQuery(""" + select le + from LoadEventLogEntry le + order by le.id desc + """, LoadEventLogEntry.class) + .getResultList(); + } + + public Map getLoadEventLogEntryMap() { + return getLoadEventLogEntryMap(getLoadEventLogEntries()); + } + + public Map getLoadEventLogEntryMap(List logEntries) { + return logEntries + .stream() + .collect( + Collectors.toMap( + LoadEventLogEntry::getEntityName, + Function.identity() + ) + ); + } + + private void assertPostEntityLogEntry( + Post post, LoadEventLogEntry eventLogEntry) { + assertEquals( + "vlad@vladmihalcea.com", + eventLogEntry.getCreatedBy() + ); + + assertEquals( + Post.class.getName(), + eventLogEntry.getEntityName() + ); + + assertEquals( + String.valueOf(post.getId()), + eventLogEntry.getEntityId() + ); + } + + private void assertPostCommentEntityLogEntry(PostComment postComment, LoadEventLogEntry eventLogEntry) { + assertEquals( + "vlad@vladmihalcea.com", + eventLogEntry.getCreatedBy() + ); + + assertEquals( + PostComment.class.getName(), + eventLogEntry.getEntityName() + ); + + assertEquals( + String.valueOf(postComment.getId()), + eventLogEntry.getEntityId() + ); + } + + private void assertPostCommentDetailsEntityLogEntry(PostCommentDetails postCommentDetails, LoadEventLogEntry eventLogEntry) { + assertEquals( + "vlad@vladmihalcea.com", + eventLogEntry.getCreatedBy() + ); + + assertEquals( + PostCommentDetails.class.getName(), + eventLogEntry.getEntityName() + ); + + assertEquals( + String.valueOf(postCommentDetails.getId()), + eventLogEntry.getEntityId() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/listener/AuditLogPostLoadEventListener.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/listener/AuditLogPostLoadEventListener.java new file mode 100644 index 000000000..22cab7f54 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/listener/AuditLogPostLoadEventListener.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.hpjp.hibernate.audit.hibernate.listener; + +import com.vladmihalcea.hpjp.hibernate.audit.hibernate.model.Auditable; +import com.vladmihalcea.hpjp.hibernate.audit.hibernate.model.LoadEventLogEntry; +import com.vladmihalcea.hpjp.hibernate.audit.hibernate.model.LoggedUser; +import org.hibernate.event.spi.PostLoadEvent; +import org.hibernate.event.spi.PostLoadEventListener; +import org.hibernate.persister.entity.EntityPersister; + +/** + * @author Vlad Mihalcea + */ +public class AuditLogPostLoadEventListener implements PostLoadEventListener { + + public static final AuditLogPostLoadEventListener INSTANCE = new AuditLogPostLoadEventListener(); + + @Override + public void onPostLoad(PostLoadEvent event) { + final Object entity = event.getEntity(); + final EntityPersister entityPersister = event.getPersister(); + + if (entity instanceof Auditable) { + Auditable auditable = (Auditable) entity; + + event.getSession().persist( + new LoadEventLogEntry() + .setCreatedBy(LoggedUser.get()) + .setEntityName(entityPersister.getEntityName()) + .setEntityId(String.valueOf(auditable.getId())) + ); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/listener/EventListenerIntegrator.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/listener/EventListenerIntegrator.java new file mode 100644 index 000000000..21ada63ca --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/listener/EventListenerIntegrator.java @@ -0,0 +1,38 @@ +package com.vladmihalcea.hpjp.hibernate.audit.hibernate.listener; + +import org.hibernate.boot.Metadata; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; + +/** + * @author Vlad Mihalcea + */ +public class EventListenerIntegrator implements Integrator { + + public static final EventListenerIntegrator INSTANCE = new EventListenerIntegrator(); + + @Override + public void integrate( + Metadata metadata, + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + final EventListenerRegistry eventListenerRegistry = + serviceRegistry.getService(EventListenerRegistry.class); + + eventListenerRegistry.appendListeners( + EventType.POST_LOAD, + AuditLogPostLoadEventListener.INSTANCE + ); + } + + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/Auditable.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/Auditable.java new file mode 100644 index 000000000..2e042d834 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/Auditable.java @@ -0,0 +1,9 @@ +package com.vladmihalcea.hpjp.hibernate.audit.hibernate.model; + +/** + * @author Vlad Mihalcea + */ +public interface Auditable { + + I getId(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/LoadEventLogEntry.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/LoadEventLogEntry.java new file mode 100644 index 000000000..553dc1b60 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/LoadEventLogEntry.java @@ -0,0 +1,64 @@ +package com.vladmihalcea.hpjp.hibernate.audit.hibernate.model; + +import org.hibernate.annotations.CreationTimestamp; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "LoadEventLogEntry") +@Table(name = "load_event_log") +public class LoadEventLogEntry { + + @Id + @GeneratedValue + private Long id; + + @CreationTimestamp + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + private String entityName; + + private String entityId; + + public Long getId() { + return id; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public LoadEventLogEntry setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public String getEntityName() { + return entityName; + } + + public LoadEventLogEntry setEntityName(String entityName) { + this.entityName = entityName; + return this; + } + + public String getEntityId() { + return entityId; + } + + public LoadEventLogEntry setEntityId(String entityId) { + this.entityId = entityId; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/LoggedUser.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/LoggedUser.java new file mode 100644 index 000000000..d2415dd18 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/LoggedUser.java @@ -0,0 +1,21 @@ +package com.vladmihalcea.hpjp.hibernate.audit.hibernate.model; + +/** + * @author Vlad Mihalcea + */ +public class LoggedUser { + + private static final ThreadLocal userHolder = new ThreadLocal<>(); + + public static void logIn(String user) { + userHolder.set(user); + } + + public static void logOut() { + userHolder.remove(); + } + + public static String get() { + return userHolder.get(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/Post.java new file mode 100644 index 000000000..da9673a88 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/Post.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.hibernate.audit.hibernate.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post implements Auditable { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/PostComment.java new file mode 100644 index 000000000..75cf3a7b3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/PostComment.java @@ -0,0 +1,46 @@ +package com.vladmihalcea.hpjp.hibernate.audit.hibernate.model; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment implements Auditable { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/PostCommentDetails.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/PostCommentDetails.java new file mode 100644 index 000000000..f16681911 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/hibernate/model/PostCommentDetails.java @@ -0,0 +1,47 @@ +package com.vladmihalcea.hpjp.hibernate.audit.hibernate.model; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostCommentDetails") +@Table(name = "post_comment_details") +public class PostCommentDetails implements Auditable { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private PostComment comment; + + private int votes; + + public Long getId() { + return id; + } + + public PostCommentDetails setId(Long id) { + this.id = id; + return this; + } + + public PostComment getComment() { + return comment; + } + + public PostCommentDetails setComment(PostComment comment) { + this.comment = comment; + return this; + } + + public int getVotes() { + return votes; + } + + public PostCommentDetails setVotes(int votes) { + this.votes = votes; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/MySQLTriggerBasedAuditedTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/MySQLTriggerBasedAuditedTest.java new file mode 100644 index 000000000..5cf7cf27b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/MySQLTriggerBasedAuditedTest.java @@ -0,0 +1,248 @@ +package com.vladmihalcea.hpjp.hibernate.audit.trigger; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import io.hypersistence.utils.hibernate.type.json.JsonNodeStringType; +import org.hibernate.Session; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.usertype.UserType; +import org.junit.Test; +import com.vladmihalcea.hpjp.util.ReflectionUtils; + +import jakarta.persistence.*; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLTriggerBasedAuditedTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected List> additionalTypes() { + return List.of(JsonNodeStringType.INSTANCE); + } + + @Override + protected void afterInit() { + executeStatement("DROP TABLE IF EXISTS post_AUD"); + executeStatement(""" + CREATE TABLE IF NOT EXISTS post_AUD ( + id BIGINT NOT NULL, + title VARCHAR(255), + REV_TYPE ENUM('INSERT', 'UPDATE', 'DELETE') NOT NULL, + REV_TIMESTAMP DATETIME NOT NULL, + REV_CREATED_BY VARCHAR(255) NOT NULL, + PRIMARY KEY (id, REV_TYPE, REV_TIMESTAMP) + ) + """ + ); + + executeStatement(""" + CREATE TRIGGER post_insert_audit_trigger + AFTER INSERT ON post + FOR EACH ROW BEGIN + INSERT INTO post_AUD ( + id, + title, + REV_TYPE, + REV_TIMESTAMP, + REV_CREATED_BY + ) + VALUES( + NEW.id, + NEW.title, + 'INSERT', + CURRENT_TIMESTAMP, + @logged_user + ); + END + """ + ); + + executeStatement(""" + CREATE TRIGGER post_update_audit_trigger + AFTER UPDATE ON post + FOR EACH ROW BEGIN + INSERT INTO post_AUD ( + id, + title, + REV_TYPE, + REV_TIMESTAMP, + REV_CREATED_BY + ) + VALUES( + NEW.id, + NEW.title, + 'UPDATE', + CURRENT_TIMESTAMP, + @logged_user + ); + END + """ + ); + + executeStatement(""" + CREATE TRIGGER post_delete_audit_trigger + AFTER DELETE ON post + FOR EACH ROW BEGIN + INSERT INTO post_AUD ( + id, + title, + REV_TYPE, + REV_TIMESTAMP, + REV_CREATED_BY + ) + VALUES( + OLD.id, + OLD.title, + 'DELETE', + CURRENT_TIMESTAMP, + @logged_user + ); + END + """ + ); + } + + @Test + public void test() { + LoggedUser.logIn("Vlad Mihalcea"); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence 1st edition"); + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + assertEquals(1, revisions.size()); + }); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Java Persistence 2nd edition"); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + assertEquals(2, revisions.size()); + }); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + entityManager.remove( + entityManager.getReference(Post.class, 1L) + ); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + assertEquals(3, revisions.size()); + }); + } + + private void setCurrentLoggedUser(EntityManager entityManager) { + Session session = entityManager.unwrap(Session.class); + Dialect dialect = session.getSessionFactory().unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect(); + String loggedUser = ReflectionUtils.invokeMethod( + dialect, + "inlineLiteral", + LoggedUser.get() + ); + + session.doWork(connection -> { + update( + connection, + String.format( + "SET @logged_user = %s", loggedUser + ) + ); + }); + } + + private List getPostRevisions(EntityManager entityManager) { + return entityManager.createNativeQuery(""" + SELECT * + FROM post_AUD + ORDER BY REV_TIMESTAMP + """, Tuple.class) + .getResultList(); + } + + public static class LoggedUser { + + private static final ThreadLocal userHolder = new ThreadLocal<>(); + + public static void logIn(String user) { + userHolder.set(user); + } + + public static void logOut() { + userHolder.remove(); + } + + public static String get() { + return userHolder.get(); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public String toString() { + return "Post{" + + "id=" + id + + ", title='" + title + '\'' + + '}'; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/MySQLTriggerBasedJsonAuditLogTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/MySQLTriggerBasedJsonAuditLogTest.java new file mode 100644 index 000000000..ee01be0d2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/MySQLTriggerBasedJsonAuditLogTest.java @@ -0,0 +1,355 @@ +package com.vladmihalcea.hpjp.hibernate.audit.trigger; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import io.hypersistence.utils.hibernate.type.json.JsonNodeStringType; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.usertype.UserType; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLTriggerBasedJsonAuditLogTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected List> additionalTypes() { + return List.of(JsonNodeStringType.INSTANCE); + } + + @Override + protected void afterInit() { + executeStatement("DROP TABLE IF EXISTS book_audit_log"); + executeStatement(""" + CREATE TABLE IF NOT EXISTS book_audit_log ( + book_id BIGINT NOT NULL, + old_row_data JSON, + new_row_data JSON, + dml_type ENUM('INSERT', 'UPDATE', 'DELETE') NOT NULL, + dml_timestamp DATETIME NOT NULL, + dml_created_by VARCHAR(255) NOT NULL, + trx_timestamp timestamp NOT NULL, + PRIMARY KEY (book_id, dml_type, dml_timestamp) + ) + """ + ); + + executeStatement(""" + CREATE TRIGGER book_insert_audit_trigger + AFTER INSERT ON book FOR EACH ROW + BEGIN + INSERT INTO book_audit_log ( + book_id, + old_row_data, + new_row_data, + dml_type, + dml_timestamp, + dml_created_by, + trx_timestamp + ) + VALUES( + NEW.id, + null, + JSON_OBJECT( + "title", NEW.title, + "author", NEW.author, + "price_in_cents", NEW.price_in_cents, + "publisher", NEW.publisher + ), + 'INSERT', + CURRENT_TIMESTAMP, + @logged_user, + @transaction_timestamp + ); + END + """ + ); + + executeStatement(""" + CREATE TRIGGER book_update_audit_trigger + AFTER UPDATE ON book FOR EACH ROW + BEGIN + INSERT INTO book_audit_log ( + book_id, + old_row_data, + new_row_data, + dml_type, + dml_timestamp, + dml_created_by, + trx_timestamp + ) + VALUES( + NEW.id, + JSON_OBJECT( + "title", OLD.title, + "author", OLD.author, + "price_in_cents", OLD.price_in_cents, + "publisher", OLD.publisher + ), + JSON_OBJECT( + "title", NEW.title, + "author", NEW.author, + "price_in_cents", NEW.price_in_cents, + "publisher", NEW.publisher + ), + 'UPDATE', + CURRENT_TIMESTAMP, + @logged_user, + @transaction_timestamp + ); + END + """ + ); + + executeStatement(""" + CREATE TRIGGER book_delete_audit_trigger + AFTER DELETE ON book FOR EACH ROW + BEGIN + INSERT INTO book_audit_log ( + book_id, + old_row_data, + new_row_data, + dml_type, + dml_timestamp, + dml_created_by, + trx_timestamp + ) + VALUES( + OLD.id, + JSON_OBJECT( + "title", OLD.title, + "author", OLD.author, + "price_in_cents", OLD.price_in_cents, + "publisher", OLD.publisher + ), + null, + 'DELETE', + CURRENT_TIMESTAMP, + @logged_user, + @transaction_timestamp + ); + END + """ + ); + } + + @Test + public void test() { + LoggedUser.logIn("Vlad Mihalcea"); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + entityManager.persist( + new Book() + .setId(1L) + .setTitle("High-Performance Java Persistence 1st edition") + .setPublisher("Amazon") + .setPriceInCents(3990) + .setAuthor("Vlad Mihalcea") + ); + + sleep(TimeUnit.SECONDS.toMillis(1)); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + assertEquals(1, revisions.size()); + + }); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + Book book = entityManager.find(Book.class, 1L) + .setPriceInCents(4499); + + sleep(TimeUnit.SECONDS.toMillis(1)); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + assertEquals(2, revisions.size()); + }); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + entityManager.remove( + entityManager.getReference(Book.class, 1L) + ); + + sleep(TimeUnit.SECONDS.toMillis(1)); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + assertEquals(3, revisions.size()); + + List bookRevisions = entityManager.createNativeQuery(""" + SELECT + book_audit_log.dml_timestamp as version_timestamp, + r.* + FROM + book_audit_log + LEFT JOIN + JSON_TABLE( + new_row_data, + '$' + COLUMNS ( + title VARCHAR(255) PATH '$.title', + author VARCHAR(255) PATH '$.author', + price_in_cents INT(11) PATH '$.price_in_cents', + publisher VARCHAR(255) PATH '$.publisher' + ) + ) AS r ON true + WHERE + book_audit_log.book_id = :bookId + ORDER BY version_timestamp + """, Tuple.class) + .setParameter("bookId", 1L) + .getResultList(); + + assertEquals(3, bookRevisions.size()); + }); + } + + private void setCurrentLoggedUser(EntityManager entityManager) { + Session session = entityManager.unwrap(Session.class); + Dialect dialect = session.getSessionFactory().unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect(); + String loggedUser = ReflectionUtils.invokeMethod( + dialect, + "inlineLiteral", + LoggedUser.get() + ); + + session.doWork(connection -> { + update( + connection, + "SET @transaction_timestamp = CURRENT_TIMESTAMP" + ); + + update( + connection, + String.format( + "SET @logged_user = %s", loggedUser + ) + ); + }); + } + + private List getPostRevisions(EntityManager entityManager) { + return entityManager.createNativeQuery(""" + SELECT * + FROM book_audit_log + ORDER BY dml_timestamp + """, Tuple.class) + .getResultList(); + } + + public static class LoggedUser { + + private static final ThreadLocal userHolder = new ThreadLocal<>(); + + public static void logIn(String user) { + userHolder.set(user); + } + + public static void logOut() { + userHolder.remove(); + } + + public static String get() { + return userHolder.get(); + } + } + + @Entity(name = "Book") + @Table(name = "book") + @DynamicUpdate + public static class Book { + + @Id + private Long id; + + private String title; + + private String author; + + @Column(name = "price_in_cents") + private int priceInCents; + + private String publisher; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + + public int getPriceInCents() { + return priceInCents; + } + + public Book setPriceInCents(int priceInCents) { + this.priceInCents = priceInCents; + return this; + } + + public String getPublisher() { + return publisher; + } + + public Book setPublisher(String publisher) { + this.publisher = publisher; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/PostgreSQLTriggerBasedJsonAuditLogTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/PostgreSQLTriggerBasedJsonAuditLogTest.java new file mode 100644 index 000000000..479d14909 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/PostgreSQLTriggerBasedJsonAuditLogTest.java @@ -0,0 +1,323 @@ +package com.vladmihalcea.hpjp.hibernate.audit.trigger; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import io.hypersistence.utils.hibernate.type.json.JsonNodeBinaryType; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.NativeQuery; +import org.hibernate.usertype.UserType; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLTriggerBasedJsonAuditLogTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected List> additionalTypes() { + return List.of(JsonNodeBinaryType.INSTANCE); + } + + @Override + protected void afterInit() { + executeStatement("DROP TYPE dml_type CASCADE"); + executeStatement("CREATE TYPE dml_type AS ENUM ('INSERT', 'UPDATE', 'DELETE')"); + + executeStatement("DROP TABLE IF EXISTS book_audit_log CASCADE"); + executeStatement(""" + CREATE TABLE IF NOT EXISTS book_audit_log ( + book_id bigint NOT NULL, + old_row_data jsonb, + new_row_data jsonb, + dml_type dml_type NOT NULL, + dml_timestamp timestamp NOT NULL, + dml_created_by varchar(255) NOT NULL, + trx_timestamp timestamp NOT NULL, + PRIMARY KEY (book_id, dml_type, dml_timestamp) + ) + """ + ); + + executeStatement("DROP FUNCTION IF EXISTS book_audit_trigger_func cascade"); + + executeStatement(""" + CREATE OR REPLACE FUNCTION book_audit_trigger_func() + RETURNS trigger AS $body$ + BEGIN + if (TG_OP = 'INSERT') then + INSERT INTO book_audit_log ( + book_id, + old_row_data, + new_row_data, + dml_type, + dml_timestamp, + dml_created_by, + trx_timestamp + ) + VALUES( + NEW.id, + null, + to_jsonb(NEW), + 'INSERT', + statement_timestamp(), + current_setting('var.logged_user'), + transaction_timestamp() + ); + + RETURN NEW; + elsif (TG_OP = 'UPDATE') then + INSERT INTO book_audit_log ( + book_id, + old_row_data, + new_row_data, + dml_type, + dml_timestamp, + dml_created_by, + trx_timestamp + ) + VALUES( + NEW.id, + to_jsonb(OLD), + to_jsonb(NEW), + 'UPDATE', + statement_timestamp(), + current_setting('var.logged_user'), + transaction_timestamp() + ); + + RETURN NEW; + elsif (TG_OP = 'DELETE') then + INSERT INTO book_audit_log ( + book_id, + old_row_data, + new_row_data, + dml_type, + dml_timestamp, + dml_created_by, + trx_timestamp + ) + VALUES( + OLD.id, + to_jsonb(OLD), + null, + 'DELETE', + statement_timestamp(), + current_setting('var.logged_user'), + transaction_timestamp() + ); + + RETURN OLD; + END IF; + END; + $body$ + LANGUAGE plpgsql + """ + ); + + executeStatement(""" + CREATE TRIGGER book_audit_trigger + AFTER INSERT OR UPDATE OR DELETE ON book + FOR EACH ROW EXECUTE FUNCTION book_audit_trigger_func(); + """ + ); + } + + @Test + public void test() { + LoggedUser.logIn("Vlad Mihalcea"); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + entityManager.persist( + new Book() + .setId(1L) + .setTitle("High-Performance Java Persistence 1st edition") + .setPublisher("Amazon") + .setPriceInCents(3990) + .setAuthor("Vlad Mihalcea") + ); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + assertEquals(1, revisions.size()); + }); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + Book book = entityManager.find(Book.class, 1L) + .setPriceInCents(4499); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + assertEquals(2, revisions.size()); + }); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + entityManager.remove( + entityManager.getReference(Book.class, 1L) + ); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + assertEquals(3, revisions.size()); + + List bookRevisions = entityManager.createNativeQuery(""" + SELECT + dml_timestamp as version_timestamp, + new_row_data ->> 'title' as title, + new_row_data ->> 'author' as author, + cast(new_row_data ->> 'price_in_cents' as int) as price_in_cents, + new_row_data ->> 'publisher' as publisher + FROM + book_audit_log + WHERE + book_audit_log.book_id = :bookId + ORDER BY dml_timestamp + """, Tuple.class) + .setParameter("bookId", 1L) + .getResultList(); + + assertEquals(3, bookRevisions.size()); + }); + } + + private void setCurrentLoggedUser(EntityManager entityManager) { + Session session = entityManager.unwrap(Session.class); + Dialect dialect = session.getSessionFactory().unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect(); + String loggedUser = dialect.inlineLiteral(LoggedUser.get()); + + session.doWork(connection -> update( + connection, + String.format( + "SET LOCAL var.logged_user = %s", loggedUser + ) + )); + } + + private List getPostRevisions(EntityManager entityManager) { + return entityManager.createNativeQuery(""" + SELECT + book_id, + old_row_data, + new_row_data, + dml_type, + dml_timestamp, + dml_created_by, + trx_timestamp + FROM book_audit_log + ORDER BY dml_timestamp + """, Tuple.class) + .unwrap(NativeQuery.class) + .getResultList(); + } + + public static class LoggedUser { + + private static final ThreadLocal userHolder = new ThreadLocal<>(); + + public static void logIn(String user) { + userHolder.set(user); + } + + public static void logOut() { + userHolder.remove(); + } + + public static String get() { + return userHolder.get(); + } + } + + @Entity(name = "Book") + @Table(name = "book") + @DynamicUpdate + public static class Book { + + @Id + private Long id; + + private String title; + + private String author; + + @Column(name = "price_in_cents") + private int priceInCents; + + private String publisher; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + + public int getPriceInCents() { + return priceInCents; + } + + public Book setPriceInCents(int priceInCents) { + this.priceInCents = priceInCents; + return this; + } + + public String getPublisher() { + return publisher; + } + + public Book setPublisher(String publisher) { + this.publisher = publisher; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/SQLServerTriggerBasedJsonAuditLogTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/SQLServerTriggerBasedJsonAuditLogTest.java new file mode 100644 index 000000000..c2f7f7eec --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/SQLServerTriggerBasedJsonAuditLogTest.java @@ -0,0 +1,355 @@ +package com.vladmihalcea.hpjp.hibernate.audit.trigger; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import io.hypersistence.utils.hibernate.type.json.JsonNodeStringType; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.usertype.UserType; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerTriggerBasedJsonAuditLogTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected Database database() { + return Database.SQLSERVER; + } + + @Override + protected List> additionalTypes() { + return List.of(JsonNodeStringType.INSTANCE); + } + + @Override + protected void afterInit() { + executeStatement("DROP TABLE BookAuditLog"); + executeStatement(""" + CREATE TABLE BookAuditLog ( + BookId bigint NOT NULL, + OldRowData nvarchar(1000) CHECK(ISJSON(OldRowData) = 1), + NewRowData nvarchar(1000) CHECK(ISJSON(NewRowData) = 1), + DmlType varchar(10) NOT NULL CHECK (DmlType IN ('INSERT', 'UPDATE', 'DELETE')), + DmlTimestamp datetime NOT NULL, + DmlCreatedBy varchar(255) NOT NULL, + TrxTimestamp datetime NOT NULL, + PRIMARY KEY (BookId, DmlType, DmlTimestamp) + ) + """ + ); + + executeStatement(""" + CREATE TRIGGER TR_Book_Insert_AuditLog ON Book + FOR INSERT AS + BEGIN + DECLARE @loggedUser varchar(255) + SELECT @loggedUser = cast(SESSION_CONTEXT(N'loggedUser') as varchar(255)) + + DECLARE @transactionTimestamp datetime = SYSUTCdatetime() + + INSERT INTO BookAuditLog ( + BookId, + OldRowData, + NewRowData, + DmlType, + DmlTimestamp, + DmlCreatedBy, + TrxTimestamp + ) + VALUES( + (SELECT id FROM Inserted), + null, + (SELECT * FROM Inserted FOR JSON PATH, WITHOUT_ARRAY_WRAPPER), + 'INSERT', + CURRENT_TIMESTAMP, + @loggedUser, + @transactionTimestamp + ); + END + """ + ); + + executeStatement(""" + CREATE TRIGGER TR_Book_Update_AuditLog ON Book + FOR UPDATE AS + BEGIN + DECLARE @loggedUser varchar(255) + SELECT @loggedUser = cast(SESSION_CONTEXT(N'loggedUser') as varchar(255)) + + DECLARE @transactionTimestamp datetime = SYSUTCdatetime() + + DECLARE @oldRecord nvarchar(1000) + DECLARE @newRecord nvarchar(1000) + + SET @oldRecord = (SELECT * FROM Deleted FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) + SET @newRecord = (SELECT * FROM Inserted FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) + + IF @oldRecord != @newRecord + INSERT INTO BookAuditLog ( + BookId, + OldRowData, + NewRowData, + DmlType, + DmlTimestamp, + DmlCreatedBy, + TrxTimestamp + ) + VALUES( + (SELECT id FROM Inserted), + @oldRecord, + @newRecord, + 'UPDATE', + CURRENT_TIMESTAMP, + @loggedUser, + @transactionTimestamp + ); + END + """ + ); + + executeStatement(""" + CREATE TRIGGER TR_Book_Delete_AuditLog ON Book + FOR DELETE AS + BEGIN + DECLARE @loggedUser varchar(255) + SELECT @loggedUser = cast(SESSION_CONTEXT(N'loggedUser') as varchar(255)) + + DECLARE @transactionTimestamp datetime = SYSUTCdatetime() + + INSERT INTO BookAuditLog ( + BookId, + OldRowData, + NewRowData, + DmlType, + DmlTimestamp, + DmlCreatedBy, + TrxTimestamp + ) + VALUES( + (SELECT id FROM Deleted), + (SELECT * FROM Deleted FOR JSON PATH, WITHOUT_ARRAY_WRAPPER), + null, + 'DELETE', + CURRENT_TIMESTAMP, + @loggedUser, + @transactionTimestamp + ); + END + """ + ); + } + + @Test + public void test() { + LoggedUser.logIn("Vlad Mihalcea"); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + entityManager.persist( + new Book() + .setId(1L) + .setTitle("High-Performance Java Persistence 1st edition") + .setPublisher("Amazon") + .setPriceInCents(3990) + .setAuthor("Vlad Mihalcea") + ); + + sleep(TimeUnit.SECONDS.toMillis(1)); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + assertEquals(1, revisions.size()); + + }); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + Book book = entityManager.find(Book.class, 1L) + .setPriceInCents(4499); + + sleep(TimeUnit.SECONDS.toMillis(1)); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + assertEquals(2, revisions.size()); + }); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + entityManager.remove( + entityManager.getReference(Book.class, 1L) + ); + + sleep(TimeUnit.SECONDS.toMillis(1)); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + assertEquals(3, revisions.size()); + + List bookRevisions = entityManager.createNativeQuery(""" + SELECT + BookAuditLog.DmlTimestamp as VersionTimestamp, + r.* + FROM + BookAuditLog + OUTER APPLY + OPENJSON ( + JSON_QUERY( + NewRowData, + '$' + ) + ) + WITH ( + title varchar(255) '$.Title', + author varchar(255) '$.Author', + price_in_cents bigint '$.PriceInCents', + publisher varchar(255) '$.Publisher' + ) AS r + WHERE + BookAuditLog.BookId = :bookId + ORDER BY VersionTimestamp + """, Tuple.class) + .setParameter("bookId", 1L) + .getResultList(); + + assertEquals(3, bookRevisions.size()); + }); + } + + private void setCurrentLoggedUser(EntityManager entityManager) { + Session session = entityManager.unwrap(Session.class); + Dialect dialect = session.getSessionFactory().unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect(); + String loggedUser = ReflectionUtils.invokeMethod( + dialect, + "inlineLiteral", + LoggedUser.get() + ); + + session.doWork(connection -> update( + connection, + String.format( + "EXEC sys.sp_set_session_context @key = N'loggedUser', @value = N%s, @read_only = 1", loggedUser + ) + )); + } + + private List getPostRevisions(EntityManager entityManager) { + return entityManager.createNativeQuery(""" + SELECT * + FROM BookAuditLog + ORDER BY DmlTimestamp + """, Tuple.class) + .getResultList(); + } + + public static class LoggedUser { + + private static final ThreadLocal userHolder = new ThreadLocal<>(); + + public static void logIn(String user) { + userHolder.set(user); + } + + public static void logOut() { + userHolder.remove(); + } + + public static String get() { + return userHolder.get(); + } + } + + @Entity(name = "Book") + @Table(name = "Book") + @DynamicUpdate + public static class Book { + + @Id + @Column(name = "Id") + private Long id; + + @Column(name = "Title") + private String title; + + @Column(name = "Author") + private String author; + + @Column(name = "PriceInCents") + private int priceInCents; + + @Column(name = "Publisher") + private String publisher; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + + public int getPriceInCents() { + return priceInCents; + } + + public Book setPriceInCents(int priceInCents) { + this.priceInCents = priceInCents; + return this; + } + + public String getPublisher() { + return publisher; + } + + public Book setPublisher(String publisher) { + this.publisher = publisher; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/YugabyteDBTriggerBasedJsonAuditLogTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/YugabyteDBTriggerBasedJsonAuditLogTest.java new file mode 100644 index 000000000..a782b3c84 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/audit/trigger/YugabyteDBTriggerBasedJsonAuditLogTest.java @@ -0,0 +1,443 @@ +package com.vladmihalcea.hpjp.hibernate.audit.trigger; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class YugabyteDBTriggerBasedJsonAuditLogTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class, + Author.class + }; + } + + @Override + protected Database database() { + return Database.YUGABYTEDB; + } + + @Override + public void init() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + super.init(); + } + + @Override + protected void afterInit() { + executeStatement("DROP TYPE dml_type CASCADE"); + executeStatement("CREATE TYPE dml_type AS ENUM ('INSERT', 'UPDATE', 'DELETE')"); + + executeStatement("DROP TABLE IF EXISTS audit_log CASCADE"); + executeStatement( + String.format( + """ + CREATE TABLE IF NOT EXISTS audit_log ( + table_name varchar(255) NOT NULL, + row_id bigint NOT NULL, + old_row_data jsonb, + new_row_data jsonb, + dml_type dml_type NOT NULL, + dml_timestamp timestamp NOT NULL, + dml_created_by varchar(255) NOT NULL, + trx_timestamp timestamp NOT NULL, + PRIMARY KEY (%s, dml_type, dml_timestamp) + ) + """, + database() == Database.YUGABYTEDB ? "(table_name, row_id) HASH" : "table_name, row_id" + ) + ); + + executeStatement("DROP FUNCTION IF EXISTS audit_log_trigger_function cascade"); + + executeStatement(""" + CREATE OR REPLACE FUNCTION audit_log_trigger_function() + RETURNS trigger AS $body$ + BEGIN + if (TG_OP = 'INSERT') then + INSERT INTO audit_log ( + table_name, + row_id, + old_row_data, + new_row_data, + dml_type, + dml_timestamp, + dml_created_by, + trx_timestamp + ) + VALUES( + TG_TABLE_NAME, + NEW.id, + null, + to_jsonb(NEW), + 'INSERT', + statement_timestamp(), + current_setting('var.logged_user'), + transaction_timestamp() + ); + + RETURN NEW; + elsif (TG_OP = 'UPDATE') then + INSERT INTO audit_log ( + table_name, + row_id, + old_row_data, + new_row_data, + dml_type, + dml_timestamp, + dml_created_by, + trx_timestamp + ) + VALUES( + TG_TABLE_NAME, + NEW.id, + to_jsonb(OLD), + to_jsonb(NEW), + 'UPDATE', + statement_timestamp(), + current_setting('var.logged_user'), + transaction_timestamp() + ); + + RETURN NEW; + elsif (TG_OP = 'DELETE') then + INSERT INTO audit_log ( + table_name, + row_id, + old_row_data, + new_row_data, + dml_type, + dml_timestamp, + dml_created_by, + trx_timestamp + ) + VALUES( + TG_TABLE_NAME, + OLD.id, + to_jsonb(OLD), + null, + 'DELETE', + statement_timestamp(), + current_setting('var.logged_user'), + transaction_timestamp() + ); + + RETURN OLD; + END IF; + END; + $body$ + LANGUAGE plpgsql + """ + ); + + executeStatement(""" + CREATE TRIGGER book_audit_trigger + AFTER INSERT OR UPDATE OR DELETE ON book + FOR EACH ROW EXECUTE FUNCTION audit_log_trigger_function() + """ + ); + + executeStatement(""" + CREATE TRIGGER author_audit_trigger + AFTER INSERT OR UPDATE OR DELETE ON author + FOR EACH ROW EXECUTE FUNCTION audit_log_trigger_function() + """ + ); + } + + @Test + public void test() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + LoggedUser.logIn("Vlad Mihalcea"); + + AtomicInteger auditLogCount = new AtomicInteger(); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + Author author = new Author() + .setId(1L) + .setFirstName("Vlad") + .setLastName("Mihalcea") + .setCountry("România"); + + entityManager.persist(author); + + entityManager.persist( + new Book() + .setId(1L) + .setTitle("High-Performance Java Persistence 1st edition") + .setPublisher("Amazon") + .setPriceInCents(3990) + .setAuthor(author) + ); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + //Inserting the author + auditLogCount.incrementAndGet(); + //Inserting the book + auditLogCount.incrementAndGet(); + + assertEquals(auditLogCount.get(), revisions.size()); + }); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + entityManager.find(Author.class, 1L) + .setTaxTreatyClaiming(true); + + entityManager.find(Book.class, 1L) + .setPriceInCents(4499); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + //Updating the author + auditLogCount.incrementAndGet(); + //Updating the book + auditLogCount.incrementAndGet(); + + assertEquals(auditLogCount.get(), revisions.size()); + }); + + doInJPA(entityManager -> { + setCurrentLoggedUser(entityManager); + + entityManager.remove( + entityManager.getReference(Book.class, 1L) + ); + }); + + doInJPA(entityManager -> { + List revisions = getPostRevisions(entityManager); + + //Deleting the book + auditLogCount.incrementAndGet(); + + assertEquals(auditLogCount.get(), revisions.size()); + + List bookRevisions = entityManager.createNativeQuery(""" + SELECT + row_id AS id, + cast(new_row_data ->> 'price_in_cents' AS int) AS price_in_cents, + new_row_data ->> 'publisher' AS publisher, + new_row_data ->> 'title' AS title, + new_row_data ->> 'author_id' AS author_id, + dml_timestamp as version_timestamp + FROM + audit_log + WHERE + table_name = 'book' AND + audit_log.row_id = :bookId + ORDER BY dml_timestamp + """, Tuple.class) + .setParameter("bookId", 1L) + .getResultList(); + + assertEquals(3, bookRevisions.size()); + }); + } + + private void setCurrentLoggedUser(EntityManager entityManager) { + Session session = entityManager.unwrap(Session.class); + Dialect dialect = session.getSessionFactory().unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect(); + String loggedUser = dialect.inlineLiteral(LoggedUser.get()); + + session.doWork(connection -> { + update( + connection, + String.format( + "SET LOCAL var.logged_user = %s", loggedUser + ) + ); + }); + } + + private List getPostRevisions(EntityManager entityManager) { + return entityManager.createNativeQuery(""" + SELECT + row_id, + old_row_data, + new_row_data, + dml_type, + dml_timestamp, + dml_created_by, + trx_timestamp + FROM audit_log + ORDER BY dml_timestamp + """, Tuple.class) + .unwrap(org.hibernate.query.NativeQuery.class) + .getResultList(); + } + + public static class LoggedUser { + + private static final ThreadLocal userHolder = new ThreadLocal<>(); + + public static void logIn(String user) { + userHolder.set(user); + } + + public static void logOut() { + userHolder.remove(); + } + + public static String get() { + return userHolder.get(); + } + } + + @Entity(name = "Book") + @Table(name = "book") + @DynamicUpdate + public static class Book { + + @Id + private Long id; + + private String title; + + @ManyToOne(fetch = FetchType.LAZY) + private Author author; + + @Column(name = "price_in_cents") + private int priceInCents; + + private String publisher; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public Author getAuthor() { + return author; + } + + public Book setAuthor(Author author) { + this.author = author; + return this; + } + + public int getPriceInCents() { + return priceInCents; + } + + public Book setPriceInCents(int priceInCents) { + this.priceInCents = priceInCents; + return this; + } + + public String getPublisher() { + return publisher; + } + + public Book setPublisher(String publisher) { + this.publisher = publisher; + return this; + } + } + + @Entity(name = "Author") + @Table(name = "author") + @DynamicUpdate + public static class Author { + + @Id + private Long id; + + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + private String country; + + @Column(name = "tax_treaty_claiming") + private boolean taxTreatyClaiming; + + public Long getId() { + return id; + } + + public Author setId(Long id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public Author setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public Author setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public String getCountry() { + return country; + } + + public Author setCountry(String country) { + this.country = country; + return this; + } + + public boolean isTaxTreatyClaiming() { + return taxTreatyClaiming; + } + + public Author setTaxTreatyClaiming(boolean taxTreatyClaiming) { + this.taxTreatyClaiming = taxTreatyClaiming; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/basic/DefaultTimestampTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/basic/DefaultTimestampTest.java similarity index 91% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/basic/DefaultTimestampTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/basic/DefaultTimestampTest.java index d9bba88de..b0ee347bd 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/basic/DefaultTimestampTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/basic/DefaultTimestampTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.basic; +package com.vladmihalcea.hpjp.hibernate.basic; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Date; import static org.junit.Assert.assertEquals; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/basic/MySQLBinaryTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/basic/MySQLBinaryTest.java new file mode 100644 index 000000000..461733974 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/basic/MySQLBinaryTest.java @@ -0,0 +1,81 @@ +package com.vladmihalcea.hpjp.hibernate.basic; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.assertArrayEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLBinaryTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post("First post"); + post.setImage(new byte[] {1, 2, 3}); + entityManager.persist(post); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertArrayEquals(new byte[] {1, 2, 3}, Arrays.copyOf(post.getImage(), 3)); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private byte[] image; + + public Post() {} + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public byte[] getImage() { + return image; + } + + public void setImage(byte[] image) { + this.image = image; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/BatchMergeVsUpdateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/BatchMergeVsUpdateTest.java new file mode 100644 index 000000000..0cbd9df59 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/BatchMergeVsUpdateTest.java @@ -0,0 +1,197 @@ +package com.vladmihalcea.hpjp.hibernate.batch; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class BatchMergeVsUpdateTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.jdbc.batch_size", "5"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_versioned_data", "true"); + return properties; + } + + public void afterInit() { + doInJPA(entityManager -> { + for (long i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle( + String.format("High-Performance Java Persistence, Part no. %d", i) + ) + .addComment( + new PostComment() + .setReview("Excellent") + ) + ); + } + }); + } + + @Test + public void testMerge() { + List posts = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + """, Post.class) + .getResultList(); + }); + + for (Post post : posts) { + post.setTitle("Vlad Mihalcea's " + post.getTitle()); + for (PostComment comment : post.getComments()) { + comment.setReview(comment.getReview() + " read!"); + } + } + + doInJPA(entityManager -> { + LOGGER.info("Merge"); + for (Post post : posts) { + entityManager.merge(post); + } + }); + } + + @Test + public void testUpdate() { + List posts = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + """, Post.class) + .getResultList(); + }); + + for (Post post : posts) { + post.setTitle("Vlad Mihalcea's " + post.getTitle()); + for (PostComment comment : post.getComments()) { + comment.setReview(comment.getReview() + " read!"); + } + } + + doInJPA(entityManager -> { + LOGGER.info("Update"); + Session session = entityManager.unwrap(Session.class); + for (Post post : posts) { + session.update(post); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/BatchTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/BatchTest.java new file mode 100644 index 000000000..2ac630873 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/BatchTest.java @@ -0,0 +1,117 @@ +package com.vladmihalcea.hpjp.hibernate.batch; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Session; +import org.jboss.logging.Logger; +import org.junit.Test; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +public class BatchTest extends AbstractTest { + + private static final Logger log = Logger.getLogger( BatchTest.class ); + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Test + public void testScroll() { + withBatchAndSessionManagement(); + } + + private void withBatch() { + int entityCount = 20; + EntityManager entityManager = null; + EntityTransaction txn = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + entityManager.unwrap(Session.class).setJdbcBatchSize(10); + + txn = entityManager.getTransaction(); + txn.begin(); + + int entityManagerBatchSize = 20; + + for ( long i = 0; i < entityCount; ++i ) { + Post person = new Post( i, String.format( "Post nr %d", i )); + entityManager.persist( person ); + + if ( i > 0 && i % entityManagerBatchSize == 0 ) { + entityManager.flush(); + entityManager.clear(); + } + } + + txn.commit(); + } catch (RuntimeException e) { + if ( txn != null && txn.isActive()) { + txn.rollback(); + } + throw e; + } finally { + if (entityManager != null) { + entityManager.close(); + } + } + } + + private void withBatchAndSessionManagement() { + int entityCount = 20; + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).setJdbcBatchSize(10); + + for ( long i = 0; i < entityCount; ++i ) { + Post person = new Post( i, String.format( "Post nr %d", i )); + entityManager.persist( person ); + } + }); + } + + private void withBatchAndResetBackToGlobalSetting() { + EntityManager entityManager = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + entityManager.getTransaction().begin(); + + + } finally { + if (entityManager != null) { + entityManager.getTransaction().rollback(); + entityManager.close(); + } + } + } + + @Entity(name = "Post") + public static class Post { + + @Id + private Long id; + + private String name; + + public Post() {} + + public Post(long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/BatchingOptimisticLockingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/BatchingOptimisticLockingTest.java new file mode 100644 index 000000000..27a954ca1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/BatchingOptimisticLockingTest.java @@ -0,0 +1,122 @@ +package com.vladmihalcea.hpjp.hibernate.batch; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class BatchingOptimisticLockingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Test + public void testOptimsticLOcking() { + doInJPA(entityManager -> { + for (int i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setTitle(String.format("Post no. %d", i)) + ); + } + }); + + try { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + posts.forEach(post -> post.setTitle(post.getTitle() + " - 2nd edition")); + + executeSync( + () -> doInJPA(_entityManager -> { + Post post = _entityManager.createQuery(""" + select p + from Post p + order by p.id + """, Post.class) + .setMaxResults(1) + .getSingleResult(); + + post.setTitle(post.getTitle() + " - corrected"); + }) + ); + }); + } catch (Exception e) { + assertTrue( + ExceptionUtil.rootCause(e).getMessage() + .startsWith( + "Batch update returned unexpected row count from update [0]; " + + "actual row count: 0; " + + "expected: 1; " + + "statement executed:" + ) + ); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + private String title; + + @Version + private short version; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public short getVersion() { + return version; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/BatchingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/BatchingTest.java new file mode 100644 index 000000000..88922277b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/BatchingTest.java @@ -0,0 +1,258 @@ +package com.vladmihalcea.hpjp.hibernate.batch; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; + +/** + * BatchingTest - Test to check the JDBC batch support + * + * @author Vlad Mihalcea + */ +public class BatchingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.STATEMENT_BATCH_SIZE, "5"); + properties.put(AvailableSettings.ORDER_INSERTS, Boolean.TRUE); + properties.put(AvailableSettings.ORDER_UPDATES, Boolean.TRUE); + } + + @Test + public void testInsertPosts() { + LOGGER.info("testInsertPosts"); + insertPosts(); + } + + @Test + public void testInsertPostsAndComments() { + LOGGER.info("testInsertPostsAndComments"); + insertPostsAndComments(); + } + + @Test + public void testUpdatePosts() { + insertPosts(); + + LOGGER.info("testUpdatePosts"); + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + posts.forEach(post -> post.setTitle(post.getTitle().replaceAll("no", "nr"))); + }); + } + + @Test + public void testUpdatePostsAndComments() { + insertPostsAndComments(); + + LOGGER.info("testUpdatePostsAndComments"); + doInJPA(entityManager -> { + entityManager.createQuery(""" + select c + from PostComment c + join fetch c.post + """, PostComment.class) + .getResultList() + .forEach(c -> { + c.setReview(c.getReview().replaceAll("Good", "Very good")); + Post post = c.getPost(); + post.setTitle(post.getTitle().replaceAll("no", "nr")); + }); + }); + } + + @Test + public void testDeletePosts() { + insertPosts(); + + LOGGER.info("testDeletePosts"); + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + posts.forEach(entityManager::remove); + }); + } + + @Test + public void testDeletePostsAndComments() { + insertPostsAndComments(); + + LOGGER.info("testDeletePostsAndComments"); + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + """, Post.class) + .getResultList(); + + posts.forEach(entityManager::remove); + }); + } + + @Test + public void testDeletePostsAndCommentsWithManualChildRemoval() { + insertPostsAndComments(); + + LOGGER.info("testDeletePostsAndCommentsWithManualChildRemoval"); + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + """, Post.class) + .getResultList(); + + for (Post post : posts) { + for (Iterator commentIterator = post.getComments().iterator(); + commentIterator.hasNext(); ) { + PostComment comment = commentIterator.next(); + comment.setPost(null); + commentIterator.remove(); + } + } + entityManager.flush(); + posts.forEach(entityManager::remove); + }); + } + + private void insertPosts() { + doInJPA(entityManager -> { + for (long i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle(String.format("Post no. %d", i)) + ); + } + }); + } + + private void insertPostsAndComments() { + doInJPA(entityManager -> { + for (long i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle(String.format("Post no. %d", i)) + .addComment(new PostComment("Good")) + ); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public PostComment() {} + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/DeletingWithSQLCascadeBatchingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/DeletingWithSQLCascadeBatchingTest.java new file mode 100644 index 000000000..7b6a45580 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/DeletingWithSQLCascadeBatchingTest.java @@ -0,0 +1,152 @@ +package com.vladmihalcea.hpjp.hibernate.batch; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * DeletingWithoutCascadeBatchingTest - Test to check the JDBC batch support for delete + * + * @author Vlad Mihalcea + */ +public class DeletingWithSQLCascadeBatchingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_versioned_data", "true"); + } + + @Test + public void testDeletePostsAndCommentsWithSQLOnDeleteCascade() { + doInJPA(entityManager -> { + for (int i = 0; i < 3; i++) { + Post post = new Post(String.format("Post no. %d", i)); + post.addComment(new PostComment("Good")); + entityManager.persist(post); + } + }); + + LOGGER.info("testDeletePostsAndCommentsWithSQLCascade"); + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + posts.forEach(entityManager::remove); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + private String title; + + public Post() {} + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "post") + @OnDelete(action = OnDeleteAction.CASCADE) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @ManyToOne + @org.hibernate.annotations.ForeignKey(name = "fk_post_comment_post") + private Post post; + + private String review; + + public PostComment() {} + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/DeletingWithoutCascadeBatchingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/DeletingWithoutCascadeBatchingTest.java new file mode 100644 index 000000000..287d876fa --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/DeletingWithoutCascadeBatchingTest.java @@ -0,0 +1,170 @@ +package com.vladmihalcea.hpjp.hibernate.batch; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * DeletingWithoutCascadeBatchingTest - Test to check the JDBC batch support for delete + * + * @author Vlad Mihalcea + */ +public class DeletingWithoutCascadeBatchingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.jdbc.batch_size", "5"); + //properties.put("hibernate.order_inserts", "true"); + //properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_versioned_data", "true"); + return properties; + } + + @Test + public void testDeletePostsAndCommentsWithBulkDelete() { + insertPostsAndComments(); + + LOGGER.info("testDeletePostsAndCommentsWithBulkDelete"); + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + where p.title like 'Post no%' + """, Post.class) + .getResultList(); + + entityManager.createQuery(""" + delete + from PostComment c + where c.post in :posts + """) + .setParameter("posts", posts) + .executeUpdate(); + + posts.forEach(entityManager::remove); + }); + } + + private void insertPostsAndComments() { + doInJPA(entityManager -> { + for (long i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle(String.format("Post no. %d", i)) + .addComment(new PostComment("Good")) + ); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = { + CascadeType.PERSIST, + CascadeType.MERGE + } + ) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public PostComment() {} + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/MySQLBatchInsertLoadTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/MySQLBatchInsertLoadTest.java new file mode 100644 index 000000000..dc48e154f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/MySQLBatchInsertLoadTest.java @@ -0,0 +1,111 @@ +package com.vladmihalcea.hpjp.hibernate.batch; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.sql.PreparedStatement; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class MySQLBatchInsertLoadTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_versioned_data", "true"); + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Test + public void test() { + LOGGER.info("testInsertPosts"); + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + session.doWork(connection -> { + try (PreparedStatement st = connection.prepareStatement(""" + INSERT INTO post (title) + VALUES (?) + """)) { + for (long i = 1; i <= 3; i++) { + st.setString(1, String.format("High-Performance Java Persistence, Part %d", i)); + st.addBatch(); + } + st.executeBatch(); + } + }); + session.createQuery("select p from Post p").getResultList(); + }); + } + + @Test + public void testTransactionless() { + EntityManager entityManager = entityManagerFactory().createEntityManager(); + + for (long i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setTitle( + String.format( + "High-Performance Java Persistence, part %d", + i + ) + ) + ); + } + + try { + entityManager.getTransaction().begin(); + entityManager.getTransaction().commit(); + } catch (Exception e) { + entityManager.getTransaction().rollback(); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/MySQLBatchRewriteTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/MySQLBatchRewriteTest.java new file mode 100644 index 000000000..1502aac6a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/MySQLBatchRewriteTest.java @@ -0,0 +1,275 @@ +package com.vladmihalcea.hpjp.hibernate.batch; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class MySQLBatchRewriteTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "10"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new MySQLDataSourceProvider() + .setRewriteBatchedStatements(true); + /*return new MySQLDataSourceProvider();*/ + } + + @Test + public void testInsertPostsUsingStatement() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + String INSERT = "insert into post (id, title) values (%1$d, 'Post no. %1$d')"; + for (long id = 1; id <= 10; id++) { + statement.addBatch( + String.format(INSERT, id) + ); + } + statement.executeBatch(); + } + }); + }); + } + + @Test + public void testInsertPosts() { + insertPosts(); + } + + @Test + public void testInsertPostsAndComments() { + insertPostsAndComments(); + } + + @Test + public void testUpdatePosts() { + insertPosts(); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + """, Post.class) + .getResultList(); + + posts.forEach(post -> post.setTitle(post.getTitle().replaceAll("no", "nr"))); + }); + } + + @Test + public void testUpdatePostsAndComments() { + insertPostsAndComments(); + + doInJPA(entityManager -> { + List comments = entityManager.createQuery(""" + select c + from PostComment c + join fetch c.post + """, PostComment.class) + .getResultList(); + + comments.forEach(comment -> { + comment.setReview(comment.getReview().replaceAll("Good", "Very good")); + Post post = comment.getPost(); + post.setTitle(post.getTitle().replaceAll("no", "nr")); + }); + }); + } + + @Test + public void testDeletePosts() { + insertPosts(); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + """, Post.class) + .getResultList(); + + posts.forEach(entityManager::remove); + }); + } + + @Test + public void testDeletePostsAndComments() { + insertPostsAndComments(); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + """, Post.class) + .getResultList(); + + posts.forEach(entityManager::remove); + }); + } + + @Test + public void testDeletePostsAndCommentsWithManualChildRemoval() { + insertPostsAndComments(); + + LOGGER.info("testDeletePostsAndCommentsWithManualChildRemoval"); + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + """, Post.class) + .getResultList(); + + for (Post post : posts) { + for (Iterator commentIterator = post.getComments().iterator(); + commentIterator.hasNext(); ) { + PostComment comment = commentIterator.next(); + comment.setPost(null); + commentIterator.remove(); + } + } + entityManager.flush(); + posts.forEach(entityManager::remove); + }); + } + + private void insertPosts() { + doInJPA(entityManager -> { + for (long i = 1; i <= 10; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle(String.format("Post no. %d", i)) + ); + } + }); + } + + private void insertPostsAndComments() { + doInJPA(entityManager -> { + for (long i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle(String.format("Post no. %d", i)) + .addComment( + new PostComment() + .setId(i) + .setReview("Good") + ) + ); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/OracleVersionedBatchingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/OracleVersionedBatchingTest.java new file mode 100644 index 000000000..e05016f29 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/OracleVersionedBatchingTest.java @@ -0,0 +1,230 @@ +package com.vladmihalcea.hpjp.hibernate.batch; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * VersionedBatchingTest - Test to check the JDBC batch support for versioned entities + * + * @author Vlad Mihalcea + */ +public class OracleVersionedBatchingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.jdbc.batch_size", "5"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + return properties; + } + + @Test + public void testInsertPosts() { + LOGGER.info("testInsertPosts"); + insertPosts(); + } + + @Test + public void testInsertPostsAndComments() { + LOGGER.info("testInsertPostsAndComments"); + insertPostsAndComments(); + } + + @Test + public void testUpdatePosts() { + insertPosts(); + + LOGGER.info("testUpdatePosts"); + doInJPA(entityManager -> { + List posts = entityManager.createQuery( + "select p " + + "from Post p ", Post.class) + .getResultList(); + + posts.forEach(post -> post.setTitle(post.getTitle().replaceAll("no", "nr"))); + }); + } + + @Test + public void testUpdatePostsAndComments() { + insertPostsAndComments(); + + LOGGER.info("testUpdatePostsAndComments"); + doInJPA(entityManager -> { + List comments = entityManager.createQuery( + "select c " + + "from PostComment c " + + "join fetch c.post ", PostComment.class) + .getResultList(); + + comments.forEach(comment -> { + comment.setReview(comment.getReview().replaceAll("Good", "Very good")); + Post post = comment.getPost(); + post.setTitle(post.getTitle().replaceAll("no", "nr")); + }); + }); + } + + @Test + public void testDeletePosts() { + insertPosts(); + + LOGGER.info("testDeletePosts"); + doInJPA(entityManager -> { + List posts = entityManager.createQuery( + "select p " + + "from Post p ", Post.class) + .getResultList(); + + posts.forEach(entityManager::remove); + }); + } + + @Test + public void testDeletePostsAndComments() { + insertPostsAndComments(); + + LOGGER.info("testDeletePostsAndComments"); + doInJPA(entityManager -> { + List posts = entityManager.createQuery( + "select p " + + "from Post p " + + "join fetch p.comments ", Post.class) + .getResultList(); + + posts.forEach(entityManager::remove); + }); + } + + private void insertPosts() { + doInJPA(entityManager -> { + for (int i = 0; i < 3; i++) { + entityManager.persist(new Post(String.format("Post no. %d", i + 1))); + } + }); + } + + private void insertPostsAndComments() { + doInJPA(entityManager -> { + for (int i = 0; i < 3; i++) { + Post post = new Post(String.format("Post no. %d", i)); + post.addComment(new PostComment("Good")); + entityManager.persist(post); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + private String title; + + @Version + private short version; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Post() {} + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @ManyToOne + private Post post; + + private String review; + + @Version + private short version; + + public PostComment() {} + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/PersistenceContextExtendedBatchTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/PersistenceContextExtendedBatchTest.java similarity index 90% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/PersistenceContextExtendedBatchTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/PersistenceContextExtendedBatchTest.java index bed9d41ef..e7a39b0e6 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/PersistenceContextExtendedBatchTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/PersistenceContextExtendedBatchTest.java @@ -1,14 +1,14 @@ -package com.vladmihalcea.book.hpjp.hibernate.batch; +package com.vladmihalcea.hpjp.hibernate.batch; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.Session; import org.jboss.logging.Logger; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.EntityManager; -import javax.persistence.EntityTransaction; -import javax.persistence.Id; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; +import jakarta.persistence.Id; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/PostgreSQLBatchRewriteTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/PostgreSQLBatchRewriteTest.java new file mode 100644 index 000000000..00b64c125 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/PostgreSQLBatchRewriteTest.java @@ -0,0 +1,259 @@ +package com.vladmihalcea.hpjp.hibernate.batch; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.*; +import org.junit.Test; +import org.postgresql.ds.PGSimpleDataSource; + +import javax.sql.DataSource; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLBatchRewriteTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "10"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_versioned_data", "true"); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() { + @Override + public DataSource dataSource() { + PGSimpleDataSource dataSource = (PGSimpleDataSource) super.dataSource(); + dataSource.setReWriteBatchedInserts(true); + return dataSource; + } + }; + } + + @Test + public void testInsertPosts() { + insertPosts(); + } + + @Test + public void testInsertPostsAndComments() { + insertPostsAndComments(); + } + + @Test + public void testUpdatePosts() { + insertPosts(); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery( + "select p " + + "from Post p ", Post.class) + .getResultList(); + + posts.forEach(post -> post.setTitle(post.getTitle().replaceAll("no", "nr"))); + }); + } + + @Test + public void testUpdatePostsAndComments() { + insertPostsAndComments(); + + doInJPA(entityManager -> { + List comments = entityManager.createQuery( + "select c " + + "from PostComment c " + + "join fetch c.post ", PostComment.class) + .getResultList(); + + comments.forEach(comment -> { + comment.setReview(comment.getReview().replaceAll("Good", "Very good")); + Post post = comment.getPost(); + post.setTitle(post.getTitle().replaceAll("no", "nr")); + }); + }); + } + + @Test + public void testDeletePosts() { + insertPosts(); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery( + "select p " + + "from Post p ", Post.class) + .getResultList(); + + posts.forEach(entityManager::remove); + }); + } + + @Test + public void testDeletePostsAndComments() { + insertPostsAndComments(); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery( + "select p " + + "from Post p " + + "join fetch p.comments ", Post.class) + .getResultList(); + + posts.forEach(entityManager::remove); + }); + } + + @Test + public void testDeletePostsAndCommentsWithManualChildRemoval() { + insertPostsAndComments(); + + LOGGER.info("testDeletePostsAndCommentsWithManualChildRemoval"); + doInJPA(entityManager -> { + List posts = entityManager.createQuery( + "select p " + + "from Post p " + + "join fetch p.comments ", Post.class) + .getResultList(); + + for (Post post : posts) { + for (Iterator commentIterator = post.getComments().iterator(); + commentIterator.hasNext(); ) { + PostComment comment = commentIterator.next(); + comment.setPost(null); + commentIterator.remove(); + } + } + entityManager.flush(); + posts.forEach(entityManager::remove); + }); + } + + private void insertPosts() { + doInJPA(entityManager -> { + for (int i = 0; i < 10; i++) { + entityManager.persist( + new Post(String.format("Post no. %d", i + 1)) + ); + } + }); + } + + private void insertPostsAndComments() { + doInJPA(entityManager -> { + for (int i = 0; i < 3; i++) { + Post post = new Post(String.format("Post no. %d", i)); + post.addComment(new PostComment("Good")); + entityManager.persist(post); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + private String title; + + public Post() {} + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public PostComment() {} + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/SQLServerBulkCopyForBatchInsertTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/SQLServerBulkCopyForBatchInsertTest.java new file mode 100644 index 000000000..5fbaddd73 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/SQLServerBulkCopyForBatchInsertTest.java @@ -0,0 +1,262 @@ +package com.vladmihalcea.hpjp.hibernate.batch; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.SQLServerDataSourceProvider; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerBulkCopyForBatchInsertTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.SQLSERVER; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "10"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return ((SQLServerDataSourceProvider) super.dataSourceProvider()) + .setUseBulkCopyForBatchInsert(true) + .setSendStringParametersAsUnicode(true); + } + + @Test + public void testInsertPosts() { + insertPosts(); + } + + @Test + public void testInsertPostsAndComments() { + insertPostsAndComments(); + } + + @Test + public void testUpdatePosts() { + insertPosts(); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + posts.forEach(post -> post.setTitle(post.getTitle().replaceAll("no", "nr"))); + }); + } + + @Test + public void testUpdatePostsAndComments() { + insertPostsAndComments(); + + doInJPA(entityManager -> { + List comments = entityManager.createQuery(""" + select c + from PostComment c + join fetch c.post + """, PostComment.class) + .getResultList(); + + comments.forEach(comment -> { + comment.setReview(comment.getReview().replaceAll("Good", "Very good")); + Post post = comment.getPost(); + post.setTitle(post.getTitle().replaceAll("no", "nr")); + }); + }); + } + + @Test + public void testDeletePosts() { + insertPosts(); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + posts.forEach(entityManager::remove); + }); + } + + @Test + public void testDeletePostsAndComments() { + insertPostsAndComments(); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + """, Post.class) + .getResultList(); + + posts.forEach(entityManager::remove); + }); + } + + @Test + public void testDeletePostsAndCommentsWithManualChildRemoval() { + insertPostsAndComments(); + + LOGGER.info("testDeletePostsAndCommentsWithManualChildRemoval"); + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + """, Post.class) + .getResultList(); + + for (Post post : posts) { + for (Iterator commentIterator = post.getComments().iterator(); + commentIterator.hasNext(); ) { + PostComment comment = commentIterator.next(); + comment.setPost(null); + commentIterator.remove(); + } + } + entityManager.flush(); + posts.forEach(entityManager::remove); + }); + } + + private void insertPosts() { + doInJPA(entityManager -> { + for (int i = 0; i < 10; i++) { + entityManager.persist( + new Post(String.format("Post no. %d", i + 1)) + ); + } + }); + } + + private void insertPostsAndComments() { + doInJPA(entityManager -> { + for (int i = 0; i < 3; i++) { + Post post = new Post(String.format("Post no. %d", i)); + post.addComment(new PostComment("Good")); + entityManager.persist(post); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + private String title; + + public Post() {} + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public PostComment() {} + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/StatelessSessionBatchingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/StatelessSessionBatchingTest.java new file mode 100644 index 000000000..07853d688 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/StatelessSessionBatchingTest.java @@ -0,0 +1,260 @@ +package com.vladmihalcea.hpjp.hibernate.batch; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import jakarta.persistence.criteria.Root; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaDelete; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.stream.LongStream; + +/** + * BatchingTest - Test to check the JDBC batch support + * + * @author Vlad Mihalcea + */ +public class StatelessSessionBatchingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + private static final int POST_COUNT = 3; + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.STATEMENT_BATCH_SIZE, "50"); + } + + @Test + public void testInsertPosts() { + LOGGER.info("testInsertPosts"); + insertPosts(); + } + + @Test + public void testInsertPostsAndComments() { + LOGGER.info("testInsertPostsAndComments"); + insertPostsAndComments(); + } + + @Test + public void testUpdatePosts() { + LOGGER.info("testUpdatePosts"); + insertPosts(); + + LOGGER.info("Load Post entities"); + + List posts = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .setMaxResults(POST_COUNT) + .getResultList(); + }); + + posts.forEach(post -> + post.setTitle(post.getTitle().replaceAll("no", "nr")) + ); + + LOGGER.info("Update Post entities"); + + doInStatelessSession(session -> { + posts.forEach(session::update); + }); + } + + @Test + public void testDeletePosts() { + insertPosts(); + + LOGGER.info("testDeletePosts"); + doInStatelessSession(session -> { + List posts = session.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + posts.forEach(session::delete); + }); + } + + @Test + public void testDeletePostsAndComments() { + LOGGER.info("testDeletePostsAndComments"); + insertPostsAndComments(); + + List posts = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .setMaxResults(POST_COUNT) + .getResultList(); + }); + + doInStatelessSession(session -> { + HibernateCriteriaBuilder builder = session.getCriteriaBuilder(); + + JpaCriteriaDelete criteria = builder.createCriteriaDelete(PostComment.class); + Root post = criteria.from(PostComment.class); + session.createQuery( + criteria + .where(builder.in(post.get("post"), posts)) + ) + .executeUpdate(); + + posts.forEach(session::delete); + }); + } + + private void insertPosts() { + doInStatelessSession(session -> { + for (long i = 1; i <= POST_COUNT; i++) { + session.insert( + new Post() + .setId(i) + .setTitle(String.format("Post no. %d", i)) + ); + } + }); + } + + private void insertPostsAndComments() { + doInStatelessSession(session -> { + List posts = LongStream.rangeClosed(1, POST_COUNT).boxed() + .map(i -> { + Post post = new Post() + .setId(i) + .setTitle(String.format("Post no. %d", i)); + + session.insert(post); + + return post; + }) + .toList(); + + final int COMMENT_COUNT = 5; + + posts.forEach(post -> { + for (long i = 1; i <= COMMENT_COUNT; i++) { + session.insert( + new PostComment() + .setPost(post) + .setReview( + String.format( + "Post comment no. %d", + (post.getId() - 1) * COMMENT_COUNT + i + ) + ) + ); + } + }); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public PostComment() {} + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/VersionedBatchingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/VersionedBatchingTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/VersionedBatchingTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/VersionedBatchingTest.java index cdbb13eef..3ec45bd3c 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/VersionedBatchingTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/VersionedBatchingTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.batch; +package com.vladmihalcea.hpjp.hibernate.batch; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -140,7 +140,11 @@ public static class Post { private String title; @Version - private int version; + private short version; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); public Post() {} @@ -152,10 +156,6 @@ public Post(String title) { this.title = title; } - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - public Long getId() { return id; } @@ -196,7 +196,7 @@ public static class PostComment { private String review; @Version - private int version; + private short version; public PostComment() {} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/AbstractBatchUpdateExceptionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/AbstractBatchUpdateExceptionTest.java new file mode 100644 index 000000000..0a5af92ff --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/AbstractBatchUpdateExceptionTest.java @@ -0,0 +1,75 @@ +package com.vladmihalcea.hpjp.hibernate.batch.failure; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.sql.BatchUpdateException; +import java.sql.PreparedStatement; + +/** + * @author Vlad Mihalcea + */ +public abstract class AbstractBatchUpdateExceptionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testInsertConstraintViolation() { + LOGGER.info("testInsertPosts"); + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + session.doWork(connection -> { + try (PreparedStatement st = connection.prepareStatement(""" + INSERT INTO post (id, title) + VALUES (?, ?) + """)) { + for (long i = 1; i <= 3; i++) { + st.setLong(1, i % 2); + st.setString(2, String.format("High-Performance Java Persistence, Part %d", i)); + st.addBatch(); + } + st.executeBatch(); + } catch (BatchUpdateException e) { + onBatchUpdateException(e); + } + }); + }); + } + + protected abstract void onBatchUpdateException(BatchUpdateException e); + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/MySQLBatchUpdateExceptionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/MySQLBatchUpdateExceptionTest.java new file mode 100644 index 000000000..1ab612f4f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/MySQLBatchUpdateExceptionTest.java @@ -0,0 +1,29 @@ +package com.vladmihalcea.hpjp.hibernate.batch.failure; + +import com.vladmihalcea.hpjp.util.providers.Database; + +import java.sql.BatchUpdateException; +import java.util.Arrays; + +import static org.junit.Assert.assertSame; + +/** + * @author Vlad Mihalcea + */ +public class MySQLBatchUpdateExceptionTest extends AbstractBatchUpdateExceptionTest { + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected void onBatchUpdateException(BatchUpdateException e) { + assertSame(3, e.getUpdateCounts().length); + LOGGER.info(e.getMessage()); + LOGGER.info( + "Batch has managed to process {} entries", + Arrays.stream(e.getUpdateCounts()).asLongStream().filter(l -> l > 0).count() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/OracleBatchUpdateExceptionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/OracleBatchUpdateExceptionTest.java new file mode 100644 index 000000000..1896e15e5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/OracleBatchUpdateExceptionTest.java @@ -0,0 +1,26 @@ +package com.vladmihalcea.hpjp.hibernate.batch.failure; + +import com.vladmihalcea.hpjp.util.providers.Database; + +import java.sql.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +/** + * @author Vlad Mihalcea + */ +public class OracleBatchUpdateExceptionTest extends AbstractBatchUpdateExceptionTest { + + @Override + protected Database database() { + return Database.ORACLE; + } + + @Override + protected void onBatchUpdateException(BatchUpdateException e) { + assertSame(2, e.getUpdateCounts().length); + LOGGER.info(e.getMessage()); + LOGGER.info("Batch has managed to process {} entries", e.getUpdateCounts().length); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/PostgreSQLBatchUpdateExceptionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/PostgreSQLBatchUpdateExceptionTest.java new file mode 100644 index 000000000..945dc733a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/PostgreSQLBatchUpdateExceptionTest.java @@ -0,0 +1,23 @@ +package com.vladmihalcea.hpjp.hibernate.batch.failure; + +import com.vladmihalcea.hpjp.util.providers.Database; + +import java.sql.BatchUpdateException; + +import static org.junit.Assert.assertSame; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLBatchUpdateExceptionTest extends AbstractBatchUpdateExceptionTest { + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void onBatchUpdateException(BatchUpdateException e) { + LOGGER.info("Batch failure", e); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/SQLServerBatchUpdateExceptionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/SQLServerBatchUpdateExceptionTest.java new file mode 100644 index 000000000..9b54752db --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/failure/SQLServerBatchUpdateExceptionTest.java @@ -0,0 +1,28 @@ +package com.vladmihalcea.hpjp.hibernate.batch.failure; + +import com.vladmihalcea.hpjp.util.providers.Database; + +import java.sql.BatchUpdateException; +import java.util.Arrays; + +import static org.junit.Assert.assertSame; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerBatchUpdateExceptionTest extends AbstractBatchUpdateExceptionTest { + + @Override + protected Database database() { + return Database.SQLSERVER; + } + + @Override + protected void onBatchUpdateException(BatchUpdateException e) { + assertSame(3, e.getUpdateCounts().length); + LOGGER.info(e.getMessage()); + LOGGER.info("Batch has managed to process {} entries", + Arrays.stream(e.getUpdateCounts()).asLongStream().filter(l -> l > 0).count() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/IdentityBatchingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/IdentityBatchingTest.java similarity index 91% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/IdentityBatchingTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/IdentityBatchingTest.java index 5a25b1842..abf42ddac 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/batch/IdentityBatchingTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/IdentityBatchingTest.java @@ -1,10 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.batch; +package com.vladmihalcea.hpjp.hibernate.batch.identity; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.dialect.Dialect; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.List; import java.util.Properties; @@ -78,7 +77,7 @@ protected int itemsCount() { } protected int batchSize() { - return Integer.valueOf(Dialect.DEFAULT_BATCH_SIZE); + return dialect().getDefaultStatementBatchSize(); } @Entity(name = "Post") diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/MySQLIdentityStatelessSessionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/MySQLIdentityStatelessSessionTest.java new file mode 100644 index 000000000..c84688127 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/MySQLIdentityStatelessSessionTest.java @@ -0,0 +1,84 @@ +package com.vladmihalcea.hpjp.hibernate.batch.identity.stateless; + +import com.vladmihalcea.hpjp.hibernate.batch.identity.stateless.model.BatchInsertPost; +import com.vladmihalcea.hpjp.hibernate.batch.identity.stateless.model.Post; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.StatelessSession; +import org.hibernate.Transaction; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class MySQLIdentityStatelessSessionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + BatchInsertPost.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected void beforeInit() { + executeStatement("drop table if exists post cascade"); + executeStatement("create table post (version smallint, created_on datetime(6), id bigint not null AUTO_INCREMENT, updated_on datetime(6), created_by varchar(255), title varchar(255), updated_by varchar(255), primary key (id))" + ); + } + + @Test + public void testPersist() { + StatelessSession session = null; + Transaction transaction = null; + try { + session = sessionFactory().withStatelessOptions().openStatelessSession(); + transaction = session.beginTransaction(); + int i = 1; + + session.setJdbcBatchSize(5); + + session.insert( + new BatchInsertPost().setTitle( + String.format( + "High-Performance Java Persistence, Part %d", + i++ + ) + ) + ); + session.insert( + new BatchInsertPost().setTitle( + String.format( + "High-Performance Java Persistence, Part %d", + i++ + ) + ) + ); + if(transaction != null) { + transaction.commit(); + } + } catch (Exception e) { + LOGGER.error("INSERT failure", e); + if(transaction != null) { + transaction.rollback(); + } + } finally { + if(session != null) { + session.close(); + } + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/NoIdentityGenerator.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/NoIdentityGenerator.java new file mode 100644 index 000000000..90bbfeeb9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/NoIdentityGenerator.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.hibernate.batch.identity.stateless; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.IdentifierGenerator; +import org.hibernate.id.factory.spi.StandardGenerator; + +/** + * @author Vlad Mihalcea + */ +public class NoIdentityGenerator implements IdentifierGenerator, StandardGenerator { + + @Override + public Object generate(SharedSessionContractImplementor session, Object obj) { + return null; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/model/AbstractPost.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/model/AbstractPost.java new file mode 100644 index 000000000..005b815b4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/model/AbstractPost.java @@ -0,0 +1,85 @@ +package com.vladmihalcea.hpjp.hibernate.batch.identity.stateless.model; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Version; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@MappedSuperclass +public abstract class AbstractPost { + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_on") + private LocalDateTime updatedOn; + + @Column(name = "updated_by") + private String updatedBy; + + @Version + private Short version; + + public String getTitle() { + return title; + } + + public T setTitle(String title) { + this.title = title; + return (T) this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public T setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return (T) this; + } + + public String getCreatedBy() { + return createdBy; + } + + public T setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return (T) this; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public T setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + return (T) this; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public T setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + return (T) this; + } + + public Short getVersion() { + return version; + } + + public T setVersion(Short version) { + this.version = version; + return (T) this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/model/BatchInsertPost.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/model/BatchInsertPost.java new file mode 100644 index 000000000..9a3e3ca54 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/model/BatchInsertPost.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hpjp.hibernate.batch.identity.stateless.model; + +import jakarta.persistence.*; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.SQLInsert; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +@SQLInsert(sql = "insert into post (created_by,created_on,title,updated_by,updated_on,version,id) values (?,?,?,?,?,?,default)") +public class BatchInsertPost extends AbstractPost { + + @Id + @Column(insertable = false) + @GeneratedValue(generator = "mysql_identity_generator") + @GenericGenerator( + name = "mysql_identity_generator", + strategy = "com.vladmihalcea.hpjp.hibernate.batch.identity.stateless.NoIdentityGenerator" + ) + private Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/model/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/model/Post.java new file mode 100644 index 000000000..2a4a1049f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/batch/identity/stateless/model/Post.java @@ -0,0 +1,25 @@ +package com.vladmihalcea.hpjp.hibernate.batch.identity.stateless.model; + +import jakarta.persistence.*; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.SQLInsert; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post extends AbstractPost { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/binding/EntityBindingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/binding/EntityBindingTest.java new file mode 100644 index 000000000..49c63e9e9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/binding/EntityBindingTest.java @@ -0,0 +1,167 @@ +package com.vladmihalcea.hpjp.hibernate.binding; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.DataAccessException; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.Post; +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.PostComment; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class EntityBindingTest extends AbstractTest { + + public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; + + public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"; + + public static final String INSERT_POST_DETAILS= "insert into post_details (id, created_on, version) values (?, ?, ?)"; + + private BlogEntityProvider entityProvider = new BlogEntityProvider(); + + private Long id = 1L; + private long expectedCount = 2; + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + public void init() { + super.init(); + doInJDBC(connection -> { + try ( + PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); + PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); + PreparedStatement postDetailsStatement = connection.prepareStatement(INSERT_POST_DETAILS); + ) { + + int postCount = getPostCount(); + int postCommentCount = getPostCommentCount(); + + int index; + + for (int i = 0; i < postCount; i++) { + if (i > 0 && i % 100 == 0) { + postStatement.executeBatch(); + postDetailsStatement.executeBatch(); + } + + index = 0; + postStatement.setString(++index, String.format("Post no. %1$d", i)); + postStatement.setInt(++index, i); + postStatement.setLong(++index, i); + postStatement.addBatch(); + + index = 0; + postDetailsStatement.setInt(++index, i); + postDetailsStatement.setTimestamp(++index, new Timestamp(System.currentTimeMillis())); + postDetailsStatement.setInt(++index, i); + postDetailsStatement.addBatch(); + } + postStatement.executeBatch(); + postDetailsStatement.executeBatch(); + + for (int i = 0; i < postCount; i++) { + for (int j = 0; j < postCommentCount; j++) { + index = 0; + postCommentStatement.setLong(++index, i); + postCommentStatement.setString(++index, String.format("Post comment %1$d", j)); + postCommentStatement.setInt(++index, i); + postCommentStatement.setLong(++index, (postCommentCount * i) + j); + postCommentStatement.addBatch(); + if (j % 100 == 0) { + postCommentStatement.executeBatch(); + } + } + } + postCommentStatement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + } + + @Test + public void testJdbcOneToManyMapping() { + doInJDBC(connection -> { + try (PreparedStatement statement = connection.prepareStatement(""" + SELECT + p.id, p.title, p.version, + pc.id, pc.review, pc.version + FROM post AS p + JOIN post_comment AS pc ON p.id = pc.post_id + WHERE + p.id BETWEEN ? AND ? + 1 + """ + )) { + statement.setLong(1, id); + statement.setLong(2, id); + try (ResultSet resultSet = statement.executeQuery()) { + List posts = toPosts(resultSet); + assertEquals(expectedCount, posts.size()); + } + } catch (SQLException e) { + throw new DataAccessException( e); + } + }); + } + + private List toPosts(ResultSet resultSet) throws SQLException { + Map postMap = new LinkedHashMap<>(); + while (resultSet.next()) { + Long postId = resultSet.getLong(1); + Post post = postMap.get(postId); + if(post == null) { + post = new Post(postId); + postMap.put(postId, post); + post.setTitle(resultSet.getString(2)); + post.setVersion(resultSet.getShort(3)); + } + PostComment comment = new PostComment(); + comment.setId(resultSet.getLong(4)); + comment.setReview(resultSet.getString(5)); + comment.setVersion(resultSet.getShort(6)); + post.addComment(comment); + } + return new ArrayList<>(postMap.values()); + } + + @Test + public void testJPAParameterBinding() { + doInJPA(entityManager -> { + List posts = entityManager.createQuery( + "select p " + + "from Post p " + + "join fetch p.comments " + + "where " + + " p.id BETWEEN :id AND :id + 1", + Post.class) + .setParameter("id", id) + .getResultList(); + assertEquals(expectedCount, posts.size()); + }); + } + + protected int getPostCount() { + return 10; + } + + protected int getPostCommentCount() { + return 10; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/binding/ParameterBindingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/binding/ParameterBindingTest.java similarity index 95% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/binding/ParameterBindingTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/binding/ParameterBindingTest.java index 3e2f4b752..dad01f6b3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/binding/ParameterBindingTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/binding/ParameterBindingTest.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.binding; +package com.vladmihalcea.hpjp.hibernate.binding; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.exception.DataAccessException; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.DataAccessException; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; import org.junit.Test; import java.sql.PreparedStatement; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bootstrap/AbstractJPAProgrammaticBootstrapTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bootstrap/AbstractJPAProgrammaticBootstrapTest.java new file mode 100644 index 000000000..ecec643fc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bootstrap/AbstractJPAProgrammaticBootstrapTest.java @@ -0,0 +1,116 @@ +package com.vladmihalcea.hpjp.hibernate.bootstrap; + +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.PersistenceUnitInfoImpl; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.junit.After; +import org.junit.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.spi.PersistenceUnitInfo; +import javax.sql.DataSource; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Vlad Mihalcea + */ +public abstract class AbstractJPAProgrammaticBootstrapTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + private EntityManagerFactory emf; + + public EntityManagerFactory entityManagerFactory() { + return emf; + } + + @Before + public void init() { + PersistenceUnitInfo persistenceUnitInfo = persistenceUnitInfo(getClass().getSimpleName()); + + Map configuration = new HashMap<>(); + + Integrator integrator = integrator(); + if (integrator != null) { + configuration.put("hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList(integrator)); + } + + emf = new HibernatePersistenceProvider().createContainerEntityManagerFactory( + persistenceUnitInfo, + configuration + ); + } + + @After + public void destroy() { + emf.close(); + } + + protected PersistenceUnitInfoImpl persistenceUnitInfo(String name) { + PersistenceUnitInfoImpl persistenceUnitInfo = new PersistenceUnitInfoImpl( + name, entityClassNames(), properties() + ); + + String[] resources = resources(); + if (resources != null) { + persistenceUnitInfo.getMappingFileNames().addAll(Arrays.asList(resources)); + } + + return persistenceUnitInfo; + } + + protected abstract Class[] entities(); + + protected String[] resources() { + return null; + } + + protected List entityClassNames() { + return Arrays.asList(entities()).stream().map(Class::getName).collect(Collectors.toList()); + } + + protected Properties properties() { + Properties properties = new Properties(); + properties.put("hibernate.hbm2ddl.auto", "create-drop"); + DataSource dataSource = newDataSource(); + if (dataSource != null) { + properties.put("hibernate.connection.datasource", dataSource); + } + properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); + + return properties; + } + + protected DataSource newDataSource() { + return proxyDataSource() + ? dataSourceProxyType().dataSource(dataSourceProvider().dataSource()) + : dataSourceProvider().dataSource(); + } + + protected DataSourceProxyType dataSourceProxyType() { + return DataSourceProxyType.DATA_SOURCE_PROXY; + } + + protected boolean proxyDataSource() { + return true; + } + + protected DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + protected Database database() { + return Database.HSQLDB; + } + + protected Integrator integrator() { + return null; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bootstrap/BootstrapTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bootstrap/BootstrapTest.java new file mode 100644 index 000000000..4cf4cc955 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bootstrap/BootstrapTest.java @@ -0,0 +1,99 @@ +package com.vladmihalcea.hpjp.hibernate.bootstrap; + +import com.vladmihalcea.hpjp.util.transaction.JPATransactionVoidFunction; +import org.junit.Test; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +public class BootstrapTest extends AbstractJPAProgrammaticBootstrapTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Test + public void test() { + doInJPA(entityManager -> { + for (long id = 1; id <= 3; id++) { + Post post = new Post(); + post.setId(id); + post.setTitle( + String.format( + "High-Performance Java Persistence, Part %d", id + ) + ); + entityManager.persist(post); + } + }); + } + + protected void doInJPA(JPATransactionVoidFunction function) { + EntityManager entityManager = null; + EntityTransaction txn = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + function.beforeTransactionCompletion(); + txn = entityManager.getTransaction(); + txn.begin(); + function.accept(entityManager); + if ( !txn.getRollbackOnly() ) { + txn.commit(); + } + else { + try { + txn.rollback(); + } + catch (Exception e) { + LOGGER.error( "Rollback failure", e ); + } + } + } catch (Throwable t) { + if ( txn != null && txn.isActive() ) { + try { + txn.rollback(); + } + catch (Exception e) { + LOGGER.error( "Rollback failure", e ); + } + } + throw t; + } finally { + function.afterTransactionCompletion(); + if (entityManager != null) { + entityManager.close(); + } + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bulk/JPQLBulkUpdateDeleteTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bulk/JPQLBulkUpdateDeleteTest.java new file mode 100644 index 000000000..4c2d5fbbe --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bulk/JPQLBulkUpdateDeleteTest.java @@ -0,0 +1,288 @@ +package com.vladmihalcea.hpjp.hibernate.bulk; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JPQLBulkUpdateDeleteTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testBulk() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setStatus(PostStatus.APPROVED) + ); + + entityManager.persist( + new Post() + .setId(2L) + .setTitle("Spam title") + ); + + entityManager.persist( + new Post() + .setId(3L) + .setMessage("Spam message") + ); + + entityManager.persist( + new PostComment() + .setId(1L) + .setPost(entityManager.getReference(Post.class, 1L)) + .setMessage("Spam comment") + ); + }); + + doInJPA(entityManager -> { + assertEquals(2, flagPostSpam(entityManager)); + assertEquals(1, flagPostCommentSpam(entityManager)); + }); + + doInJPA(entityManager -> { + assertEquals(2, + entityManager.createQuery(""" + update Post + set updatedOn = :timestamp + where status = :status + """) + .setParameter("timestamp", Timestamp.valueOf(LocalDateTime.now().minusDays(7))) + .setParameter("status", PostStatus.SPAM) + .executeUpdate() + ); + + assertEquals(1, + entityManager.createQuery(""" + update PostComment + set updatedOn = :timestamp + where status = :status + """) + .setParameter("timestamp", Timestamp.valueOf(LocalDateTime.now().minusDays(3))) + .setParameter("status", PostStatus.SPAM) + .executeUpdate() + ); + }); + + doInJPA(entityManager -> { + assertEquals(2, deletePostSpam(entityManager)); + assertEquals(1, deletePostCommentSpam(entityManager)); + }); + } + + public int flagPostSpam(EntityManager entityManager) { + int updateCount = entityManager.createQuery(""" + update Post + set + updatedOn = CURRENT_TIMESTAMP, + status = :newStatus + where + status = :oldStatus and + ( + lower(title) like :spamToken or + lower(message) like :spamToken + ) + """) + .setParameter("newStatus", PostStatus.SPAM) + .setParameter("oldStatus", PostStatus.PENDING) + .setParameter("spamToken", "%spam%") + .executeUpdate(); + + return updateCount; + } + + public int flagPostCommentSpam(EntityManager entityManager) { + int updateCount = entityManager.createQuery(""" + update PostComment + set + updatedOn = CURRENT_TIMESTAMP, + status = :newStatus + where + status = :oldStatus and + lower(message) like :spamToken + """) + .setParameter("newStatus", PostStatus.SPAM) + .setParameter("oldStatus", PostStatus.PENDING) + .setParameter("spamToken", "%spam%") + .executeUpdate(); + + return updateCount; + } + + public int deletePostSpam(EntityManager entityManager) { + int deleteCount = entityManager.createQuery(""" + delete from Post + where + status = :status and + updatedOn <= :validityThreshold + """) + .setParameter("status", PostStatus.SPAM) + .setParameter( + "validityThreshold", + Timestamp.valueOf( + LocalDateTime.now().minusDays(7) + ) + ) + .executeUpdate(); + + return deleteCount; + } + + public int deletePostCommentSpam(EntityManager entityManager) { + int deleteCount = entityManager.createQuery(""" + delete from PostComment + where + status = :status and + updatedOn <= :validityThreshold + """) + .setParameter("status", PostStatus.SPAM) + .setParameter( + "validityThreshold", + Timestamp.valueOf( + LocalDateTime.now().minusDays(3) + ) + ) + .executeUpdate(); + + return deleteCount; + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM + } + + @MappedSuperclass + public static abstract class PostModerate { + + @Enumerated(EnumType.ORDINAL) + @Column(columnDefinition = "smallint") + private PostStatus status = PostStatus.PENDING; + + @Column(name = "updated_on") + private Date updatedOn = new Date(); + + public PostStatus getStatus() { + return status; + } + + public T setStatus(PostStatus status) { + this.status = status; + return (T) this; + } + + public Date getUpdatedOn() { + return updatedOn; + } + + public T setUpdatedOn(Date updatedOn) { + this.updatedOn = updatedOn; + return (T) this; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post extends PostModerate { + + @Id + private Long id; + + private String title; + + private String message; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public String getMessage() { + return message; + } + + public Post setMessage(String message) { + this.message = message; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment extends PostModerate { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String message; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getMessage() { + return message; + } + + public PostComment setMessage(String message) { + this.message = message; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancedOneToOneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancedOneToOneTest.java new file mode 100644 index 000000000..0f92a333f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancedOneToOneTest.java @@ -0,0 +1,169 @@ +package com.vladmihalcea.hpjp.hibernate.bytecode; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.LazyToOne; +import org.hibernate.annotations.LazyToOneOption; +import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.persistence.*; +import java.util.Date; + +import static junit.framework.TestCase.fail; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +@RunWith(BytecodeEnhancerRunner.class) +public class BytecodeEnhancedOneToOneTest extends AbstractTest { + + //Needed as otherwise we get a No unique field [LOGGER] error + private final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostDetails.class + }; + } + + @Test + public void testLazyLoadingNoProxy() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence, 1st Part") + .setDetails( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + ) + ); + }); + + Post post = doInJPA(entityManager -> { + return entityManager.find(Post.class, 1L); + }); + + try { + assertNotNull(post.getDetails().getCreatedOn()); + + fail("Should throw LazyInitializationException"); + } catch (Exception expected) { + LOGGER.info("The @OneToOne association was fetched lazily", expected); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToOne( + mappedBy = "post", + fetch = FetchType.LAZY, + cascade = CascadeType.ALL + ) + @LazyToOne(LazyToOneOption.NO_PROXY) + private PostDetails details; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancedTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancedTest.java new file mode 100644 index 000000000..c9d7086a3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancedTest.java @@ -0,0 +1,52 @@ +package com.vladmihalcea.hpjp.hibernate.bytecode; + +import com.vladmihalcea.hpjp.hibernate.forum.Post; +import com.vladmihalcea.hpjp.hibernate.forum.PostComment; +import com.vladmihalcea.hpjp.hibernate.forum.PostDetails; +import com.vladmihalcea.hpjp.hibernate.forum.Tag; +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +/** + * @author Vlad Mihalcea + */ +public class BytecodeEnhancedTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + PostComment.class, + Tag.class + }; + } + + @Test + public void testDirtyChecking() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("Good") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("Excellent") + ) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + post.setTitle("High-Performance Java Persistence, 2nd edition"); + entityManager.flush(); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancementBidirectionalOneToManyAssociationManagementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancementBidirectionalOneToManyAssociationManagementTest.java new file mode 100644 index 000000000..3e8a0f06b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancementBidirectionalOneToManyAssociationManagementTest.java @@ -0,0 +1,169 @@ +package com.vladmihalcea.hpjp.hibernate.bytecode; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner; +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +@RunWith(BytecodeEnhancerRunner.class) +@EnhancementOptions( + biDirectionalAssociationManagement = true +) +public class BytecodeEnhancementBidirectionalOneToManyAssociationManagementTest extends AbstractTest { + + //Needed as otherwise we get a No unique field [LOGGER] error + private final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Test + public void testSetParentAssociation() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + PostComment comment = new PostComment() + .setId(1L) + .setReview("Excellent book to understand Java Persistence"); + + assertNull(comment.getPost()); + post.setComments(List.of(comment)); + assertSame(post, comment.getPost()); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + assertEquals("High-Performance Java Persistence", comment.getPost().getTitle()); + assertEquals("Excellent book to understand Java Persistence", comment.getReview()); + }); + } + + @Test + public void testSetChildAssociation() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + PostComment comment = new PostComment() + .setId(1L) + .setReview("Excellent book to understand Java Persistence"); + + assertFalse(post.getComments().contains(comment)); + comment.setPost(post); + assertTrue(post.getComments().contains(comment)); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + assertEquals("High-Performance Java Persistence", comment.getPost().getTitle()); + assertEquals("Excellent book to understand Java Persistence", comment.getReview()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public void setComments(List comments) { + this.comments = comments; + } + } + + @Entity + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancementBidirectionalOneToOneAssociationManagementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancementBidirectionalOneToOneAssociationManagementTest.java new file mode 100644 index 000000000..3437547c2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancementBidirectionalOneToOneAssociationManagementTest.java @@ -0,0 +1,187 @@ +package com.vladmihalcea.hpjp.hibernate.bytecode; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.LazyToOne; +import org.hibernate.annotations.LazyToOneOption; +import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner; +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.persistence.*; +import java.util.Date; + +import static junit.framework.TestCase.fail; +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +@RunWith(BytecodeEnhancerRunner.class) +@EnhancementOptions( + biDirectionalAssociationManagement = true +) +public class BytecodeEnhancementBidirectionalOneToOneAssociationManagementTest extends AbstractTest { + + //Needed as otherwise we get a No unique field [LOGGER] error + private final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostDetails.class + }; + } + + @Test + public void testSetParentAssociation() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + PostDetails details = new PostDetails() + .setCreatedBy("Vlad Mihalcea"); + + assertNull(details.getPost()); + post.setDetails(details); + assertSame(post, details.getPost()); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + PostDetails details = entityManager.find(PostDetails.class, 1L); + assertEquals("High-Performance Java Persistence", details.getPost().getTitle()); + assertEquals("Vlad Mihalcea", details.getCreatedBy()); + }); + } + + @Test + public void testSetChildAssociation() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + PostDetails details = new PostDetails() + .setCreatedBy("Vlad Mihalcea"); + + assertNull(post.getDetails()); + details.setPost(post); + assertSame(details, post.getDetails()); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + PostDetails details = entityManager.find(PostDetails.class, 1L); + + assertEquals("High-Performance Java Persistence", details.getPost().getTitle()); + assertEquals("Vlad Mihalcea", details.getCreatedBy()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToOne( + mappedBy = "post", + fetch = FetchType.LAZY, + cascade = CascadeType.ALL + ) + @LazyToOne(LazyToOneOption.NO_PROXY) + private PostDetails details; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + this.details = details; + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancementDirtyCheckingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancementDirtyCheckingTest.java new file mode 100644 index 000000000..6afa65dd0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/bytecode/BytecodeEnhancementDirtyCheckingTest.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.hibernate.bytecode; + +import com.vladmihalcea.hpjp.hibernate.forum.Tag; +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * @author Vlad Mihalcea + */ +@RunWith(BytecodeEnhancerRunner.class) +public class BytecodeEnhancementDirtyCheckingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Tag.class + }; + } + + @Test + public void testDirtyChecking() { + doInJPA(entityManager -> { + Tag tag = new Tag(); + tag.setId(1L); + tag.setName("High-Performance Hibernate"); + + entityManager.persist(tag); + }); + + doInJPA(entityManager -> { + Tag tag = entityManager.find(Tag.class, 1L); + + tag.setName("High-Performance Java Persistence"); + entityManager.flush(); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/CollectionCacheTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/CollectionCacheTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/CollectionCacheTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/CollectionCacheTest.java index 52d133925..504affc6e 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/CollectionCacheTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/CollectionCacheTest.java @@ -1,15 +1,14 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache; +package com.vladmihalcea.hpjp.hibernate.cache; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.ObjectNotFoundException; -import org.hibernate.SQLQuery; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.query.NativeQuery; import org.junit.Before; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.sql.PreparedStatement; import java.util.ArrayList; import java.util.List; @@ -37,7 +36,7 @@ protected Class[] entities() { protected Properties properties() { Properties properties = super.properties(); properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); + properties.put("hibernate.cache.region.factory_class", "jcache"); return properties; } @@ -175,12 +174,12 @@ public void testConsistencyWhenSQLUpdating() { entityManager.createNativeQuery( "update Commit c " + "set c.review = true ") - .unwrap(SQLQuery.class) + .unwrap(NativeQuery.class) .addSynchronizedEntityClass(Commit.class) .executeUpdate(); }); doInJPA(entityManager -> { - Repository repository = (Repository) + Repository repository = entityManager.find(Repository.class, 1L); for(Commit commit : repository.getCommits()) { assertTrue(commit.review); @@ -208,7 +207,7 @@ public void testConsistencyWhenManuallySQLUpdating() { statement.executeUpdate(); } }); - entityManager.getEntityManagerFactory().unwrap(SessionFactory.class).getCache().evictCollection( + entityManager.getEntityManagerFactory().unwrap(SessionFactory.class).getCache().evictCollectionData( Repository.class.getName() + ".commits", repository.getId() ); diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/CollectionLoadedStateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/CollectionLoadedStateTest.java new file mode 100644 index 000000000..26410762f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/CollectionLoadedStateTest.java @@ -0,0 +1,160 @@ +package com.vladmihalcea.hpjp.hibernate.cache; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class CollectionLoadedStateTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.region.factory_class", "jcache"); + properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); + } + + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + doInJPA(entityManager -> { + Post post = entityManager + .find(Post.class, 1L) + .addComment( + new PostComment() + .setId(1L) + .setReview("JDBC part review") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("Hibernate part review") + ); + }); + } + + @Test + public void testEntityLoad() { + + printCollectionCacheRegionStatistics(Post.class, "comments"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + }); + + printCollectionCacheRegionStatistics(Post.class, "comments"); + + doInJPA(entityManager -> { + LOGGER.info("Load from cache"); + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + }); + + printCollectionCacheRegionStatistics(Post.class, "comments"); + } + + @Entity(name = "Post") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class PostComment { + + @Id + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/EntityCacheEntryLoadedStateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/EntityCacheEntryLoadedStateTest.java new file mode 100644 index 000000000..b774c5506 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/EntityCacheEntryLoadedStateTest.java @@ -0,0 +1,223 @@ +package com.vladmihalcea.hpjp.hibernate.cache; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Date; +import java.util.Properties; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class EntityCacheEntryLoadedStateTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.region.factory_class", "jcache"); + properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + + + doInJPA(entityManager -> { + Post post = entityManager.getReference(Post.class,1L); + + entityManager.persist( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn(new Date()) + .setPost(post) + ); + + entityManager.persist( + new PostComment() + .setId(1L) + .setReview("Part one - JDBC and Database Essentials") + .setPost(post) + ); + + entityManager.persist( + new PostComment() + .setId(2L) + .setReview("Part one - JPA and Hibernate") + .setPost(post) + ); + }); + } + + @Test + public void testEntityLoad() { + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNotNull(post); + PostDetails details = entityManager.find(PostDetails.class, 1L); + assertNotNull(details); + PostComment comment = entityManager.find(PostComment.class, 1L); + assertNotNull(comment); + }); + + printCacheRegionStatistics(PostComment.class.getName()); + printCacheRegionStatistics(PostDetails.class.getName()); + printCacheRegionStatistics(Post.class.getName()); + + doInJPA(entityManager -> { + LOGGER.info("Load from cache"); + Post post = entityManager.find(Post.class, 1L); + assertNotNull(post); + PostDetails details = entityManager.find(PostDetails.class, 1L); + assertNotNull(details); + PostComment comment = entityManager.find(PostComment.class, 1L); + assertNotNull(comment); + }); + + printCacheRegionStatistics(Post.class.getName()); + } + + @Entity(name = "Post") + @Table(name = "post") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostDetails") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/EntityCacheEntryReferenceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/EntityCacheEntryReferenceTest.java new file mode 100644 index 000000000..b10037af1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/EntityCacheEntryReferenceTest.java @@ -0,0 +1,84 @@ +package com.vladmihalcea.hpjp.hibernate.cache; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Immutable; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.Properties; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class EntityCacheEntryReferenceTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.region.factory_class", "jcache"); + properties.put("hibernate.cache.use_reference_entries", Boolean.TRUE.toString()); + properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + } + + @Test + public void testEntityLoad() { + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNotNull(post); + }); + + printCacheRegionStatistics(Post.class.getName()); + } + + @Entity(name = "Post") + @Immutable + @Cache(usage = CacheConcurrencyStrategy.READ_ONLY) + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/EntityNullResultCacheTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/EntityNullResultCacheTest.java new file mode 100644 index 000000000..fa60b02b4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/EntityNullResultCacheTest.java @@ -0,0 +1,202 @@ +package com.vladmihalcea.hpjp.hibernate.cache; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.CacheLayout; +import org.hibernate.annotations.QueryHints; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import jakarta.persistence.*; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class EntityNullResultCacheTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Book.class + }; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.cache.region.factory_class", "jcache"); + properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); + properties.put("hibernate.cache.use_query_cache", Boolean.TRUE.toString()); + properties.put(AvailableSettings.QUERY_CACHE_LAYOUT, CacheLayout.SHALLOW); + return properties; + } + + @Test + public void testFindExistingEntity() { + + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn( "978-9730228236" ); + book.setTitle( "High-Performance Java Persistence" ); + book.setAuthor( "Vlad Mihalcea" ); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + entityManager.getEntityManagerFactory().getCache().evictAll(); + printCacheRegionStatistics(Book.class.getName()); + + Book book = entityManager.find(Book.class, "978-9730228236"); + assertEquals("Vlad Mihalcea", book.getAuthor()); + + printCacheRegionStatistics(Book.class.getName()); + + executeSync(() -> { + doInJPA(_entityManager -> { + Book _book = _entityManager.find(Book.class, "978-9730228236"); + + assertEquals("High-Performance Java Persistence", _book.getTitle()); + }); + }); + }); + } + + @Test + public void testFindNonExistingEntity() { + + doInJPA(entityManager -> { + printCacheRegionStatistics(Book.class.getName()); + + Book book = entityManager.find(Book.class, "978-9730456472"); + assertNull(book); + + printCacheRegionStatistics(Book.class.getName()); + + executeSync(() -> { + doInJPA(_entityManager -> { + Book _book = _entityManager.find(Book.class, "978-9730456472"); + + assertNull(_book); + }); + }); + }); + } + + @Test + public void testFindNonExistingEntityWithQuery() { + + doInJPA(entityManager -> { + printQueryCacheRegionStatistics(); + + try { + Book book = entityManager.createQuery( + "select b " + + "from Book b " + + "where b.isbn = :isbn", Book.class) + .setParameter("isbn", "978-9730456472") + .setHint(QueryHints.CACHEABLE, true) + .getSingleResult(); + } catch (NoResultException expected) { + } + + printQueryCacheRegionStatistics(); + + executeSync(() -> { + doInJPA(_entityManager -> { + try { + Book _book = _entityManager.createQuery( + "select b " + + "from Book b " + + "where b.isbn = :isbn", Book.class) + .setParameter("isbn", "978-9730456472") + .setHint(QueryHints.CACHEABLE, true) + .getSingleResult(); + } catch (NoResultException expected) { + } + }); + }); + }); + } + + @Test + public void testFindNonExistingEntityWithCriteria() { + + try { + Book book = getCacheableEntity(Book.class, "isbn", "978-9730456472"); + } catch (NoResultException expected) { + } + + printQueryCacheRegionStatistics(); + + executeSync(() -> { + try { + Book _book = getCacheableEntity(Book.class, "isbn", "978-9730456472"); + } catch (NoResultException expected) { + } + }); + } + + public T getCacheableEntity( + Class entityClass, + String identifierName, + Object identifierValue) { + return doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteria = builder.createQuery(entityClass); + Root fromClause = criteria.from(entityClass); + + criteria.where(builder.equal(fromClause.get(identifierName), identifierValue)); + + return entityManager + .createQuery(criteria) + .setHint(QueryHints.CACHEABLE, true) + .getSingleResult(); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Book { + + @Id + private String isbn; + + private String title; + + private String author; + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/InheritanceCacheTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/InheritanceCacheTest.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/InheritanceCacheTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/InheritanceCacheTest.java index 17b4a8ab4..893a3c92f 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/InheritanceCacheTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/InheritanceCacheTest.java @@ -1,12 +1,13 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache; +package com.vladmihalcea.hpjp.hibernate.cache; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.annotations.*; +import org.hibernate.cfg.AvailableSettings; import org.junit.Before; import org.junit.Test; -import javax.persistence.*; -import javax.persistence.Entity; +import jakarta.persistence.*; +import jakarta.persistence.Entity; import java.util.Date; import java.util.Properties; @@ -26,9 +27,10 @@ protected Class[] entities() { @Override protected Properties properties() { Properties properties = super.properties(); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); + properties.put("hibernate.cache.region.factory_class", "jcache"); properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); properties.put("hibernate.cache.use_query_cache", Boolean.TRUE.toString()); + properties.put(AvailableSettings.QUERY_CACHE_LAYOUT, CacheLayout.SHALLOW); return properties; } @@ -82,7 +84,7 @@ public static class Post { private String title; @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/JPACacheableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/JPACacheableTest.java new file mode 100644 index 000000000..87dc2bb46 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/JPACacheableTest.java @@ -0,0 +1,83 @@ +package com.vladmihalcea.hpjp.hibernate.cache; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.Cacheable; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.stat.Statistics; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class JPACacheableTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.cache.region.factory_class", "jcache"); + properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); + return properties; + } + + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + }); + } + + @Test + public void testEntityLoad() { + + Statistics statistics = sessionFactory().getStatistics(); + statistics.clear(); + assertEquals(0, statistics.getPrepareStatementCount()); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNotNull(post); + }); + assertEquals(0, statistics.getPrepareStatementCount()); + } + + @Entity(name = "Post") + @Cacheable + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/LoadedStateBenchmarkTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/LoadedStateBenchmarkTest.java new file mode 100644 index 000000000..3f32ec529 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/LoadedStateBenchmarkTest.java @@ -0,0 +1,197 @@ +package com.vladmihalcea.hpjp.hibernate.cache; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Immutable; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertNotNull; + + +/** + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public class LoadedStateBenchmarkTest extends AbstractTest { + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Timer timer = metricRegistry.timer(getClass().getSimpleName()); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .build(); + + private int insertCount; + + public LoadedStateBenchmarkTest(int insertCount) { + this.insertCount = insertCount; + } + + @Parameterized.Parameters + public static Collection dataProvider() { + List providers = new ArrayList<>(); + providers.add(new Object[]{100}); + providers.add(new Object[]{500}); + providers.add(new Object[]{1000}); + providers.add(new Object[]{5000}); + providers.add(new Object[]{10000}); + return providers; + } + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class + }; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + return properties; + } + + @Before + public void init() { + super.init(); + doInJPA(entityManager -> { + for (long i = 0; i < insertCount; i++) { + Post post = new Post(); + post.setId(i); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); +/* + PostDetails details = new PostDetails(); + details.setCreatedBy("Vlad Mihalcea"); + details.setCreatedOn(new Date()); + details.setPost(post); + entityManager.persist(details);*/ + } + }); + } + + @Test + @Ignore + public void testReadOnlyFetchPerformance() { + //warming-up + doInJPA(entityManager -> { + for (long i = 0; i < 10000; i++) { + Post post = entityManager.find(Post.class, i % insertCount); + //PostDetails details = entityManager.find(PostDetails.class, i); + assertNotNull(post); + } + }); + doInJPA(entityManager -> { + long startNanos = System.nanoTime(); + for (long i = 0; i < insertCount; i++) { + Post post = entityManager.find(Post.class, i); + //PostDetails details = entityManager.find(PostDetails.class, i); + } + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + }); + logReporter.report(); + } + + + @Entity(name = "Post") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @org.hibernate.annotations.Immutable + public static class Post implements Serializable { + + @Id + private Long id; + + private String title; + + @Version + private short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostDetails") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @Immutable + //This does not work since it features an association type + public static class PostDetails implements Serializable { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/LoadedStateReferenceEntitiesTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/LoadedStateReferenceEntitiesTest.java new file mode 100644 index 000000000..145d3e3f3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/LoadedStateReferenceEntitiesTest.java @@ -0,0 +1,23 @@ +package com.vladmihalcea.hpjp.hibernate.cache; + +import java.util.Properties; + + +/** + * + * @author Vlad Mihalcea + */ +public class LoadedStateReferenceEntitiesTest extends LoadedStateBenchmarkTest { + + public LoadedStateReferenceEntitiesTest(int insertCount) { + super(insertCount); + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.cache.use_reference_entries", Boolean.TRUE.toString()); + return properties; + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/QueryLoadedStateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/QueryLoadedStateTest.java new file mode 100644 index 000000000..302ff2596 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/QueryLoadedStateTest.java @@ -0,0 +1,131 @@ +package com.vladmihalcea.hpjp.hibernate.cache; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.CacheLayout; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class QueryLoadedStateTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + properties.put("hibernate.cache.use_query_cache", Boolean.TRUE.toString()); + properties.put(AvailableSettings.QUERY_CACHE_LAYOUT, CacheLayout.SHALLOW); + } + + public void afterInit() { + doInJPA(entityManager -> { + Post post1 = new Post(); + post1.setId(1L); + post1.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post1); + + Post post2 = new Post(); + post2.setId(2L); + post2.setTitle("High-Performance Hibernate"); + + entityManager.persist(post2); + }); + } + + @Test + public void test() { + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + where p.title like :titlePattern + """, Post.class) + .setParameter("titlePattern", "High-Performance%") + .setHint("org.hibernate.cacheable", true) + .getResultList(); + + assertEquals(2, posts.size()); + }); + + printQueryCacheRegionStatistics(); + + doInJPA(entityManager -> { + LOGGER.info("Load from cache"); + List posts = entityManager.createQuery(""" + select p + from Post p + where p.title like :titlePattern + """, Post.class) + .setParameter("titlePattern", "High-Performance%") + .setHint("org.hibernate.cacheable", true) + .getResultList(); + + assertEquals(2, posts.size()); + }); + + printQueryCacheRegionStatistics(); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + where p.title like :titlePattern + """) + .setParameter("titlePattern", "High-Performance%") + .unwrap(org.hibernate.query.Query.class) + .setCacheable(true) + .getResultList(); + + assertEquals(2, posts.size()); + }); + + printQueryCacheRegionStatistics(); + } + + @Entity(name = "Post") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/nonstrictreadwrite/NonStrictReadWriteCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/nonstrictreadwrite/NonStrictReadWriteCacheConcurrencyStrategyTest.java new file mode 100644 index 000000000..70ef1da23 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/nonstrictreadwrite/NonStrictReadWriteCacheConcurrencyStrategyTest.java @@ -0,0 +1,254 @@ +package com.vladmihalcea.hpjp.hibernate.cache.nonstrictreadwrite; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class NonStrictReadWriteCacheConcurrencyStrategyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("JDBC part review") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("Hibernate part review") + ) + ); + }); + printEntityCacheRegionStatistics(Post.class); + printEntityCacheRegionStatistics(PostComment.class); + printCollectionCacheRegionStatistics(Post.class, "comments"); + + LOGGER.info("Post entity inserted"); + } + + @Test + public void testPostEntityLoad() { + + LOGGER.info("Load Post entity and comments collection"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + printEntityCacheRegionStatistics(Post.class); + assertEquals(2, post.getComments().size()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + }); + } + + @Test + public void testPostEntityEvictModifyLoad() { + + LOGGER.info("Evict, modify, load"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.detach(post); + + post.setTitle("High-Performance Hibernate"); + entityManager.merge(post); + entityManager.flush(); + + entityManager.detach(post); + post = entityManager.find(Post.class, 1L); + printEntityCacheRegionStatistics(Post.class); + }); + } + + @Test + public void testEntityUpdate() { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Hibernate"); + }); + + printCacheRegionStatistics(Post.class.getName()); + } + + @Test + public void testCollectionUpdate() { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + PostComment comment = post.getComments().remove(0); + comment.setPost(null); + }); + + printCollectionCacheRegionStatistics(Post.class, "comments"); + printCacheRegionStatistics(PostComment.class.getName()); + } + + @Test + public void testNonVersionedEntityUpdate() { + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + }); + printCacheRegionStatistics(PostComment.class.getName()); + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + comment.setReview("JDBC and Database part review"); + }); + printCacheRegionStatistics(PostComment.class.getName()); + } + + @Test + public void testEntityDelete() { + LOGGER.info("Cache entries can be deleted"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + }); + + printCacheRegionStatistics(Post.class.getName()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.remove(post); + }); + + printCacheRegionStatistics(Post.class.getName()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNull(post); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/nonstrictreadwrite/NonStrictReadWriteCacheConcurrencyStrategyWithConcurrentUpdateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/nonstrictreadwrite/NonStrictReadWriteCacheConcurrencyStrategyWithConcurrentUpdateTest.java similarity index 94% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/nonstrictreadwrite/NonStrictReadWriteCacheConcurrencyStrategyWithConcurrentUpdateTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/nonstrictreadwrite/NonStrictReadWriteCacheConcurrencyStrategyWithConcurrentUpdateTest.java index 35eefb2d7..63eadc0e7 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/nonstrictreadwrite/NonStrictReadWriteCacheConcurrencyStrategyWithConcurrentUpdateTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/nonstrictreadwrite/NonStrictReadWriteCacheConcurrencyStrategyWithConcurrentUpdateTest.java @@ -1,12 +1,12 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.nonstrictreadwrite; +package com.vladmihalcea.hpjp.hibernate.cache.nonstrictreadwrite; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.hibernate.*; import org.hibernate.annotations.CacheConcurrencyStrategy; import org.junit.Before; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Properties; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; @@ -64,7 +64,7 @@ protected Interceptor interceptor() { protected Properties properties() { Properties properties = super.properties(); properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); + properties.put("hibernate.cache.region.factory_class", "jcache"); return properties; } @@ -128,8 +128,8 @@ public static class Repository { private String name; - @javax.persistence.Version - private int version; + @jakarta.persistence.Version + private short version; public Repository() { } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/PostCommentSummary.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/PostCommentSummary.java new file mode 100644 index 000000000..78123a49b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/PostCommentSummary.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hpjp.hibernate.cache.query; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentSummary { + + private Long commentId; + + private String title; + + private String review; + + public PostCommentSummary(Long commentId, String title, String review) { + this.commentId = commentId; + this.title = title; + this.review = review; + } + + public Long getCommentId() { + return commentId; + } + + public String getTitle() { + return title; + } + + public String getReview() { + return review; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/PostSummary.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/PostSummary.java new file mode 100644 index 000000000..207e65e42 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/PostSummary.java @@ -0,0 +1,50 @@ +package com.vladmihalcea.hpjp.hibernate.cache.query; + +import java.util.Date; + +/** + * @author Vlad Mihalcea + */ +public class PostSummary { + + private Long id; + + private String title; + + private Date createdOn; + + private int commentCount; + + public PostSummary(Long id, String title, Date createdOn, Number commentCount) { + this.id = id; + this.title = title; + this.createdOn = createdOn; + this.commentCount = commentCount.intValue(); + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public Date getCreatedOn() { + return createdOn; + } + + public int getCommentCount() { + return commentCount; + } + + @Override + public String toString() { + return "PostSummary{" + + "id=" + id + + ", title='" + title + '\'' + + ", createdOn=" + createdOn + + ", commentCount=" + commentCount + + '}'; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/QueryCacheDTOTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/QueryCacheDTOTest.java new file mode 100644 index 000000000..9016b4ac0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/QueryCacheDTOTest.java @@ -0,0 +1,248 @@ +package com.vladmihalcea.hpjp.hibernate.cache.query; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import org.hibernate.annotations.CacheLayout; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.jpa.AvailableHints; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.junit.After; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class QueryCacheDTOTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + properties.put("hibernate.cache.use_query_cache", Boolean.TRUE.toString()); + properties.put(AvailableSettings.QUERY_CACHE_LAYOUT, CacheLayout.SHALLOW); + properties.put( + "hibernate.integrator_provider", + ClassImportIntegratorIntegratorProvider.class + ); + } + + public void afterInit() { + doInJPA(entityManager -> { + for (int i = 0; i < 10; i++) { + Post post = new Post(); + post.setTitle( + String.format("High-Performance Java Persistence, Chapter %d", i + 1) + ); + + int commentCount = (int) (Math.random() * 10); + + for (int j = 0; j < commentCount; j++) { + PostComment comment = new PostComment(); + comment.setReview( + String.format("Comment %d", j + 1) + ); + + post.addComment(comment); + } + entityManager.persist(post); + } + }); + } + + @After + public void destroy() { + entityManagerFactory().getCache().evictAll(); + super.destroy(); + } + + @Test + public void test2ndLevelDtoProjection() { + doInJPA(entityManager -> { + List latestPosts = getLatestPostSummaries( + entityManager, + 5, + false + ); + + assertEquals(5, latestPosts.size()); + }); + + doInJPA(entityManager -> { + List latestPosts = getLatestPostSummaries( + entityManager, + 5, + false + ); + + assertEquals(5, latestPosts.size()); + }); + + doInJPA(entityManager -> { + List latestPosts = getLatestPostSummaries( + entityManager, + 5, + true + ); + + printQueryCacheRegionStatistics(); + assertEquals(5, latestPosts.size()); + }); + + doInJPA(entityManager -> { + List latestPosts = getLatestPostSummaries( + entityManager, + 5, + true + ); + + printQueryCacheRegionStatistics(); + assertEquals(5, latestPosts.size()); + }); + } + + List getLatestPostSummaries( + EntityManager entityManager, + int maxResults, + boolean cacheable) { + List latestPosts = entityManager.createQuery(""" + select new PostSummary(p.id, p.title, p.createdOn, count(pc.id)) + from PostComment pc + left join pc.post p + group by p.id, p.title, p.createdOn + order by p.createdOn desc + """, PostSummary.class) + .setMaxResults(maxResults) + .setHint(AvailableHints.HINT_CACHEABLE, cacheable) + .getResultList(); + + LOGGER.debug("Latest posts: {}", latestPosts); + + return latestPosts; + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn = new Date(); + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } + + public static class ClassImportIntegratorIntegratorProvider implements IntegratorProvider { + + @Override + public List getIntegrators() { + return List.of( + new ClassImportIntegrator( + List.of( + PostSummary.class + ) + ) + ); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/QueryCacheNPlus1Test.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/QueryCacheNPlus1Test.java new file mode 100644 index 000000000..ac3ba58bb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/QueryCacheNPlus1Test.java @@ -0,0 +1,173 @@ +package com.vladmihalcea.hpjp.hibernate.cache.query; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.CacheLayout; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.jpa.AvailableHints; +import org.junit.After; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class QueryCacheNPlus1Test extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + properties.put("hibernate.cache.use_query_cache", Boolean.TRUE.toString()); + properties.put(AvailableSettings.QUERY_CACHE_LAYOUT, CacheLayout.SHALLOW); + } + + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + PostComment part1 = new PostComment(); + part1.setReview("Part one - JDBC"); + part1.setPost(post); + entityManager.persist(part1); + + PostComment part2 = new PostComment(); + part2.setReview("Part two - Hibernate"); + part2.setPost(post); + entityManager.persist(part2); + + PostComment part3 = new PostComment(); + part3.setReview("Part two - jOOQ"); + part3.setPost(post); + entityManager.persist(part3); + }); + } + + @After + public void destroy() { + entityManagerFactory().getCache().evictAll(); + super.destroy(); + } + + public List getLatestPostComments( + EntityManager entityManager) { + return entityManager.createQuery(""" + select pc + from PostComment pc + order by pc.post.id desc + """, PostComment.class) + .setMaxResults(10) + .setHint(AvailableHints.HINT_CACHEABLE, true) + .getResultList(); + } + + @Test + public void test2ndLevelCacheWithQuery() { + doInJPA(entityManager -> { + printQueryCacheRegionStatistics(); + assertEquals(3, getLatestPostComments(entityManager).size()); + + printQueryCacheRegionStatistics(); + assertEquals(3, getLatestPostComments(entityManager).size()); + }); + } + + @Test + public void test2ndLevelCacheWithQueryNPlus1() { + doInJPA(entityManager -> { + printQueryCacheRegionStatistics(); + assertEquals(3, getLatestPostComments(entityManager).size()); + printQueryCacheRegionStatistics(); + }); + + doInJPA(entityManager -> { + entityManager.getEntityManagerFactory().getCache().evict(PostComment.class); + }); + + doInJPA(entityManager -> { + assertEquals(3, getLatestPostComments(entityManager).size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/QueryCacheTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/QueryCacheTest.java new file mode 100644 index 000000000..25214954a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/query/QueryCacheTest.java @@ -0,0 +1,366 @@ +package com.vladmihalcea.hpjp.hibernate.cache.query; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.CacheLayout; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.jpa.AvailableHints; +import org.hibernate.query.NativeQuery; +import org.junit.After; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class QueryCacheTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + properties.put("hibernate.cache.use_query_cache", Boolean.TRUE.toString()); + properties.put(AvailableSettings.QUERY_CACHE_LAYOUT, CacheLayout.SHALLOW); + } + + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("JDBC part review") + ) + ); + }); + } + + @After + public void destroy() { + entityManagerFactory().getCache().evictAll(); + super.destroy(); + } + + public List getLatestPostComments(EntityManager entityManager) { + return entityManager.createQuery(""" + select pc + from PostComment pc + order by pc.post.id desc + """, PostComment.class) + .setMaxResults(10) + .setHint(AvailableHints.HINT_CACHEABLE, true) + .getResultList(); + } + + private List getLatestPostCommentsByPostId(EntityManager entityManager) { + return entityManager.createQuery(""" + select pc + from PostComment pc + where pc.post.id = :postId + """, PostComment.class) + .setParameter("postId", 1L) + .setMaxResults(10) + .setHint(AvailableHints.HINT_CACHEABLE, true) + .getResultList(); + } + + private List getLatestPostCommentsByPost(EntityManager entityManager) { + Post post = entityManager.find(Post.class, 1L); + return entityManager.createQuery(""" + select pc + from PostComment pc + where pc.post = :post + """, PostComment.class) + .setParameter("post", post) + .setMaxResults(10) + .setHint(AvailableHints.HINT_CACHEABLE, true) + .getResultList(); + } + + private List getPostCommentSummaryByPost(EntityManager entityManager, Long postId) { + return entityManager.createQuery(""" + select new + com.vladmihalcea.hpjp.hibernate.cache.query.PostCommentSummary( + pc.id, + p.title, + pc.review + ) + from PostComment pc + left join pc.post p + where p.id = :postId + """, PostCommentSummary.class) + .setParameter("postId", postId) + .setMaxResults(10) + .setHint(AvailableHints.HINT_CACHEABLE, true) + .getResultList(); + } + + @Test + public void test2ndLevelCacheWithoutResults() { + doInJPA(entityManager -> { + entityManager.createQuery("delete from PostComment").executeUpdate(); + }); + doInJPA(entityManager -> { + LOGGER.info("Query cache with basic type parameter"); + List comments = getLatestPostCommentsByPostId(entityManager); + assertTrue(comments.isEmpty()); + }); + doInJPA(entityManager -> { + LOGGER.info("Query cache with entity type parameter"); + List comments = getLatestPostCommentsByPostId(entityManager); + assertTrue(comments.isEmpty()); + }); + } + + @Test + public void test2ndLevelCacheWithQuery() { + doInJPA(entityManager -> { + printQueryCacheRegionStatistics(); + assertEquals(1, getLatestPostComments(entityManager).size()); + printQueryCacheRegionStatistics(); + assertEquals(1, getLatestPostComments(entityManager).size()); + }); + } + + @Test + public void test2ndLevelCacheWithQueryEntityLoad() { + doInJPA(entityManager -> { + printCacheRegionStatistics(PostComment.class.getName()); + printQueryCacheRegionStatistics(); + + assertEquals(1, getLatestPostComments(entityManager).size()); + + printCacheRegionStatistics(PostComment.class.getName()); + printQueryCacheRegionStatistics(); + + executeSync(() -> { + doInJPA(_entityManager -> { + List _comments = getLatestPostComments(_entityManager); + assertEquals(1, _comments.size()); + + _comments.get(0).setReview("Revision 2"); + }); + }); + + printCacheRegionStatistics(PostComment.class.getName()); + printQueryCacheRegionStatistics(); + List comments = getLatestPostComments(entityManager); + }); + } + + @Test + public void test2ndLevelCacheWithParameters() { + doInJPA(entityManager -> { + LOGGER.info("Query cache with basic type parameter"); + List comments = getLatestPostCommentsByPostId(entityManager); + assertEquals(1, comments.size()); + }); + doInJPA(entityManager -> { + LOGGER.info("Query cache with entity type parameter"); + List comments = getLatestPostCommentsByPost(entityManager); + assertEquals(1, comments.size()); + }); + } + + @Test + public void test2ndLevelCacheWithProjection() { + Long postId = 1L; + + doInJPA(entityManager -> { + LOGGER.info("Query cache with projection"); + List comments = getPostCommentSummaryByPost(entityManager, postId); + printQueryCacheRegionStatistics(); + assertEquals(1, comments.size()); + }); + doInJPA(entityManager -> { + LOGGER.info("Query cache with projection"); + List comments = getPostCommentSummaryByPost(entityManager, postId); + assertEquals(1, comments.size()); + printQueryCacheRegionStatistics(); + }); + } + + @Test + public void test2ndLevelCacheWithQueryInvalidation() { + doInJPA(entityManager -> { + + assertEquals(1, getLatestPostComments(entityManager).size()); + printQueryCacheRegionStatistics(); + + LOGGER.info("Insert a new PostComment"); + Post post = entityManager.find(Post.class, 1L); + post.addComment( + new PostComment() + .setId(2L) + .setReview("JDBC part review") + ); + entityManager.flush(); + + assertEquals(2, getLatestPostComments(entityManager).size()); + printQueryCacheRegionStatistics(); + }); + + LOGGER.info("After transaction commit"); + printQueryCacheRegionStatistics(); + + doInJPA(entityManager -> { + LOGGER.info("Check query cache"); + assertEquals(2, getLatestPostComments(entityManager).size()); + }); + printQueryCacheRegionStatistics(); + } + + @Test + public void test2ndLevelCacheWithNativeQueryInvalidation() { + doInJPA(entityManager -> { + assertEquals(1, getLatestPostComments(entityManager).size()); + printQueryCacheRegionStatistics(); + + int postCount = ((Number) entityManager.createNativeQuery( + "SELECT count(*) FROM post") + .getSingleResult()).intValue(); + + assertEquals(postCount, getLatestPostComments(entityManager).size()); + printQueryCacheRegionStatistics(); + }); + } + + @Test + public void test2ndLevelCacheWithNativeUpdateStatementInvalidation() { + doInJPA(entityManager -> { + assertEquals(1, getLatestPostComments(entityManager).size()); + printQueryCacheRegionStatistics(); + + entityManager.createNativeQuery(""" + UPDATE post + SET title = '\"'||title||'\"' + """) + .executeUpdate(); + + assertEquals(1, getLatestPostComments(entityManager).size()); + printQueryCacheRegionStatistics(); + }); + } + + @Test + public void test2ndLevelCacheWithNativeUpdateStatementSynchronization() { + doInJPA(entityManager -> { + assertEquals(1, getLatestPostComments(entityManager).size()); + printQueryCacheRegionStatistics(); + + LOGGER.info("Execute native query with synchronization"); + entityManager.createNativeQuery(""" + UPDATE post + SET title = '\"'||title||'\"' + """) + .unwrap(NativeQuery.class) + .addSynchronizedEntityClass(Post.class) + .executeUpdate(); + + assertEquals(1, getLatestPostComments(entityManager).size()); + printQueryCacheRegionStatistics(); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readonly/IdentityReadOnlyCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readonly/IdentityReadOnlyCacheConcurrencyStrategyTest.java new file mode 100644 index 000000000..5c29a4a01 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readonly/IdentityReadOnlyCacheConcurrencyStrategyTest.java @@ -0,0 +1,80 @@ +package com.vladmihalcea.hpjp.hibernate.cache.readonly; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Properties; + + +/** + * @author Vlad Mihalcea + */ +public class IdentityReadOnlyCacheConcurrencyStrategyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + } + + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + }); + printCacheRegionStatistics(Post.class.getName()); + LOGGER.info("Post entity inserted"); + } + + @Test + public void testPostEntityLoad() { + + LOGGER.info("Entities are not loaded from cache for identity as it's read-through"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + printEntityCacheRegionStatistics(Post.class); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + @Version + private short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readonly/ReadOnlyCacheConcurrencyStrategyImmutableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readonly/ReadOnlyCacheConcurrencyStrategyImmutableTest.java new file mode 100644 index 000000000..c81f98dce --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readonly/ReadOnlyCacheConcurrencyStrategyImmutableTest.java @@ -0,0 +1,171 @@ +package com.vladmihalcea.hpjp.hibernate.cache.readonly; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Immutable; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + + +/** + * @author Vlad Mihalcea + */ +public class ReadOnlyCacheConcurrencyStrategyImmutableTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + } + + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("JDBC part review") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("Hibernate part review") + ) + ); + }); + printEntityCacheRegionStatistics(Post.class); + printEntityCacheRegionStatistics(PostComment.class); + printCollectionCacheRegionStatistics(Post.class, "comments"); + + LOGGER.info("Post entity inserted"); + } + + @Test + public void testReadOnlyEntityUpdate() { + LOGGER.info("Read-only cache entries cannot be updated"); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Hibernate"); + }); + } + + @Test + public void testCollectionCacheUpdate() { + + LOGGER.info("Read-only collection cache entries cannot be updated"); + + try { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + PostComment comment = post.getComments().remove(0); + comment.setPost(null); + }); + } catch (Exception e) { + LOGGER.error("Expected", e); + } + } + + @Entity(name = "Post") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) + @Immutable + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private short version; + + @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "post") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) + @Immutable + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) + @Immutable + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readonly/ReadOnlyCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readonly/ReadOnlyCacheConcurrencyStrategyTest.java new file mode 100644 index 000000000..9762ab38a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readonly/ReadOnlyCacheConcurrencyStrategyTest.java @@ -0,0 +1,241 @@ +package com.vladmihalcea.hpjp.hibernate.cache.readonly; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class ReadOnlyCacheConcurrencyStrategyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + } + + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("JDBC part review") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("Hibernate part review") + ) + ); + }); + printEntityCacheRegionStatistics(Post.class); + printEntityCacheRegionStatistics(PostComment.class); + printCollectionCacheRegionStatistics(Post.class, "comments"); + + LOGGER.info("Post entity inserted"); + } + + @Test + public void testPostEntityLoad() { + + LOGGER.info("Entities are loaded from cache"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + printEntityCacheRegionStatistics(Post.class); + }); + } + + @Test + public void testCollectionCacheLoad() { + LOGGER.info("Collections require separate caching"); + + printCollectionCacheRegionStatistics(Post.class, "comments"); + + doInJPA(entityManager -> { + LOGGER.info("Load PostComment from database"); + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + }); + + printCacheRegionStatistics(Post.class.getName()); + printCacheRegionStatistics(PostComment.class.getName()); + + doInJPA(entityManager -> { + LOGGER.info("Load PostComment from cache"); + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + }); + } + + @Test + public void testCollectionCacheUpdate() { + LOGGER.info("Collection cache entries cannot be updated"); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + PostComment comment = post.getComments().remove(0); + comment.setPost(null); + }); + + printCollectionCacheRegionStatistics(Post.class, "comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + try { + doInJPA(entityManager -> { + LOGGER.info("Load PostComment from cache"); + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, post.getComments().size()); + }); + } catch (Exception e) { + LOGGER.error("Expected", e); + } + } + + @Test + public void testEntityUpdate() { + try { + LOGGER.info("Cache entries cannot be updated"); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Hibernate"); + }); + } catch (Exception e) { + LOGGER.error("Expected", e); + } + } + + @Test + public void testEntityDelete() { + LOGGER.info("Cache entries can be deleted"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + }); + + printCacheRegionStatistics(Post.class.getName()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.remove(post); + }); + + printCacheRegionStatistics(Post.class.getName()); + printCacheRegionStatistics(PostComment.class.getName()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNull(post); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", + cascade = CascadeType.ALL, orphanRemoval = true) + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_ONLY) + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readonly/SequenceReadOnlyCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readonly/SequenceReadOnlyCacheConcurrencyStrategyTest.java similarity index 87% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readonly/SequenceReadOnlyCacheConcurrencyStrategyTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readonly/SequenceReadOnlyCacheConcurrencyStrategyTest.java index f1a2b536c..0555c24f8 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readonly/SequenceReadOnlyCacheConcurrencyStrategyTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readonly/SequenceReadOnlyCacheConcurrencyStrategyTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.readonly; +package com.vladmihalcea.hpjp.hibernate.cache.readonly; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.annotations.CacheConcurrencyStrategy; import org.junit.Before; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Properties; @@ -27,7 +27,7 @@ protected Class[] entities() { protected Properties properties() { Properties properties = super.properties(); properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); + properties.put("hibernate.cache.region.factory_class", "jcache"); return properties; } @@ -50,7 +50,7 @@ public void testPostEntityLoad() { doInJPA(entityManager -> { Post post = entityManager.find(Post.class, 1L); - printCacheRegionStatistics(post.getClass().getName()); + printEntityCacheRegionStatistics(Post.class); }); } @@ -66,7 +66,7 @@ public static class Post { private String title; @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/IdentityReadWriteCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/IdentityReadWriteCacheConcurrencyStrategyTest.java new file mode 100644 index 000000000..4a3fb5ce1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/IdentityReadWriteCacheConcurrencyStrategyTest.java @@ -0,0 +1,155 @@ +package com.vladmihalcea.hpjp.hibernate.cache.readwrite; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + + +/** + * @author Vlad Mihalcea + */ +public class IdentityReadWriteCacheConcurrencyStrategyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setReview("JDBC part review") + ) + .addComment( + new PostComment() + .setReview("Hibernate part review") + ) + ); + }); + printEntityCacheRegionStatistics(Post.class); + printEntityCacheRegionStatistics(PostComment.class); + printCollectionCacheRegionStatistics(Post.class, "comments"); + + LOGGER.info("Post entity inserted"); + } + + @Test + public void testPostEntityLoad() { + + LOGGER.info("Load Post entity and comments collection"); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + printEntityCacheRegionStatistics(Post.class); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class PostComment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/ReadWriteCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/ReadWriteCacheConcurrencyStrategyTest.java new file mode 100644 index 000000000..2481c8b20 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/ReadWriteCacheConcurrencyStrategyTest.java @@ -0,0 +1,272 @@ +package com.vladmihalcea.hpjp.hibernate.cache.readwrite; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.cache.internal.EnabledCaching; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class ReadWriteCacheConcurrencyStrategyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("JDBC part review") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("Hibernate part review") + ) + ); + }); + printEntityCacheRegionStatistics(Post.class); + printEntityCacheRegionStatistics(PostComment.class); + printCollectionCacheRegionStatistics(Post.class, "comments"); + + LOGGER.info("Post entity inserted"); + } + + @Test + public void testPostEntityLoad() { + + LOGGER.info("Load Post entity and comments collection"); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + printEntityCacheRegionStatistics(Post.class); + assertEquals(2, post.getComments().size()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + }); + } + + @Test + public void testPostEntityEvictModifyLoad() { + + LOGGER.info("Evict, modify, load"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.detach(post); + + post.setTitle("High-Performance Hibernate"); + entityManager.merge(post); + entityManager.flush(); + + entityManager.detach(post); + post = entityManager.find(Post.class, 1L); + printEntityCacheRegionStatistics(Post.class); + }); + } + + @Test + public void testEntityUpdate() { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Hibernate"); + PostComment comment = post.getComments().remove(0); + comment.setPost(null); + + entityManager.flush(); + + printCacheRegionStatistics(Post.class.getName()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + LOGGER.debug("Commit after flush"); + }); + printCacheRegionStatistics(Post.class.getName()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + printCacheRegionStatistics(PostComment.class.getName()); + } + + @Test + public void testNonVersionedEntityUpdate() { + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + }); + printCacheRegionStatistics(PostComment.class.getName()); + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + comment.setReview("JDBC and Database part review"); + }); + printCacheRegionStatistics(PostComment.class.getName()); + } + + @Test + public void testEntityDelete() { + LOGGER.info("Cache entries can be deleted"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + }); + + printCacheRegionStatistics(Post.class.getName()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.remove(post); + }); + + printCacheRegionStatistics(Post.class.getName()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNull(post); + }); + } + + @Test + public void testNPlusOneCollectionInvalidation() { + //Test N+1 issue with cached collections + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + + LOGGER.info("Clear PostComment cache entries"); + Cache cache = entityManager.getEntityManagerFactory().getCache(); + cache.evict(PostComment.class); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + for(PostComment comment : post.getComments()) { + LOGGER.info("Comment {}", comment.getReview()); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/ReadWriteCacheConcurrencyStrategyWithLockTimeoutTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/ReadWriteCacheConcurrencyStrategyWithLockTimeoutTest.java similarity index 80% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/ReadWriteCacheConcurrencyStrategyWithLockTimeoutTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/ReadWriteCacheConcurrencyStrategyWithLockTimeoutTest.java index 2dfb9df20..f1039bf9a 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/ReadWriteCacheConcurrencyStrategyWithLockTimeoutTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/ReadWriteCacheConcurrencyStrategyWithLockTimeoutTest.java @@ -1,26 +1,25 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.readwrite; +package com.vladmihalcea.hpjp.hibernate.cache.readwrite; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.apache.commons.lang3.builder.ToStringBuilder; import org.hibernate.EmptyInterceptor; import org.hibernate.Interceptor; import org.hibernate.Transaction; import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.cache.internal.DefaultCacheKeysFactory; -import org.hibernate.cache.spi.EntityRegion; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.persister.entity.EntityPersister; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serializable; import java.lang.reflect.Field; import java.util.Properties; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; - /** * ReadWriteCacheConcurrencyStrategyWithTimeoutTest - Test to check CacheConcurrencyStrategy.READ_WRITE with lock timeout * @@ -53,7 +52,7 @@ public void beforeTransactionCompletion(Transaction tx) { protected Properties properties() { Properties properties = super.properties(); properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); + properties.put("hibernate.cache.region.factory_class", "jcache"); properties.put("net.sf.ehcache.hibernate.cache_lock_timeout", String.valueOf(250)); return properties; } @@ -68,10 +67,11 @@ public void init() { } @Test + @Ignore("Check the timeout property in latest EhCache version") public void testRepositoryEntityUpdate() { try { doInJPA(entityManager -> { - Repository repository = (Repository) entityManager.find(Repository.class, 1L); + Repository repository = entityManager.find(Repository.class, 1L); repository.setName("High-Performance Hibernate"); applyInterceptor.set(true); }); @@ -108,15 +108,21 @@ public void testRepositoryEntityUpdate() { @SuppressWarnings("unchecked") private T getCacheEntry(Class clazz, Long id) throws IllegalAccessException { - EntityPersister entityPersister = ((SessionFactoryImplementor) sessionFactory()).getEntityPersister(clazz.getName() ); - return (T) getCache(clazz).get(cacheKey(1L, entityPersister)); + //TODO: Find a way to inspect the cache + /*EntityPersister entityPersister = ((SessionFactoryImplementor) sessionFactory()).getEntityPersister(clazz.getName() ); + return (T) getCache(clazz).get(cacheKey(1L, entityPersister));*/ + return null; } - private net.sf.ehcache.Cache getCache(Class clazz) throws IllegalAccessException { - EntityPersister entityPersister = ((SessionFactoryImplementor) sessionFactory()).getEntityPersister(clazz.getName() ); - EntityRegion region = entityPersister.getCacheAccessStrategy().getRegion(); - Field cacheField = getField(region.getClass(), "cache"); - return (net.sf.ehcache.Cache) cacheField.get(region); + private Cache getCache(Class clazz) throws IllegalAccessException { + //TODO: Find a way to inspect the cache + /*EntityPersister entityPersister = ((SessionFactoryImplementor) sessionFactory()).getEntityPersister(clazz.getName() ); + DomainDataRegion region = entityPersister.getCacheAccessStrategy().getRegion(); + Field storageAccessField = getField(region.getClass(), "storageAccess"); + StorageAccess storageAccess = (StorageAccess) storageAccessField.get(region); + Field cacheField = getField(storageAccess.getClass(), "cache"); + return (net.sf.ehcache.Cache) cacheField.get(storageAccess);*/ + return null; } private Field getField(Class clazz, String fieldName) { @@ -154,7 +160,7 @@ public static class Repository { private String name; @Version - private int version; + private short version; public Repository() { } diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/RepositoryReadWriteCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/RepositoryReadWriteCacheConcurrencyStrategyTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/RepositoryReadWriteCacheConcurrencyStrategyTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/RepositoryReadWriteCacheConcurrencyStrategyTest.java index 82554344f..b52a3a58e 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/RepositoryReadWriteCacheConcurrencyStrategyTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/RepositoryReadWriteCacheConcurrencyStrategyTest.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.readwrite; +package com.vladmihalcea.hpjp.hibernate.cache.readwrite; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.Session; @@ -9,7 +9,7 @@ import org.junit.Before; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -36,7 +36,7 @@ protected Class[] entities() { protected Properties properties() { Properties properties = super.properties(); properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); + properties.put("hibernate.cache.region.factory_class", "jcache"); return properties; } @@ -123,7 +123,7 @@ public static class Repository { private String name; @Version - private int version; + private short version; @OneToMany(mappedBy = "repository", cascade = CascadeType.ALL) @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/SequenceReadWriteCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/SequenceReadWriteCacheConcurrencyStrategyTest.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/SequenceReadWriteCacheConcurrencyStrategyTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/SequenceReadWriteCacheConcurrencyStrategyTest.java index 0d4868e60..12954d42a 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/readwrite/SequenceReadWriteCacheConcurrencyStrategyTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/readwrite/SequenceReadWriteCacheConcurrencyStrategyTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.readwrite; +package com.vladmihalcea.hpjp.hibernate.cache.readwrite; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.annotations.CacheConcurrencyStrategy; import org.junit.Before; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -30,7 +30,7 @@ protected Class[] entities() { protected Properties properties() { Properties properties = super.properties(); properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); - properties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); + properties.put("hibernate.cache.region.factory_class", "jcache"); return properties; } @@ -52,7 +52,7 @@ public void init() { entityManager.persist(post); }); printCacheRegionStatistics(Post.class.getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); + printCollectionCacheRegionStatistics(Post.class, "comments"); LOGGER.info("Post entity inserted"); } @@ -63,8 +63,8 @@ public void testPostEntityLoad() { doInJPA(entityManager -> { Post post = entityManager.find(Post.class, 1L); assertEquals(2, post.getComments().size()); - printCacheRegionStatistics(post.getClass().getName()); - printCacheRegionStatistics(Post.class.getName() + ".comments"); + printEntityCacheRegionStatistics(Post.class); + printCollectionCacheRegionStatistics(Post.class, "comments"); }); } @@ -80,7 +80,7 @@ public static class Post { private String title; @Version - private int version; + private short version; @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/assigned/TransactionalCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/assigned/TransactionalCacheConcurrencyStrategyTest.java new file mode 100644 index 000000000..5cb1b3e33 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/assigned/TransactionalCacheConcurrencyStrategyTest.java @@ -0,0 +1,240 @@ +package com.vladmihalcea.hpjp.hibernate.cache.transactional.assigned; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.transaction.JPATransactionVoidFunction; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceContext; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import static com.vladmihalcea.hpjp.hibernate.cache.transactional.assigned.TransactionalEntities.Post; +import static com.vladmihalcea.hpjp.hibernate.cache.transactional.assigned.TransactionalEntities.PostComment; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TransactionalCacheConcurrencyStrategyTestConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class TransactionalCacheConcurrencyStrategyTest extends AbstractTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private TransactionTemplate transactionTemplate; + + @Override + protected void doInJPA(JPATransactionVoidFunction function) { + transactionTemplate.execute((TransactionCallback) status -> { + function.accept(entityManager); + return null; + }); + } + + @Override + protected Class[] entities() { + return new Class[0]; + } + + @Override + public EntityManagerFactory entityManagerFactory() { + return entityManager.getEntityManagerFactory(); + } + + public void init() { + doInJPA(entityManager -> { + entityManager.createQuery("delete from PostComment").executeUpdate(); + entityManager.createQuery("delete from Post").executeUpdate(); + entityManager.getEntityManagerFactory().getCache().evictAll(); + }); + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("JDBC part review") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("Hibernate part review") + ) + ); + }); + doInJPA(entityManager -> { + printEntityCacheRegionStatistics(Post.class); + printEntityCacheRegionStatistics(PostComment.class); + printCollectionCacheRegionStatistics(Post.class, "comments"); + + LOGGER.info("Post entity inserted"); + }); + } + + @Test + public void testPostEntityLoad() { + LOGGER.info("Load Post entity and comments collection"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + printEntityCacheRegionStatistics(Post.class); + assertEquals(2, post.getComments().size()); + printCacheRegionStatistics(Post.class.getName() + ".comments"); + }); + + LOGGER.info("Load Post entity for the 2nd time and comments collection"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + printEntityCacheRegionStatistics(Post.class); + assertEquals(2, post.getComments().size()); + printCacheRegionStatistics(Post.class.getName() + ".comments"); + }); + } + + @Test + public void testPostEntityEvictModifyLoad() { + + LOGGER.info("Evict, modify, load"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.detach(post); + + post.setTitle("High-Performance Hibernate"); + entityManager.merge(post); + entityManager.flush(); + + entityManager.detach(post); + post = entityManager.find(Post.class, 1L); + printEntityCacheRegionStatistics(Post.class); + }); + } + + @Test + public void testEntityUpdate() { + LOGGER.debug("testEntityUpdate"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + }); + + doInJPA(entityManager -> { + printCacheRegionStatistics(Post.class.getName()); + printCacheRegionStatistics(Post.class.getName() + ".comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Hibernate"); + PostComment comment = post.getComments().remove(0); + comment.setPost(null); + + entityManager.flush(); + + printCacheRegionStatistics(Post.class.getName()); + printCacheRegionStatistics(Post.class.getName() + ".comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + LOGGER.debug("Commit after flush"); + }); + printCacheRegionStatistics(Post.class.getName()); + printCacheRegionStatistics(Post.class.getName() + ".comments"); + printCacheRegionStatistics(PostComment.class.getName()); + } + + @Test + public void testEntityUpdateWithRollback() { + LOGGER.debug("testEntityUpdate"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + }); + + try { + doInJPA(entityManager -> { + printCacheRegionStatistics(Post.class.getName()); + printCacheRegionStatistics(Post.class.getName() + ".comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Hibernate"); + PostComment comment = post.getComments().remove(0); + comment.setPost(null); + + entityManager.flush(); + + printCacheRegionStatistics(Post.class.getName()); + printCacheRegionStatistics(Post.class.getName() + ".comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + if(comment.getId() != null) { + throw new IllegalStateException("Intentional roll back!"); + } + }); + } catch (Exception expected) { + LOGGER.info("Expected", expected); + } + + doInJPA(entityManager -> { + printCacheRegionStatistics(Post.class.getName()); + printCacheRegionStatistics(Post.class.getName() + ".comments"); + printCacheRegionStatistics(PostComment.class.getName()); + }); + } + + @Test + public void testNonVersionedEntityUpdate() { + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + }); + printCacheRegionStatistics(PostComment.class.getName()); + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + comment.setReview("JDBC and Database part review"); + }); + printCacheRegionStatistics(PostComment.class.getName()); + } + + @Test + public void testEntityDelete() { + LOGGER.info("Cache entries can be deleted"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + }); + + doInJPA(entityManager -> { + printCacheRegionStatistics(Post.class.getName()); + printCacheRegionStatistics(Post.class.getName() + ".comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + Post post = entityManager.find(Post.class, 1L); + entityManager.remove(post); + }); + + doInJPA(entityManager -> { + printCacheRegionStatistics(Post.class.getName()); + printCacheRegionStatistics(Post.class.getName() + ".comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + Post post = entityManager.find(Post.class, 1L); + assertNull(post); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/assigned/TransactionalCacheConcurrencyStrategyTestConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/assigned/TransactionalCacheConcurrencyStrategyTestConfiguration.java new file mode 100644 index 000000000..5405b7e27 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/assigned/TransactionalCacheConcurrencyStrategyTestConfiguration.java @@ -0,0 +1,44 @@ +package com.vladmihalcea.hpjp.hibernate.cache.transactional.assigned; + +import com.vladmihalcea.hpjp.spring.transaction.jta.narayana.config.NarayanaJTATransactionManagerConfiguration; +import org.ehcache.jsr107.EhcacheCachingProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; + +import javax.cache.CacheManager; +import java.net.URISyntaxException; +import java.util.Properties; + +@Configuration +public class TransactionalCacheConcurrencyStrategyTestConfiguration extends NarayanaJTATransactionManagerConfiguration { + + @Override + protected Properties additionalProperties() { + Properties properties = super.additionalProperties(); + properties.put("hibernate.cache.region.factory_class", "jcache"); + properties.put("hibernate.javax.cache.cache_manager", cacheManager()); + properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); + properties.put("hibernate.cache.use_structured_entries", Boolean.FALSE.toString()); + return properties; + } + + @Bean + @DependsOn("transactionManager") + public CacheManager cacheManager() { + try { + return new EhcacheCachingProvider().getCacheManager( + getClass().getResource("/ehcache.xml").toURI(), + getClass().getClassLoader() + ); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + protected String[] packagesToScan() { + return new String[]{ + TransactionalEntities.class.getPackage().getName() + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/TransactionalEntities.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/assigned/TransactionalEntities.java similarity index 76% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/TransactionalEntities.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/assigned/TransactionalEntities.java index b3f627a4e..5e05351c3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/TransactionalEntities.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/assigned/TransactionalEntities.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.transactional; +package com.vladmihalcea.hpjp.hibernate.cache.transactional.assigned; import org.hibernate.annotations.CacheConcurrencyStrategy; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; @@ -21,9 +21,6 @@ public static class Post { private String title; - @Version - private int version; - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) private List comments = new ArrayList<>(); @@ -32,25 +29,28 @@ public Long getId() { return id; } - public void setId(Long id) { + public Post setId(Long id) { this.id = id; + return this; } public String getTitle() { return title; } - public void setTitle(String title) { + public Post setTitle(String title) { this.title = title; + return this; } public List getComments() { return comments; } - public void addComment(PostComment comment) { + public Post addComment(PostComment comment) { comments.add(comment); comment.setPost(this); + return this; } } @@ -71,24 +71,27 @@ public Long getId() { return id; } - public void setId(Long id) { + public PostComment setId(Long id) { this.id = id; + return this; } public Post getPost() { return post; } - public void setPost(Post post) { + public PostComment setPost(Post post) { this.post = post; + return this; } public String getReview() { return review; } - public void setReview(String review) { + public PostComment setReview(String review) { this.review = review; + return this; } } } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalCacheConcurrencyStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalCacheConcurrencyStrategyTest.java new file mode 100644 index 000000000..96ef71de1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalCacheConcurrencyStrategyTest.java @@ -0,0 +1,184 @@ +package com.vladmihalcea.hpjp.hibernate.cache.transactional.identity; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vladmihalcea.hpjp.util.transaction.JPATransactionVoidFunction; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceContext; + +import static com.vladmihalcea.hpjp.hibernate.cache.transactional.identity.IdentityTransactionalEntities.Post; +import static com.vladmihalcea.hpjp.hibernate.cache.transactional.identity.IdentityTransactionalEntities.PostComment; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = IdentityTransactionalCacheConcurrencyStrategyTestConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public class IdentityTransactionalCacheConcurrencyStrategyTest extends AbstractTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private TransactionTemplate transactionTemplate; + + @Override + protected void doInJPA(JPATransactionVoidFunction function) { + transactionTemplate.execute((TransactionCallback) status -> { + function.accept(entityManager); + return null; + }); + } + + private Post post; + private PostComment comment1; + private PostComment comment2; + + @Before + public void init() { + doInJPA(entityManager -> { + entityManager.createQuery("delete from PostComment").executeUpdate(); + entityManager.createQuery("delete from Post").executeUpdate(); + entityManager.getEntityManagerFactory().getCache().evictAll(); + + post = new Post(); + post.setTitle("High-Performance Java Persistence"); + + comment1 = new PostComment(); + comment1.setReview("JDBC part review"); + post.addComment(comment1); + + comment2 = new PostComment(); + comment2.setReview("Hibernate part review"); + post.addComment(comment2); + + entityManager.persist(post); + }); + printCacheRegionStatistics(Post.class.getName()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + LOGGER.info("Post entity inserted"); + } + + @Override + public void destroy() { + + } + + @Override + protected Class[] entities() { + return new Class[0]; + } + + @Override + public EntityManagerFactory entityManagerFactory() { + return entityManager.getEntityManagerFactory(); + } + + @Test + public void testPostEntityLoad() { + + LOGGER.info("Load Post entity and comments collection"); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, this.post.getId()); + assertEquals(2, post.getComments().size()); + printEntityCacheRegionStatistics(Post.class); + printCollectionCacheRegionStatistics(Post.class, "comments"); + }); + } + + @Test + public void testPostEntityEvictModifyLoad() { + + LOGGER.info("Evict, modify, load"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, this.post.getId()); + entityManager.detach(post); + + post.setTitle("High-Performance Hibernate"); + entityManager.merge(post); + entityManager.flush(); + + entityManager.detach(post); + post = entityManager.find(Post.class, this.post.getId()); + printEntityCacheRegionStatistics(Post.class); + }); + } + + @Test + public void testEntityUpdate() { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, this.post.getId()); + post.setTitle("High-Performance Hibernate"); + PostComment comment = post.getComments().remove(0); + comment.setPost(null); + + entityManager.flush(); + + printCacheRegionStatistics(Post.class.getName()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + LOGGER.debug("Commit after flush"); + }); + printCacheRegionStatistics(Post.class.getName()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + printCacheRegionStatistics(PostComment.class.getName()); + } + + @Test + public void testNonVersionedEntityUpdate() { + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, this.comment1.getId()); + }); + printCacheRegionStatistics(PostComment.class.getName()); + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, this.comment1.getId()); + comment.setReview("JDBC and Database part review"); + }); + printCacheRegionStatistics(PostComment.class.getName()); + } + + @Test + public void testEntityDelete() { + LOGGER.info("Cache entries can be deleted"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, this.post.getId()); + assertEquals(2, post.getComments().size()); + }); + + printCacheRegionStatistics(Post.class.getName()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, this.post.getId()); + entityManager.remove(post); + }); + + printCacheRegionStatistics(Post.class.getName()); + printCollectionCacheRegionStatistics(Post.class, "comments"); + printCacheRegionStatistics(PostComment.class.getName()); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, this.post.getId()); + assertNull(post); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalCacheConcurrencyStrategyTestConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalCacheConcurrencyStrategyTestConfiguration.java new file mode 100644 index 000000000..342c9b74a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalCacheConcurrencyStrategyTestConfiguration.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.hibernate.cache.transactional.identity; + +import com.vladmihalcea.hpjp.util.spring.config.jta.HSQLDBJtaTransactionManagerConfiguration; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + +@Configuration +public class IdentityTransactionalCacheConcurrencyStrategyTestConfiguration extends + HSQLDBJtaTransactionManagerConfiguration { + + @Override + protected Properties additionalProperties() { + Properties properties = super.additionalProperties(); + properties.put("hibernate.cache.region.factory_class", "jcache"); + properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); + return properties; + } + + @Override + protected Class configurationClass() { + return IdentityTransactionalEntities.class; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalEntities.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalEntities.java similarity index 94% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalEntities.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalEntities.java index e4f1bf735..00a0095bc 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalEntities.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cache/transactional/identity/IdentityTransactionalEntities.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.cache.transactional.identity; +package com.vladmihalcea.hpjp.hibernate.cache.transactional.identity; import org.hibernate.annotations.CacheConcurrencyStrategy; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; @@ -23,7 +23,7 @@ public static class Post { private String title; @Version - private int version; + private short version; @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cascade/CascadeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cascade/CascadeTest.java new file mode 100644 index 000000000..6d1e29b66 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/cascade/CascadeTest.java @@ -0,0 +1,267 @@ +package com.vladmihalcea.hpjp.hibernate.cascade; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + + +/** + * @author Vlad Mihalcea + */ +public class CascadeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + private Long postId; + + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post() + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setReview("A must-read for every Java developer!") + ); + + + entityManager.persist(post); + + postId = post.getId(); + }); + } + + @Test + public void testMerge() { + Post post = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", postId) + .getSingleResult(); + }); + + post.setTitle(post.getTitle() + " - 2nd edition"); + + PostComment comment = post.getComments() + .stream() + .filter(c -> c.getReview().startsWith("Best book")) + .findAny() + .orElseGet(null); + comment.setReview(comment.getReview().replace("Best", "The best")); + + post.addComment( + new PostComment() + .setReview("A great reference book") + ); + + doInJPA(entityManager -> { + entityManager.merge(post); + }); + } + + @Test + public void testRemove() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", postId) + .getSingleResult(); + + entityManager.remove(post); + }); + } + + @Test + public void testDetach() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", postId) + .getSingleResult(); + + assertTrue(entityManager.contains(post)); + for (PostComment comment : post.getComments()) { + assertTrue(entityManager.contains(comment)); + } + + entityManager.detach(post); + + assertFalse(entityManager.contains(post)); + for (PostComment comment : post.getComments()) { + assertFalse(entityManager.contains(comment)); + } + }); + } + + @Test + public void testUpdate() { + Post post = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", postId) + .getSingleResult(); + }); + + post.setTitle(post.getTitle() + " - 2nd edition"); + + PostComment comment = post.getComments() + .stream() + .filter(c -> c.getReview().startsWith("Best book")) + .findAny() + .orElseGet(null); + comment.setReview(comment.getReview().replace("Best", "The best")); + + post.addComment( + new PostComment() + .setReview("A great reference book") + ); + + doInJPA(entityManager -> { + entityManager + .unwrap(Session.class) + .update(post); + }); + } + + @Test + public void testOrphanRemoval() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments c + where p.id = :id + order by p.id, c.id + """, Post.class) + .setParameter("id", postId) + .getSingleResult(); + + post.removeComment(post.getComments().get(0)); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/AbstractEntityOptimisticLockingCollectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/AbstractEntityOptimisticLockingCollectionTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/AbstractEntityOptimisticLockingCollectionTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/AbstractEntityOptimisticLockingCollectionTest.java index c535d0a51..e3fd58702 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/AbstractEntityOptimisticLockingCollectionTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/AbstractEntityOptimisticLockingCollectionTest.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import java.util.List; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/AbstractLockModeOptimisticTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/AbstractLockModeOptimisticTest.java similarity index 91% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/AbstractLockModeOptimisticTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/AbstractLockModeOptimisticTest.java index caaa0ae10..a65718d57 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/AbstractLockModeOptimisticTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/AbstractLockModeOptimisticTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Before; -import javax.persistence.*; +import jakarta.persistence.*; /** * AbstractLockModeOptimisticTest - Base Test to check LockMode.OPTIMISTIC @@ -44,7 +44,7 @@ public static class Post { private String body; @Version - private int version; + private short version; public Long getId() { return id; @@ -84,7 +84,7 @@ public static class PostComment { private String review; @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/AllPropertiesOptimisticLockingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/AllPropertiesOptimisticLockingTest.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/AllPropertiesOptimisticLockingTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/AllPropertiesOptimisticLockingTest.java index ecab68fd7..2321c0b4e 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/AllPropertiesOptimisticLockingTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/AllPropertiesOptimisticLockingTest.java @@ -1,15 +1,16 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.StaleStateException; import org.hibernate.annotations.DynamicUpdate; import org.hibernate.annotations.OptimisticLockType; import org.hibernate.annotations.OptimisticLocking; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** * @author Vlad Mihalcea @@ -63,7 +64,7 @@ public void testStaleStateException() { } catch (Exception expected) { LOGGER.error("Throws", expected); assertEquals(OptimisticLockException.class, expected.getCause().getClass()); - assertEquals(StaleStateException.class, expected.getCause().getCause().getClass()); + assertTrue(StaleStateException.class.isAssignableFrom(expected.getCause().getCause().getClass())); } } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/BulkUpdateOptimisticLockingIntegerVersionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/BulkUpdateOptimisticLockingIntegerVersionTest.java new file mode 100644 index 000000000..c4ceefdb3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/BulkUpdateOptimisticLockingIntegerVersionTest.java @@ -0,0 +1,304 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import jakarta.persistence.criteria.*; +import org.hibernate.annotations.DynamicUpdate; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BulkUpdateOptimisticLockingIntegerVersionTest extends AbstractTest { + + private static final int SPAM_POST_COUNT = 10; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + public void afterInit() { + doInJPA(entityManager -> { + for (long i = 1; i <= SPAM_POST_COUNT; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle(String.format("Spam post %d", i)) + ); + } + }); + } + + @Test + public void testLostUpdate() { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(PostStatus.PENDING, post.getStatus()); + assertEquals(0, post.getVersion()); + + executeSync(() -> doInJPA( + _entityManager -> { + int updateCount = entityManager.createQuery(""" + update Post + set status = :newStatus + where + status = :oldStatus and + lower(title) like :pattern + """) + .setParameter("oldStatus", PostStatus.PENDING) + .setParameter("newStatus", PostStatus.SPAM) + .setParameter("pattern", "%spam%") + .executeUpdate(); + + assertEquals(SPAM_POST_COUNT, updateCount); + } + )); + + post.setStatus(PostStatus.APPROVED); + }); + } + + @Test + public void testJPQL() { + doInJPA(entityManager -> { + int zeroVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 0) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, zeroVersionCount); + + int updateCount = entityManager.createQuery(""" + update Post + set status = :newStatus + where + status = :oldStatus and + lower(title) like :pattern + """) + .setParameter("oldStatus", PostStatus.PENDING) + .setParameter("newStatus", PostStatus.SPAM) + .setParameter("pattern", "%spam%") + .executeUpdate(); + + assertEquals(SPAM_POST_COUNT, updateCount); + + int oneVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 1) + .getSingleResult() + .intValue(); + + assertEquals(0, oneVersionCount); + }); + } + + @Test + public void testJPQLWithVersion() { + doInJPA(entityManager -> { + int zeroVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 0) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, zeroVersionCount); + + int updateCount = entityManager.createQuery(""" + update Post + set + status = :newStatus, + version = version + 1 + where + status = :oldStatus and + lower(title) like :pattern + """) + .setParameter("oldStatus", PostStatus.PENDING) + .setParameter("newStatus", PostStatus.SPAM) + .setParameter("pattern", "%spam%") + .executeUpdate(); + + assertEquals(SPAM_POST_COUNT, updateCount); + + int oneVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 1) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, oneVersionCount); + }); + } + + @Test + public void testHQL() { + doInJPA(entityManager -> { + int zeroVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 0) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, zeroVersionCount); + + int updateCount = entityManager.createQuery(""" + update versioned Post + set status = :newStatus + where + status = :oldStatus and + lower(title) like :pattern + """) + .setParameter("oldStatus", PostStatus.PENDING) + .setParameter("newStatus", PostStatus.SPAM) + .setParameter("pattern", "%spam%") + .executeUpdate(); + + assertEquals(SPAM_POST_COUNT, updateCount); + + int oneVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 1) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, oneVersionCount); + }); + } + + @Test + public void testCriteriaAPI() { + doInJPA(entityManager -> { + int zeroVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 0) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, zeroVersionCount); + + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaUpdate update = builder.createCriteriaUpdate(Post.class); + + Root root = update.from(Post.class); + + Expression wherePredicate = builder.and( + builder.equal(root.get("status"), PostStatus.PENDING), + builder.like(builder.lower(root.get("title")), "%spam%") + ); + + Path versionPath = root.get("version"); + Expression incrementVersion = builder.sum((short) 1, versionPath); + + update + .set(root.get("status"), PostStatus.SPAM) + .set(versionPath, incrementVersion) + .where(wherePredicate); + + int updateCount = entityManager.createQuery(update).executeUpdate(); + + assertEquals(SPAM_POST_COUNT, updateCount); + + int oneVersionCount = entityManager.createQuery(""" + select count(*) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 1) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, oneVersionCount); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @DynamicUpdate + public static class Post { + + @Id + private Long id; + + private String title; + + @Enumerated(EnumType.ORDINAL) + @Column(columnDefinition = "smallint") + private PostStatus status = PostStatus.PENDING; + + @Version + private int version; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public void setStatus(PostStatus status) { + this.status = status; + } + + public int getVersion() { + return version; + } + + public Post setVersion(short version) { + this.version = version; + return this; + } + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/BulkUpdateOptimisticLockingShortVersionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/BulkUpdateOptimisticLockingShortVersionTest.java new file mode 100644 index 000000000..ea768805c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/BulkUpdateOptimisticLockingShortVersionTest.java @@ -0,0 +1,306 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.annotations.DynamicUpdate; +import org.junit.Test; + +import jakarta.persistence.*; +import jakarta.persistence.criteria.*; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BulkUpdateOptimisticLockingShortVersionTest extends AbstractTest { + + private static final int SPAM_POST_COUNT = 10; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + public void afterInit() { + doInJPA(entityManager -> { + for (long i = 1; i <= SPAM_POST_COUNT; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle(String.format("Spam post %d", i)) + ); + } + }); + } + + @Test + public void testLostUpdate() { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(PostStatus.PENDING, post.getStatus()); + assertEquals(0, post.getVersion()); + + executeSync(() -> doInJPA( + _entityManager -> { + int updateCount = entityManager.createQuery(""" + update Post + set status = :newStatus + where + status = :oldStatus and + lower(title) like :pattern + """) + .setParameter("oldStatus", PostStatus.PENDING) + .setParameter("newStatus", PostStatus.SPAM) + .setParameter("pattern", "%spam%") + .executeUpdate(); + + assertEquals(SPAM_POST_COUNT, updateCount); + } + )); + + post.setStatus(PostStatus.APPROVED); + }); + } + + @Test + public void testJPQL() { + doInJPA(entityManager -> { + int zeroVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 0) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, zeroVersionCount); + + int updateCount = entityManager.createQuery(""" + update Post + set status = :newStatus + where + status = :oldStatus and + lower(title) like :pattern + """) + .setParameter("oldStatus", PostStatus.PENDING) + .setParameter("newStatus", PostStatus.SPAM) + .setParameter("pattern", "%spam%") + .executeUpdate(); + + assertEquals(SPAM_POST_COUNT, updateCount); + + int oneVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 1) + .getSingleResult() + .intValue(); + + assertEquals(0, oneVersionCount); + }); + } + + @Test + public void testJPQLWithVersion() { + doInJPA(entityManager -> { + int zeroVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 0) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, zeroVersionCount); + + int updateCount = entityManager.createQuery(""" + update Post + set + status = :newStatus, + version = (version + :versionInc) + where + status = :oldStatus and + lower(title) like :pattern + """) + .setParameter("oldStatus", PostStatus.PENDING) + .setParameter("newStatus", PostStatus.SPAM) + .setParameter("versionInc", (short) 1) + .setParameter("pattern", "%spam%") + .executeUpdate(); + + assertEquals(SPAM_POST_COUNT, updateCount); + + int oneVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 1) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, oneVersionCount); + }); + } + + @Test + public void testHQL() { + doInJPA(entityManager -> { + int zeroVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 0) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, zeroVersionCount); + + int updateCount = entityManager.createQuery(""" + update versioned Post + set status = :newStatus + where + status = :oldStatus and + lower(title) like :pattern + """) + .setParameter("oldStatus", PostStatus.PENDING) + .setParameter("newStatus", PostStatus.SPAM) + .setParameter("pattern", "%spam%") + .executeUpdate(); + + assertEquals(SPAM_POST_COUNT, updateCount); + + int oneVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 1) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, oneVersionCount); + }); + } + + @Test + public void testCriteriaAPI() { + doInJPA(entityManager -> { + int zeroVersionCount = entityManager.createQuery(""" + select count(p) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 0) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, zeroVersionCount); + + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaUpdate update = builder.createCriteriaUpdate(Post.class); + + Root root = update.from(Post.class); + + Expression wherePredicate = builder.and( + builder.equal(root.get("status"), PostStatus.PENDING), + builder.like(builder.lower(root.get("title")), "%spam%") + ); + + Path versionPath = root.get("version"); + Expression incrementVersion = builder.sum((short) 1, versionPath); + + update + .set(root.get("status"), PostStatus.SPAM) + .set(versionPath, incrementVersion) + .where(wherePredicate); + + int updateCount = entityManager.createQuery(update).executeUpdate(); + + assertEquals(SPAM_POST_COUNT, updateCount); + + int oneVersionCount = entityManager.createQuery(""" + select count(*) + from Post p + where p.version = :version + """, Number.class) + .setParameter("version", (short) 1) + .getSingleResult() + .intValue(); + + assertEquals(SPAM_POST_COUNT, oneVersionCount); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @DynamicUpdate + public static class Post { + + @Id + private Long id; + + private String title; + + @Enumerated(EnumType.ORDINAL) + @Column(columnDefinition = "smallint") + private PostStatus status = PostStatus.PENDING; + + @Version + private short version; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public void setStatus(PostStatus status) { + this.status = status; + } + + public short getVersion() { + return version; + } + + public Post setVersion(short version) { + this.version = version; + return this; + } + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/CascadeLockElementCollectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/CascadeLockElementCollectionTest.java similarity index 95% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/CascadeLockElementCollectionTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/CascadeLockElementCollectionTest.java index 6dc8a2bb1..010365d19 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/CascadeLockElementCollectionTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/CascadeLockElementCollectionTest.java @@ -1,14 +1,13 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.Session; -import org.hibernate.jpa.AvailableSettings; -import org.junit.Before; +import org.hibernate.cfg.AvailableSettings; import org.junit.Test; -import javax.persistence.*; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -32,17 +31,15 @@ protected Class[] entities() { }; } - @Before - public void init() { - super.init(); + public void afterInit() { doInJPA(entityManager -> { Post post = new Post(); post.setTitle("Hibernate Master Class"); - entityManager.persist(post); - post.addDetails(new PostDetails()); post.addComment(new PostComment("Good post!")); post.addComment(new PostComment("Nice post!")); + + entityManager.persist(post); }); } @@ -65,7 +62,7 @@ public void testCascadeLockOnManagedEntityWithJPA() throws InterruptedException doInJPA(entityManager -> { Post post = entityManager.find(Post.class, 1L); entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, Collections.singletonMap( - AvailableSettings.LOCK_SCOPE, PessimisticLockScope.EXTENDED + AvailableSettings.JAKARTA_LOCK_SCOPE, PessimisticLockScope.EXTENDED )); }); } @@ -116,7 +113,7 @@ public void testCascadeLockOnManagedEntityWithAssociationsInitializedAndJpa() th .setParameter("id", 1L) .getSingleResult(); entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, Collections.singletonMap( - AvailableSettings.LOCK_SCOPE, PessimisticLockScope.EXTENDED + AvailableSettings.JAKARTA_LOCK_SCOPE, PessimisticLockScope.EXTENDED )); }); } @@ -127,7 +124,7 @@ public void testCascadeLockOnManagedEntityWithAssociationsUninitializedAndJpa() doInJPA(entityManager -> { Post post = entityManager.find(Post.class, 1L); entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, Collections.singletonMap( - AvailableSettings.LOCK_SCOPE, PessimisticLockScope.EXTENDED + AvailableSettings.JAKARTA_LOCK_SCOPE, PessimisticLockScope.EXTENDED )); }); } @@ -274,7 +271,7 @@ public static class Post { private String body; @Version - private int version; + private short version; public Post() {} @@ -346,14 +343,13 @@ public static class PostDetails { private String createdBy; @Version - private int version; + private short version; public PostDetails() { createdOn = new Date(); } @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") @MapsId private Post post; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/CascadeLockManyToOneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/CascadeLockManyToOneTest.java similarity index 94% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/CascadeLockManyToOneTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/CascadeLockManyToOneTest.java index 2baf4ae03..df636cc36 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/CascadeLockManyToOneTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/CascadeLockManyToOneTest.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.Session; @@ -9,7 +9,7 @@ import org.junit.Before; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; /** @@ -74,7 +74,7 @@ public static class Post { private String body; @Version - private int version; + private short version; public Post() {} @@ -116,7 +116,7 @@ public static class PostComment { private String review; @Version - private int version; + private short version; public PostComment() {} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/CascadeLockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/CascadeLockTest.java new file mode 100644 index 000000000..c602d8ac8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/CascadeLockTest.java @@ -0,0 +1,475 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Session; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * CascadeLockTest - Test to check CascadeType.LOCK + * + * @author Vlad Mihalcea + */ +public class CascadeLockTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostDetails.class, + PostComment.class + }; + } + + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("Hibernate Master Class"); + + post.addDetails(new PostDetails()); + post.addComment(new PostComment("Good post!").setId(1L)); + post.addComment(new PostComment("Nice post!").setId(2L)); + + entityManager.persist(post); + }); + } + + @Test + public void testCascadeLockOnManagedEntityWithScope() throws InterruptedException { + LOGGER.info("Test lock cascade for managed entity"); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.unwrap(Session.class) + .buildLockRequest( + new LockOptions(LockMode.PESSIMISTIC_WRITE)) + .setScope(true) + .lock(post); + }); + } + + @Test + public void testCascadeLockOnManagedEntityWithJPA() throws InterruptedException { + LOGGER.info("Test lock cascade for managed entity"); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, Collections.singletonMap( + AvailableSettings.JAKARTA_LOCK_SCOPE, PessimisticLockScope.EXTENDED + )); + }); + } + + @Test + public void testCascadeLockOnManagedEntityWithQuery() throws InterruptedException { + LOGGER.info("Test lock cascade for managed entity"); + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.details + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .getSingleResult(); + }); + } + + @Test + public void testCascadeLockOnManagedEntityWithAssociationsInitialzied() throws InterruptedException { + LOGGER.info("Test lock cascade for managed entity"); + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + Post post = (Post) entityManager.createQuery(""" + select p + from Post p + join fetch p.details + join fetch p.comments + where + p.id = :id + """ + ).setParameter("id", 1L) + .getSingleResult(); + session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).setScope(true).lock(post); + }); + } + + @Test + public void testCascadeLockOnManagedEntityWithAssociationsInitializedAndJpa() throws InterruptedException { + LOGGER.info("Test lock cascade for managed entity"); + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.details + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, Collections.singletonMap( + AvailableSettings.JAKARTA_LOCK_SCOPE, PessimisticLockScope.EXTENDED + )); + }); + } + + private void containsPost(EntityManager entityManager, Post post, boolean expected) { + assertEquals(expected, entityManager.contains(post)); + assertEquals(expected, (entityManager.contains(post.getDetails()))); + for (PostComment comment : post.getComments()) { + assertEquals(expected, (entityManager.contains(comment))); + } + } + + @Test + public void testCascadeLockOnDetachedEntityWithoutScope() { + LOGGER.info("Test lock cascade for detached entity without scope"); + + //Load the Post entity, which will become detached + Post post = doInJPA(entityManager -> + (Post) entityManager.createQuery(""" + select p + from Post p + join fetch p.details + join fetch p.comments + where p.id = :id + """ + ).setParameter("id", 1L) + .getSingleResult()); + + //Change the detached entity state + post.setTitle("Hibernate Training"); + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + //The Post entity graph is detached + containsPost(entityManager, post, false); + + //The Lock request associates the entity graph and locks the requested entity + session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(post); + + //Hibernate doesn't know if the entity is dirty + assertEquals("Hibernate Training", post.getTitle()); + + //The Post entity graph is attached + containsPost(entityManager, post, true); + }); + doInJPA(entityManager -> { + //The detached Post entity changes have been lost + Post _post = (Post) entityManager.find(Post.class, 1L); + assertEquals("Hibernate Master Class", _post.getTitle()); + }); + } + + @Test + public void testCascadeLockOnDetachedEntityWithScope() { + LOGGER.info("Test lock cascade for detached entity with scope"); + + //Load the Post entity, which will become detached + Post post = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + join fetch p.details + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + }); + + doInJPA(entityManager -> { + LOGGER.info("Reattach and lock"); + entityManager.unwrap(Session.class) + .buildLockRequest( + new LockOptions(LockMode.PESSIMISTIC_WRITE)) + .setScope(true) + .lock(post); + + //The Post entity graph is attached + containsPost(entityManager, post, true); + }); + doInJPA(entityManager -> { + //The detached Post entity changes have been lost + Post _post = (Post) entityManager.find(Post.class, 1L); + assertEquals("Hibernate Master Class", _post.getTitle()); + }); + } + + @Test + public void testCascadeLockOnDetachedEntityUninitializedWithScope() { + LOGGER.info("Test lock cascade for detached entity with scope"); + + //Load the Post entity, which will become detached + Post post = doInJPA(entityManager -> (Post) entityManager.find(Post.class, 1L)); + + doInJPA(entityManager -> { + LOGGER.info("Reattach and lock entity with associations not initialized"); + entityManager.unwrap(Session.class) + .buildLockRequest( + new LockOptions(LockMode.PESSIMISTIC_WRITE)) + .setScope(true) + .lock(post); + + LOGGER.info("Check entities are reattached"); + //The Post entity graph is attached + containsPost(entityManager, post, true); + }); + } + + @Test + public void testCascadeLockOnDetachedChildEntityUninitializedWithScope() { + LOGGER.info("Test lock cascade for detached entity with scope"); + + //Load the Post entity, which will become detached + PostComment postComment = doInJPA(entityManager -> (PostComment) entityManager.find(PostComment.class, 2L)); + + doInJPA(entityManager -> { + LOGGER.info("Reattach and lock entity with associations not initialized"); + entityManager.unwrap(Session.class) + .buildLockRequest( + new LockOptions(LockMode.PESSIMISTIC_WRITE)) + .lock(postComment); + }); + } + + @Test + public void testUpdateOnDetachedEntity() { + LOGGER.info("Test update for detached entity"); + //Load the Post entity, which will become detached + Post post = doInJPA(entityManager -> (Post) entityManager.createQuery(""" + select p + from Post p + join fetch p.details + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult()); + + //Change the detached entity state + post.setTitle("Hibernate Training"); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + //The Post entity graph is detached + containsPost(entityManager, post, false); + + //The update will trigger an entity state flush and attach the entity graph + session.update(post); + + //The Post entity graph is attached + containsPost(entityManager, post, true); + }); + doInJPA(entityManager -> { + Post _post = (Post) entityManager.find(Post.class, 1L); + assertEquals("Hibernate Training", _post.getTitle()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String body; + + @Version + private short version; + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) + private List comments = new ArrayList<>(); + + @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true, fetch = FetchType.LAZY) + private PostDetails details; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public int getVersion() { + return version; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public PostDetails getDetails() { + return details; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void addDetails(PostDetails details) { + this.details = details; + details.setPost(this); + } + + public void removeDetails() { + this.details.setPost(null); + this.details = null; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Version + private short version; + + public PostDetails() { + createdOn = new Date(); + } + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public int getVersion() { + return version; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + @Version + private short version; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public int getVersion() { + return version; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/CascadeLockUnidirectionalOneToManyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/CascadeLockUnidirectionalOneToManyTest.java new file mode 100644 index 000000000..22f0c4a30 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/CascadeLockUnidirectionalOneToManyTest.java @@ -0,0 +1,445 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Session; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * CascadeLockTest - Test to check CascadeType.LOCK + * + * @author Vlad Mihalcea + */ +public class CascadeLockUnidirectionalOneToManyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostDetails.class, + PostComment.class + }; + } + + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("Hibernate Master Class"); + + post.addDetails(new PostDetails()); + post.addComment(new PostComment("Good post!").setId(1L)); + post.addComment(new PostComment("Nice post!").setId(2L)); + + entityManager.persist(post); + }); + } + + @Test + public void testCascadeLockOnManagedEntityWithScope() throws InterruptedException { + LOGGER.info("Test lock cascade for managed entity"); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.unwrap(Session.class) + .buildLockRequest( + new LockOptions(LockMode.PESSIMISTIC_WRITE)) + .setScope(true) + .lock(post); + }); + } + + @Test + public void testCascadeLockOnManagedEntityWithJPA() throws InterruptedException { + LOGGER.info("Test lock cascade for managed entity"); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, Collections.singletonMap( + AvailableSettings.JAKARTA_LOCK_SCOPE, PessimisticLockScope.EXTENDED + )); + }); + } + + @Test + public void testCascadeLockOnManagedEntityWithQuery() throws InterruptedException { + LOGGER.info("Test lock cascade for managed entity"); + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.details + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .getSingleResult(); + }); + } + + @Test + public void testCascadeLockOnManagedEntityWithAssociationsInitialzied() throws InterruptedException { + LOGGER.info("Test lock cascade for managed entity"); + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + Post post = (Post) entityManager.createQuery(""" + select p + from Post p + join fetch p.details + join fetch p.comments + where + p.id = :id + """ + ).setParameter("id", 1L) + .getSingleResult(); + session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).setScope(true).lock(post); + }); + } + + @Test + public void testCascadeLockOnManagedEntityWithAssociationsInitializedAndJpa() throws InterruptedException { + LOGGER.info("Test lock cascade for managed entity"); + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.details + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, Collections.singletonMap( + AvailableSettings.JAKARTA_LOCK_SCOPE, PessimisticLockScope.EXTENDED + )); + }); + } + + private void containsPost(EntityManager entityManager, Post post, boolean expected) { + assertEquals(expected, entityManager.contains(post)); + assertEquals(expected, (entityManager.contains(post.getDetails()))); + for (PostComment comment : post.getComments()) { + assertEquals(expected, (entityManager.contains(comment))); + } + } + + @Test + public void testCascadeLockOnDetachedEntityWithoutScope() { + LOGGER.info("Test lock cascade for detached entity without scope"); + + //Load the Post entity, which will become detached + Post post = doInJPA(entityManager -> + (Post) entityManager.createQuery(""" + select p + from Post p + join fetch p.details + join fetch p.comments + where p.id = :id + """ + ).setParameter("id", 1L) + .getSingleResult()); + + //Change the detached entity state + post.setTitle("Hibernate Training"); + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + //The Post entity graph is detached + containsPost(entityManager, post, false); + + //The Lock request associates the entity graph and locks the requested entity + session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(post); + + //Hibernate doesn't know if the entity is dirty + assertEquals("Hibernate Training", post.getTitle()); + + //The Post entity graph is attached + containsPost(entityManager, post, true); + }); + doInJPA(entityManager -> { + //The detached Post entity changes have been lost + Post _post = (Post) entityManager.find(Post.class, 1L); + assertEquals("Hibernate Master Class", _post.getTitle()); + }); + } + + @Test + public void testCascadeLockOnDetachedEntityWithScope() { + LOGGER.info("Test lock cascade for detached entity with scope"); + + //Load the Post entity, which will become detached + Post post = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + join fetch p.details + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + }); + + doInJPA(entityManager -> { + LOGGER.info("Reattach and lock"); + entityManager.unwrap(Session.class) + .buildLockRequest( + new LockOptions(LockMode.PESSIMISTIC_WRITE)) + .setScope(true) + .lock(post); + + //The Post entity graph is attached + containsPost(entityManager, post, true); + }); + doInJPA(entityManager -> { + //The detached Post entity changes have been lost + Post _post = (Post) entityManager.find(Post.class, 1L); + assertEquals("Hibernate Master Class", _post.getTitle()); + }); + } + + @Test + public void testCascadeLockOnDetachedEntityUninitializedWithScope() { + LOGGER.info("Test lock cascade for detached entity with scope"); + + //Load the Post entity, which will become detached + Post post = doInJPA(entityManager -> (Post) entityManager.find(Post.class, 1L)); + + doInJPA(entityManager -> { + LOGGER.info("Reattach and lock entity with associations not initialized"); + entityManager.unwrap(Session.class) + .buildLockRequest( + new LockOptions(LockMode.PESSIMISTIC_WRITE)) + .setScope(true) + .lock(post); + + LOGGER.info("Check entities are reattached"); + //The Post entity graph is attached + containsPost(entityManager, post, true); + }); + } + + @Test + public void testCascadeLockOnDetachedChildEntityUninitializedWithScope() { + LOGGER.info("Test lock cascade for detached entity with scope"); + + //Load the Post entity, which will become detached + PostComment postComment = doInJPA( + entityManager -> (PostComment) entityManager.find(PostComment.class, 1L) + ); + + doInJPA(entityManager -> { + LOGGER.info("Reattach and lock entity with associations not initialized"); + entityManager.unwrap(Session.class) + .buildLockRequest( + new LockOptions(LockMode.PESSIMISTIC_WRITE)) + .lock(postComment); + }); + } + + @Test + public void testUpdateOnDetachedEntity() { + LOGGER.info("Test update for detached entity"); + //Load the Post entity, which will become detached + Post post = doInJPA(entityManager -> (Post) entityManager.createQuery(""" + select p + from Post p + join fetch p.details + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult()); + + //Change the detached entity state + post.setTitle("Hibernate Training"); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + //The Post entity graph is detached + containsPost(entityManager, post, false); + + //The update will trigger an entity state flush and attach the entity graph + session.update(post); + + //The Post entity graph is attached + containsPost(entityManager, post, true); + }); + doInJPA(entityManager -> { + Post _post = (Post) entityManager.find(Post.class, 1L); + assertEquals("Hibernate Training", _post.getTitle()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String body; + + @Version + private short version; + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true, fetch = FetchType.LAZY, optional = false) + private PostDetails details; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public PostDetails getDetails() { + return details; + } + + public void addComment(PostComment comment) { + comments.add(comment); + } + + public void addDetails(PostDetails details) { + this.details = details; + details.setPost(this); + } + + public void removeDetails() { + this.details.setPost(null); + this.details = null; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Version + private short version; + + public PostDetails() { + createdOn = new Date(); + } + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + private String review; + + @Version + private short version; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/DefaultOptimisticLockingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/DefaultOptimisticLockingTest.java new file mode 100644 index 000000000..25f06a0f3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/DefaultOptimisticLockingTest.java @@ -0,0 +1,210 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil; +import org.hibernate.StaleStateException; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class DefaultOptimisticLockingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testOptimisticLocking() { + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + entityManager.flush(); + post.setTitle("High-Performance Hibernate"); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, post.getVersion()); + }); + } + + @Test + public void testOptimisticLockExceptionUpdate() { + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + }); + + try { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + executeSync(() -> { + doInJPA(_entityManager -> { + Post _post = _entityManager.find(Post.class, 1L); + _post.setTitle("High-Performance JDBC"); + }); + }); + + post.setTitle("High-Performance Hibernate"); + }); + } catch (Exception expected) { + LOGGER.error("Throws", expected); + + assertEquals(OptimisticLockException.class, expected.getCause().getClass()); + assertTrue(StaleStateException.class.isAssignableFrom(expected.getCause().getCause().getClass())); + } + } + + @Test + public void testOptimisticLockExceptionRemove() { + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + }); + + try { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + executeSync(() -> { + doInJPA(_entityManager -> { + Post _post = _entityManager.find(Post.class, 1L); + + _entityManager.remove(_post); + }); + }); + + post.setTitle("High-Performance Hibernate"); + }); + } catch (Exception expected) { + LOGGER.error("Throws", expected); + + assertEquals(OptimisticLockException.class, expected.getCause().getClass()); + assertTrue(StaleStateException.class.isAssignableFrom(expected.getCause().getCause().getClass())); + } + } + + @Test + public void testOptimisticLockExceptionMerge() { + + Post _post = doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + + return post; + }); + + doInJPA(_entityManager -> { + Post post = _entityManager.find(Post.class, 1L); + post.setTitle("High-Performance JDBC"); + }); + + _post.setTitle("High-Performance Hibernate"); + + try { + doInJPA(entityManager -> { + entityManager.merge(_post); + }); + } catch (Exception expected) { + LOGGER.error("Throws", expected); + assertEquals(OptimisticLockException.class, expected.getClass()); + assertTrue(StaleStateException.class.isAssignableFrom(ExceptionUtil.rootCause(expected).getClass())); + } + } + + @Test + public void testOptimisticLockExceptionMergeJsonTransform() { + + String postJsonString = doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + + return JacksonUtil.toString(post); + }); + + doInJPA(_entityManager -> { + Post post = _entityManager.find(Post.class, 1L); + post.setTitle("High-Performance JDBC"); + }); + + ObjectNode postJsonNode = (ObjectNode) JacksonUtil.toJsonNode(postJsonString); + postJsonNode.put("title", "High-Performance Hibernate"); + + try { + doInJPA(entityManager -> { + Post detachedPost = JacksonUtil.fromString(postJsonNode.toString(), Post.class); + entityManager.merge(detachedPost); + }); + } catch (Exception expected) { + LOGGER.error("Throws", expected); + assertEquals(OptimisticLockException.class, expected.getClass()); + assertTrue(ExceptionUtil.rootCause(expected) instanceof StaleStateException); + } + } + + @Entity(name = "Post") @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getVersion() { + return version; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityFirstLevelCacheReuseTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityFirstLevelCacheReuseTest.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityFirstLevelCacheReuseTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityFirstLevelCacheReuseTest.java index 814421486..f6bc3f91d 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityFirstLevelCacheReuseTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityFirstLevelCacheReuseTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import static org.junit.Assert.*; @@ -55,11 +55,6 @@ public void testOptimisticLocking() { }); } - /** - * Product - Product - * - * @author Vlad Mihalcea - */ @Entity(name = "Product") @Table(name = "product") public static class Product { diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOnBidirectionalChildOwningCollectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOnBidirectionalChildOwningCollectionTest.java similarity index 95% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOnBidirectionalChildOwningCollectionTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOnBidirectionalChildOwningCollectionTest.java index 75f05660b..9ccc2108c 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOnBidirectionalChildOwningCollectionTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOnBidirectionalChildOwningCollectionTest.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; @@ -27,7 +27,7 @@ public static class Post implements AbstractEntityOptimisticLockingCollectionTes private List comments = new ArrayList(); @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOnBidirectionalParentOwningCollectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOnBidirectionalParentOwningCollectionTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOnBidirectionalParentOwningCollectionTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOnBidirectionalParentOwningCollectionTest.java index 23e1b00ab..89a8febf3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOnBidirectionalParentOwningCollectionTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOnBidirectionalParentOwningCollectionTest.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; @@ -29,7 +29,7 @@ public static class Post implements AbstractEntityOptimisticLockingCollectionTes private List comments = new ArrayList(); @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOnComponentCollectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOnComponentCollectionTest.java similarity index 95% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOnComponentCollectionTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOnComponentCollectionTest.java index 20654ee64..4c377e91b 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOnComponentCollectionTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOnComponentCollectionTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; import org.hibernate.annotations.Parent; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; @@ -32,7 +32,7 @@ public static class Post implements AbstractEntityOptimisticLockingCollectionTes private List comments = new ArrayList(); @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOnUnidirectionalCollectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOnUnidirectionalCollectionTest.java similarity index 95% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOnUnidirectionalCollectionTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOnUnidirectionalCollectionTest.java index eead015b5..7000e0d78 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOnUnidirectionalCollectionTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOnUnidirectionalCollectionTest.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; @@ -30,7 +30,7 @@ public static class Post implements AbstractEntityOptimisticLockingCollectionTes private List comments = new ArrayList(); @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOverruleOnBidirectionalParentOwningCollectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOverruleOnBidirectionalParentOwningCollectionTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOverruleOnBidirectionalParentOwningCollectionTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOverruleOnBidirectionalParentOwningCollectionTest.java index f97b358e9..c07ff34c4 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/EntityOptimisticLockingOverruleOnBidirectionalParentOwningCollectionTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/EntityOptimisticLockingOverruleOnBidirectionalParentOwningCollectionTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; import org.hibernate.annotations.OptimisticLock; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; @@ -29,7 +29,7 @@ public static class Post implements AbstractEntityOptimisticLockingCollectionTe private List comments = new ArrayList(); @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/FollowOnLockingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/FollowOnLockingTest.java similarity index 79% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/FollowOnLockingTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/FollowOnLockingTest.java index 136804733..e2f2f88ef 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/FollowOnLockingTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/FollowOnLockingTest.java @@ -1,16 +1,13 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; - -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; +package com.vladmihalcea.hpjp.hibernate.concurrency; +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import jakarta.persistence.*; import org.hibernate.LockMode; import org.hibernate.LockOptions; -import org.hibernate.dialect.Oracle10gDialect; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; -import javax.persistence.*; import java.util.List; import static org.junit.Assert.assertEquals; @@ -18,7 +15,7 @@ /** * @author Vlad Mihalcea */ -public class FollowOnLockingTest extends AbstractOracleXEIntegrationTest { +public class FollowOnLockingTest extends AbstractOracleIntegrationTest { @Override protected Class[] entities() { @@ -54,7 +51,7 @@ public void testPessimisticWrite() { .setParameter("status", PostStatus.PENDING) .setMaxResults(5) //.setLockMode(LockModeType.PESSIMISTIC_WRITE) - .unwrap(org.hibernate.Query.class) + .unwrap(org.hibernate.query.Query.class) .setLockOptions(new LockOptions(LockMode.PESSIMISTIC_WRITE).setTimeOut(LockOptions.SKIP_LOCKED)) .list(); @@ -73,7 +70,7 @@ public void testUpgradeSkipLocked() { Post.class) .setParameter("status", PostStatus.PENDING) .setFirstResult(2) - .unwrap(org.hibernate.Query.class) + .unwrap(org.hibernate.query.Query.class) .setLockOptions(new LockOptions(LockMode.UPGRADE_SKIPLOCKED)) .list(); @@ -93,7 +90,7 @@ public void testUpgradeSkipLockedOrderBy() { Post.class) .setParameter("status", PostStatus.PENDING) .setFirstResult(2) - .unwrap(org.hibernate.Query.class) + .unwrap(org.hibernate.query.Query.class) .setLockOptions(new LockOptions(LockMode.UPGRADE_SKIPLOCKED)) .list(); @@ -102,6 +99,7 @@ public void testUpgradeSkipLockedOrderBy() { } @Test + @Ignore public void testUpgradeSkipLockedOrderByMaxResult() { LOGGER.info("Test lock contention"); doInJPA(entityManager -> { @@ -113,7 +111,7 @@ public void testUpgradeSkipLockedOrderByMaxResult() { Post.class) .setParameter("status", PostStatus.PENDING) .setMaxResults(5) - .unwrap(org.hibernate.Query.class) + .unwrap(org.hibernate.query.Query.class) .setLockOptions(new LockOptions(LockMode.UPGRADE_SKIPLOCKED)) .list(); @@ -121,25 +119,6 @@ public void testUpgradeSkipLockedOrderByMaxResult() { }); } - @Override - protected DataSourceProvider dataSourceProvider() { - return new OracleDataSourceProvider() { - @Override - public String hibernateDialect() { - return OracleDialect.class.getName(); - } - }; - } - - public static class OracleDialect extends Oracle10gDialect { - - @Override - public boolean useFollowOnLocking() { - //return false; - return super.useFollowOnLocking(); - } - } - @Entity(name = "Post") @Table(name = "post") public static class Post { @@ -155,7 +134,7 @@ public static class Post { private PostStatus status; @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModeOptimisticForceIncrementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModeOptimisticForceIncrementTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModeOptimisticForceIncrementTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModeOptimisticForceIncrementTest.java index 84f079125..e01fb6fb3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModeOptimisticForceIncrementTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModeOptimisticForceIncrementTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.annotations.Immutable; import org.junit.Before; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; @@ -104,7 +104,7 @@ public Repository(String name) { } @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModeOptimisticRaceConditionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModeOptimisticRaceConditionTest.java similarity index 94% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModeOptimisticRaceConditionTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModeOptimisticRaceConditionTest.java index 20de538d3..08e12f80d 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModeOptimisticRaceConditionTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModeOptimisticRaceConditionTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; import org.hibernate.*; import org.hibernate.dialect.lock.OptimisticEntityLockException; import org.junit.Test; -import javax.persistence.EntityManager; -import javax.persistence.LockModeType; +import jakarta.persistence.EntityManager; +import jakarta.persistence.LockModeType; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModeOptimisticTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModeOptimisticTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModeOptimisticTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModeOptimisticTest.java index d1eb9eed0..4fd35b300 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModeOptimisticTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModeOptimisticTest.java @@ -1,10 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; import org.junit.Test; -import javax.persistence.LockModeType; -import javax.persistence.OptimisticLockException; -import javax.persistence.RollbackException; +import jakarta.persistence.LockModeType; +import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.RollbackException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModeOptimisticWithPessimisticLockUpgradeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModeOptimisticWithPessimisticLockUpgradeTest.java similarity index 75% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModeOptimisticWithPessimisticLockUpgradeTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModeOptimisticWithPessimisticLockUpgradeTest.java index b790b3f72..146d2aaa5 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModeOptimisticWithPessimisticLockUpgradeTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModeOptimisticWithPessimisticLockUpgradeTest.java @@ -1,7 +1,7 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import javax.persistence.EntityManager; -import javax.persistence.LockModeType; +import jakarta.persistence.EntityManager; +import jakarta.persistence.LockModeType; /** * LockModeOptimisticWithPessimisticLockUpgradeTest - Test to check LockMode.OPTIMISTIC with pessimistic lock upgrade diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModePessimisticForceIncrementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModePessimisticForceIncrementTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModePessimisticForceIncrementTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModePessimisticForceIncrementTest.java index 22755eb8a..bd8a49088 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/LockModePessimisticForceIncrementTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModePessimisticForceIncrementTest.java @@ -1,14 +1,17 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; import org.hibernate.StaleObjectStateException; import org.hibernate.annotations.Immutable; +import org.hibernate.cfg.AvailableSettings; import org.junit.Before; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; +import java.util.Properties; import java.util.concurrent.CountDownLatch; import static org.junit.Assert.assertEquals; @@ -42,6 +45,11 @@ public void init() { }); } + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS, "true"); + } + @Test public void testPessimisticForceIncrementLocking() throws InterruptedException { LOGGER.info("Test Single PESSIMISTIC_FORCE_INCREMENT Lock Mode "); @@ -131,7 +139,7 @@ public Repository(String name) { } @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModePessimisticReadWriteTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModePessimisticReadWriteTest.java new file mode 100644 index 000000000..77221399b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/LockModePessimisticReadWriteTest.java @@ -0,0 +1,347 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Session; +import org.hibernate.StaleObjectStateException; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + + +/** + * @author Vlad Mihalcea + */ +public class LockModePessimisticReadWriteTest extends AbstractTest { + + public static final int WAIT_MILLIS = 500; + + private interface LockRequestCallable { + void lock(Session session, Post post); + } + + private final CountDownLatch endLatch = new CountDownLatch(1); + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.ORACLE; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + }); + } + + private void testPessimisticLocking(LockRequestCallable primaryLockRequestCallable, LockRequestCallable secondaryLockRequestCallable) { + doInJPA(entityManager -> { + try { + Session session = entityManager.unwrap(Session.class); + Post post = entityManager.find(Post.class, 1L); + primaryLockRequestCallable.lock(session, post); + executeAsync( + () -> { + doInJPA(_entityManager -> { + Session _session = _entityManager.unwrap(Session.class); + Post _post = _entityManager.find(Post.class, 1L); + secondaryLockRequestCallable.lock(_session, _post); + }); + }, + endLatch::countDown + ); + sleep(WAIT_MILLIS); + } catch (StaleObjectStateException e) { + LOGGER.info("Optimistic locking failure: ", e); + } + }); + awaitOnLatch(endLatch); + } + + @Test + public void testPessimisticRead() { + LOGGER.info("Test PESSIMISTIC_READ"); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L, LockModeType.PESSIMISTIC_READ); + }); + } + + @Test + public void testPessimisticWrite() { + LOGGER.info("Test PESSIMISTIC_WRITE"); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L, LockModeType.PESSIMISTIC_WRITE); + }); + } + + @Test + public void testPessimisticWriteAfterFetch() { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE); + }); + } + + @Test + public void testPessimisticWriteAfterFetchWithDetachedForJPA() { + Post post = doInJPA(entityManager -> { + return entityManager.find(Post.class, 1L); + }); + try { + doInJPA(entityManager -> { + entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE); + }); + } catch (IllegalArgumentException e) { + assertEquals("entity not in the persistence context", e.getMessage()); + } + } + + @Test + public void testPessimisticWriteAfterFetchWithDetachedForHibernate() { + Post post = doInJPA(entityManager -> { + return entityManager.find(Post.class, 1L); + }); + doInJPA(entityManager -> { + LOGGER.info("Lock and reattach"); + entityManager.unwrap(Session.class) + .buildLockRequest( + new LockOptions(LockMode.PESSIMISTIC_WRITE)) + .lock(post); + post.setTitle("High-Performance Hibernate"); + }); + } + + @Test + public void testPessimisticReadDoesNotBlockPessimisticRead() throws InterruptedException { + LOGGER.info("Test PESSIMISTIC_READ doesn't block PESSIMISTIC_READ"); + testPessimisticLocking( + (session, post) -> { + session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(post); + LOGGER.info("PESSIMISTIC_READ acquired"); + }, + (session, post) -> { + session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(post); + LOGGER.info("PESSIMISTIC_READ acquired"); + } + ); + } + + @Test + public void testPessimisticReadBlocksUpdate() throws InterruptedException { + LOGGER.info("Test PESSIMISTIC_READ blocks UPDATE"); + testPessimisticLocking( + (session, post) -> { + session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(post); + LOGGER.info("PESSIMISTIC_READ acquired"); + }, + (session, post) -> { + post.setTitle("High-Performance Java Persistence 2nd edition"); + session.flush(); + LOGGER.info("Implicit lock acquired"); + } + ); + } + + @Test + public void testPessimisticReadWithPessimisticWriteNoWait() throws InterruptedException { + LOGGER.info("Test PESSIMISTIC_READ blocks PESSIMISTIC_WRITE, NO WAIT fails fast"); + testPessimisticLocking( + (session, post) -> { + session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(post); + LOGGER.info("PESSIMISTIC_READ acquired"); + }, + (session, post) -> { + session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).setTimeOut(Session.LockRequest.PESSIMISTIC_NO_WAIT).lock(post); + LOGGER.info("PESSIMISTIC_WRITE acquired"); + } + ); + } + + @Test + public void testPessimisticWriteBlocksPessimisticRead() throws InterruptedException { + LOGGER.info("Test PESSIMISTIC_WRITE blocks PESSIMISTIC_READ"); + testPessimisticLocking( + (session, post) -> { + session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(post); + LOGGER.info("PESSIMISTIC_WRITE acquired"); + }, + (session, post) -> { + session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(post); + LOGGER.info("PESSIMISTIC_READ acquired"); + } + ); + } + + @Test + public void testPessimisticWriteBlocksPessimisticWrite() throws InterruptedException { + LOGGER.info("Test PESSIMISTIC_WRITE blocks PESSIMISTIC_WRITE"); + testPessimisticLocking( + (session, post) -> { + session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(post); + LOGGER.info("PESSIMISTIC_WRITE acquired"); + }, + (session, post) -> { + session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(post); + LOGGER.info("PESSIMISTIC_WRITE acquired"); + } + ); + } + + @Test + public void testPessimisticNoWait() { + LOGGER.info("Test PESSIMISTIC_READ blocks PESSIMISTIC_WRITE, NO WAIT fails fast"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L, + LockModeType.PESSIMISTIC_WRITE + ); + + executeSync(() -> doInJPA(_entityManager -> { + try { + Post _post = _entityManager.find(Post.class, 1L, + LockModeType.PESSIMISTIC_WRITE, + Collections.singletonMap( + AvailableSettings.JAKARTA_LOCK_TIMEOUT, LockOptions.NO_WAIT + ) + ); + fail("Should throw PessimisticEntityLockException"); + } catch (LockTimeoutException expected) { + //This is expected since the first transaction already acquired this lock + } + })); + }); + } + + @Test + public void testPessimisticNoWaitJPA() throws InterruptedException { + LOGGER.info("Test PESSIMISTIC_READ blocks PESSIMISTIC_WRITE, NO WAIT fails fast"); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, + Collections.singletonMap("jakarta.persistence.lock.timeout", 0) + ); + }); + } + + @Test + public void testPessimisticTimeout() throws InterruptedException { + doInJPA(entityManager -> { + Post post = entityManager.getReference(Post.class, 1L); + + entityManager.unwrap(Session.class) + .buildLockRequest( + new LockOptions(LockMode.PESSIMISTIC_WRITE) + .setTimeOut((int) TimeUnit.SECONDS.toMillis(3))) + .lock(post); + }); + } + + @Test + public void testPessimisticTimeoutJPA() throws InterruptedException { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.lock(post, LockModeType.PESSIMISTIC_WRITE, + Collections.singletonMap("jakarta.persistence.lock.timeout", + TimeUnit.SECONDS.toMillis(3)) + ); + }); + } + + @Test + public void testPessimisticWriteQuery() throws InterruptedException { + doInJPA(entityManager -> { + List comments = entityManager + .createQuery( + "select pc " + + "from PostComment pc " + + "join fetch pc.post p ", PostComment.class) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .setHint("jakarta.persistence.lock.timeout", 0) + .getResultList(); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/NoWaitTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/NoWaitTest.java new file mode 100644 index 000000000..9fb0f7c36 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/NoWaitTest.java @@ -0,0 +1,106 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.LockOptions; +import org.hibernate.jpa.SpecHints; +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class NoWaitTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + public void afterInit() { + doInJPA(entityManager -> { + for (long i = 1; i <= 10; i++) { + Post post = new Post(); + post.setId(i); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + } + }); + } + + @Test + public void testLockContention() { + LOGGER.info("Test lock contention"); + + doInJPA(entityManager -> { + assertNotNull( + getAndLockPost( + entityManager, + 1L + ) + ); + + try { + executeSync(() -> { + doInJPA(_entityManager -> { + assertNotNull( + getAndLockPost( + _entityManager, + 1L + ) + ); + }); + }); + } catch (Exception e) { + assertTrue(ExceptionUtil.isLockTimeout(e)); + } + }); + } + + public Post getAndLockPost(EntityManager entityManager, Long postId) { + return entityManager.find( + Post.class, + postId, + LockModeType.PESSIMISTIC_WRITE, + Map.of(SpecHints.HINT_SPEC_LOCK_TIMEOUT, LockOptions.NO_WAIT) + ); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingAggregateRootVersionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingAggregateRootVersionTest.java new file mode 100644 index 000000000..a21e5171a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingAggregateRootVersionTest.java @@ -0,0 +1,347 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.HibernateException; +import org.hibernate.LockMode; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.boot.Metadata; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.Status; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.*; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Vlad Mihalcea + */ +public class OptimisticLockingAggregateRootVersionTest extends AbstractTest { + + public static class RootAwareEventListenerIntegrator implements org.hibernate.integrator.spi.Integrator { + + public static final RootAwareEventListenerIntegrator INSTANCE = new RootAwareEventListenerIntegrator(); + + @Override + public void integrate( + Metadata metadata, + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + final EventListenerRegistry eventListenerRegistry = + serviceRegistry.getService( EventListenerRegistry.class ); + + eventListenerRegistry.appendListeners(EventType.PERSIST, RootAwareInsertEventListener.INSTANCE); + eventListenerRegistry.appendListeners(EventType.FLUSH_ENTITY, RootAwareUpdateAndDeleteEventListener.INSTANCE); + } + + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + } + } + + public static class RootAwareInsertEventListener implements PersistEventListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(RootAwareInsertEventListener.class); + + public static final RootAwareInsertEventListener INSTANCE = new RootAwareInsertEventListener(); + + @Override + public void onPersist(PersistEvent event) throws HibernateException { + final Object entity = event.getObject(); + + if(entity instanceof RootAware rootAware) { + Object root = rootAware.root(); + event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT); + + LOGGER.info("Incrementing {} entity version because a {} child entity has been inserted", root, entity); + } + } + + @Override + public void onPersist(PersistEvent event, PersistContext persistContext) throws HibernateException { + onPersist(event); + } + } + + public static class RootAwareUpdateAndDeleteEventListener implements FlushEntityEventListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(RootAwareUpdateAndDeleteEventListener.class); + + public static final RootAwareUpdateAndDeleteEventListener INSTANCE = new RootAwareUpdateAndDeleteEventListener(); + + @Override + public void onFlushEntity(FlushEntityEvent event) throws HibernateException { + final EntityEntry entry = event.getEntityEntry(); + final Object entity = event.getEntity(); + final boolean mightBeDirty = entry.requiresDirtyCheck( entity ); + + if(mightBeDirty && entity instanceof RootAware rootAware) { + if(updated(event)) { + Object root = rootAware.root(); + LOGGER.info("Incrementing {} entity version because a {} child entity has been updated", root, entity); + incrementRootVersion(event, root); + } + else if (deleted(event)) { + Object root = rootAware.root(); + LOGGER.info("Incrementing {} entity version because a {} child entity has been deleted", root, entity); + incrementRootVersion(event, root); + } + } + } + + private void incrementRootVersion(FlushEntityEvent event, Object root) { + event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT); + } + + private boolean deleted(FlushEntityEvent event) { + return event.getEntityEntry().getStatus() == Status.DELETED; + } + + private boolean updated(FlushEntityEvent event) { + final EntityEntry entry = event.getEntityEntry(); + final Object entity = event.getEntity(); + + int[] dirtyProperties; + EntityPersister persister = entry.getPersister(); + final Object[] values = event.getPropertyValues(); + SessionImplementor session = event.getSession(); + + if ( event.hasDatabaseSnapshot() ) { + dirtyProperties = persister.findModified( event.getDatabaseSnapshot(), values, entity, session ); + } + else { + dirtyProperties = persister.findDirty( values, entry.getLoadedState(), entity, session ); + } + + return dirtyProperties != null; + } + } + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + PostCommentDetails.class, + }; + } + + @Override + protected Integrator integrator() { + return RootAwareEventListenerIntegrator.INSTANCE; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + PostComment comment1 = new PostComment(); + comment1.setId(1L); + comment1.setReview("Good"); + comment1.setPost(post); + + PostCommentDetails details1 = new PostCommentDetails(); + details1.setComment(comment1); + details1.setVotes(10); + + PostComment comment2 = new PostComment(); + comment2.setId(2L); + comment2.setReview("Excellent"); + comment2.setPost(post); + + PostCommentDetails details2 = new PostCommentDetails(); + details2.setComment(comment2); + details2.setVotes(10); + + entityManager.persist(post); + entityManager.persist(comment1); + entityManager.persist(comment2); + entityManager.persist(details1); + entityManager.persist(details2); + }); + + doInJPA(entityManager -> { + PostCommentDetails postCommentDetails = entityManager.createQuery(""" + select pcd + from PostCommentDetails pcd + join fetch pcd.comment pc + join fetch pc.post p + where pcd.id = :id + """, PostCommentDetails.class) + .setParameter("id", 2L) + .getSingleResult(); + + postCommentDetails.setVotes(15); + }); + + doInJPA(entityManager -> { + PostComment postComment = entityManager.createQuery(""" + select pc + from PostComment pc + join fetch pc.post p + where pc.id = :id + """, PostComment.class) + .setParameter("id", 2L) + .getSingleResult(); + + postComment.setReview("Brilliant!"); + }); + + doInJPA(entityManager -> { + Post post = entityManager.getReference(Post.class, 1L); + + PostComment postComment = new PostComment(); + postComment.setId(3L); + postComment.setReview("Worth it!"); + postComment.setPost(post); + entityManager.persist(postComment); + }); + + doInJPA(entityManager -> { + PostComment postComment = entityManager.getReference(PostComment.class, 3L); + entityManager.remove(postComment); + }); + + doInJPA(entityManager -> { + Post post = entityManager.getReference(Post.class, 1L); + entityManager.createQuery( "delete from PostComment p where p.post.id = :postId" ) + .setParameter( "postId", post.getId() ) + .executeUpdate(); + + entityManager.remove(post); + }); + } + + public interface RootAware { + T root(); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private Short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment implements RootAware { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + @Override + public Post root() { + return post; + } + } + + @Entity(name = "PostCommentDetails") + @Table(name = "post_comment_details") + public static class PostCommentDetails implements RootAware { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @OnDelete( action = OnDeleteAction.CASCADE ) + private PostComment comment; + + private int votes; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public PostComment getComment() { + return comment; + } + + public void setComment(PostComment comment) { + this.comment = comment; + } + + public int getVotes() { + return votes; + } + + public void setVotes(int votes) { + this.votes = votes; + } + + @Override + public Post root() { + return comment.root(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingBidirectionalChildUpdatesRootVersionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingBidirectionalChildUpdatesRootVersionTest.java new file mode 100644 index 000000000..e3db296e0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingBidirectionalChildUpdatesRootVersionTest.java @@ -0,0 +1,337 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +import org.hibernate.Hibernate; +import org.hibernate.HibernateException; +import org.hibernate.LockMode; +import org.hibernate.boot.Metadata; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.Status; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.*; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; + +import org.junit.Test; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vladmihalcea.hpjp.util.AbstractTest; + +/** + * @author Vlad Mihalcea + */ +public class OptimisticLockingBidirectionalChildUpdatesRootVersionTest extends AbstractTest { + + public static class RootAwareEventListenerIntegrator implements Integrator { + + public static final RootAwareEventListenerIntegrator INSTANCE = new RootAwareEventListenerIntegrator(); + + @Override + public void integrate( + Metadata metadata, + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + final EventListenerRegistry eventListenerRegistry = + serviceRegistry.getService( EventListenerRegistry.class ); + + eventListenerRegistry.appendListeners( EventType.PERSIST, RootAwareInsertEventListener.INSTANCE); + eventListenerRegistry.appendListeners( EventType.FLUSH_ENTITY, RootAwareUpdateAndDeleteEventListener.INSTANCE); + } + + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + } + } + + public static class RootAwareInsertEventListener implements PersistEventListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(RootAwareInsertEventListener.class); + + public static final RootAwareInsertEventListener INSTANCE = new RootAwareInsertEventListener(); + + @Override + public void onPersist(PersistEvent event) throws HibernateException { + final Object entity = event.getObject(); + + if(entity instanceof RootAware) { + RootAware rootAware = (RootAware) entity; + Object root = rootAware.root(); + event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT); + + LOGGER.info("Incrementing {} entity version because a {} child entity has been inserted", root, entity); + } + } + + @Override + public void onPersist(PersistEvent event, PersistContext persistContext) throws HibernateException { + onPersist(event); + } + } + + public static class RootAwareUpdateAndDeleteEventListener implements FlushEntityEventListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(RootAwareUpdateAndDeleteEventListener.class); + + public static final RootAwareUpdateAndDeleteEventListener INSTANCE = new RootAwareUpdateAndDeleteEventListener(); + + @Override + public void onFlushEntity(FlushEntityEvent event) throws HibernateException { + final EntityEntry entry = event.getEntityEntry(); + final Object entity = event.getEntity(); + final boolean mightBeDirty = entry.requiresDirtyCheck( entity ); + + if(mightBeDirty && entity instanceof RootAware) { + RootAware rootAware = (RootAware) entity; + if(updated(event)) { + Object root = rootAware.root(); + LOGGER.info("Incrementing {} entity version because a {} child entity has been updated", root, entity); + incrementRootVersion(event, root); + } + else if (deleted(event)) { + Object root = rootAware.root(); + LOGGER.info("Incrementing {} entity version because a {} child entity has been deleted", root, entity); + incrementRootVersion(event, root); + } + } + } + + private void incrementRootVersion(FlushEntityEvent event, Object root) { + EntityEntry entityEntry = event.getSession().getPersistenceContext().getEntry( Hibernate.unproxy( root) ); + if(entityEntry.getStatus() != Status.DELETED) { + event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT); + } + } + + private boolean deleted(FlushEntityEvent event) { + return event.getEntityEntry().getStatus() == Status.DELETED; + } + + private boolean updated(FlushEntityEvent event) { + final EntityEntry entry = event.getEntityEntry(); + final Object entity = event.getEntity(); + + int[] dirtyProperties; + EntityPersister persister = entry.getPersister(); + final Object[] values = event.getPropertyValues(); + SessionImplementor session = event.getSession(); + + if ( event.hasDatabaseSnapshot() ) { + dirtyProperties = persister.findModified( event.getDatabaseSnapshot(), values, entity, session ); + } + else { + dirtyProperties = persister.findDirty( values, entry.getLoadedState(), entity, session ); + } + + return dirtyProperties != null; + } + } + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + PostCommentDetails.class, + }; + } + + @Override + protected Integrator integrator() { + return RootAwareEventListenerIntegrator.INSTANCE; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + PostComment comment1 = new PostComment(); + comment1.setId(1L); + comment1.setReview("Good"); + post.addComment( comment1 ); + + PostCommentDetails details1 = new PostCommentDetails(); + details1.setComment(comment1); + details1.setVotes(10); + + PostComment comment2 = new PostComment(); + comment2.setId(2L); + comment2.setReview("Excellent"); + post.addComment( comment2 ); + + PostCommentDetails details2 = new PostCommentDetails(); + details2.setComment(comment2); + details2.setVotes(10); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.getReference(Post.class, 1L); + + entityManager.remove(post); + }); + } + + public interface RootAware { + T root(); + } + + @Entity(name = "Post") @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private Short version; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment implements RootAware { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + @OneToOne(mappedBy = "comment", cascade = CascadeType.ALL) + private PostCommentDetails details; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + @Override + public Post root() { + return post; + } + + public void setDetails(PostCommentDetails details) { + this.details = details; + } + } + + @Entity(name = "PostCommentDetails") + @Table(name = "post_comment_details") + public static class PostCommentDetails implements RootAware { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private PostComment comment; + + private int votes; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public PostComment getComment() { + return comment; + } + + public void setComment(PostComment comment) { + this.comment = comment; + comment.setDetails( this ); + } + + public int getVotes() { + return votes; + } + + public void setVotes(int votes) { + this.votes = votes; + } + + @Override + public Post root() { + return comment.root(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingContractTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingContractTest.java new file mode 100644 index 000000000..1390102cd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingContractTest.java @@ -0,0 +1,335 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.HibernateException; +import org.hibernate.LockMode; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.boot.Metadata; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.Status; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.*; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Vlad Mihalcea + */ +public class OptimisticLockingContractTest extends AbstractTest { + + public static class RootAwareEventListenerIntegrator implements Integrator { + + public static final RootAwareEventListenerIntegrator INSTANCE = new RootAwareEventListenerIntegrator(); + + @Override + public void integrate( + Metadata metadata, + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + final EventListenerRegistry eventListenerRegistry = + serviceRegistry.getService( EventListenerRegistry.class ); + + eventListenerRegistry.appendListeners(EventType.PERSIST, RootAwareInsertEventListener.INSTANCE); + eventListenerRegistry.appendListeners(EventType.FLUSH_ENTITY, RootAwareUpdateAndDeleteEventListener.INSTANCE); + } + + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + } + } + + public static class RootAwareInsertEventListener implements PersistEventListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(RootAwareInsertEventListener.class); + + public static final RootAwareInsertEventListener INSTANCE = new RootAwareInsertEventListener(); + + @Override + public void onPersist(PersistEvent event) throws HibernateException { + final Object entity = event.getObject(); + + if(entity instanceof RootAware) { + RootAware rootAware = (RootAware) entity; + Object root = rootAware.root(); + event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT); + + LOGGER.info("Incrementing {} entity version because a {} child entity has been inserted", root, entity); + } + } + + @Override + public void onPersist(PersistEvent event, PersistContext persistContext) throws HibernateException { + onPersist(event); + } + } + + public static class RootAwareUpdateAndDeleteEventListener implements FlushEntityEventListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(RootAwareUpdateAndDeleteEventListener.class); + + public static final RootAwareUpdateAndDeleteEventListener INSTANCE = new RootAwareUpdateAndDeleteEventListener(); + + @Override + public void onFlushEntity(FlushEntityEvent event) throws HibernateException { + final EntityEntry entry = event.getEntityEntry(); + final Object entity = event.getEntity(); + final boolean mightBeDirty = entry.requiresDirtyCheck( entity ); + + if(mightBeDirty && entity instanceof RootAware) { + RootAware rootAware = (RootAware) entity; + if(updated(event)) { + Object root = rootAware.root(); + LOGGER.info("Incrementing {} entity version because a {} child entity has been updated", root, entity); + incrementRootVersion(event, root); + } + else if (deleted(event)) { + Object root = rootAware.root(); + LOGGER.info("Incrementing {} entity version because a {} child entity has been deleted", root, entity); + incrementRootVersion(event, root); + } + } + } + + private void incrementRootVersion(FlushEntityEvent event, Object root) { + event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT); + } + + private boolean deleted(FlushEntityEvent event) { + return event.getEntityEntry().getStatus() == Status.DELETED; + } + + private boolean updated(FlushEntityEvent event) { + final EntityEntry entry = event.getEntityEntry(); + final Object entity = event.getEntity(); + + int[] dirtyProperties; + EntityPersister persister = entry.getPersister(); + final Object[] values = event.getPropertyValues(); + SessionImplementor session = event.getSession(); + + if ( event.hasDatabaseSnapshot() ) { + dirtyProperties = persister.findModified( event.getDatabaseSnapshot(), values, entity, session ); + } + else { + dirtyProperties = persister.findDirty( values, entry.getLoadedState(), entity, session ); + } + + return dirtyProperties != null; + } + } + + @Override + protected Class[] entities() { + return new Class[]{ + Contract.class, + Annex.class, + Signature.class, + }; + } + + @Override + protected Integrator integrator() { + return RootAwareEventListenerIntegrator.INSTANCE; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Contract contract = new Contract() + .setId(1L) + .setTitle("Hypersistence Training"); + + Annex annex1 = new Annex() + .setId(1L) + .setDetails("High-Performance Java Persistence Training") + .setContract(contract); + + Signature signature1 = new Signature() + .setAnnex(annex1) + .setUserName("Vlad Mihalcea"); + + Annex annex2 = new Annex() + .setId(2L) + .setDetails("High-Performance SQL Training") + .setContract(contract); + + Signature signature2 = new Signature() + .setAnnex(annex2) + .setUserName("Vlad Mihalcea"); + + entityManager.persist(contract); + entityManager.persist(annex1); + entityManager.persist(annex2); + entityManager.persist(signature1); + entityManager.persist(signature2); + }); + + doInJPA(entityManager -> { + Annex annex = entityManager.createQuery(""" + select a + from Annex a + join fetch a.contract c + where a.id = :id + """, Annex.class) + .setParameter("id", 2L) + .getSingleResult(); + + annex.setDetails("High-Performance SQL Online Training"); + }); + + doInJPA(entityManager -> { + Contract contract = entityManager.getReference(Contract.class, 1L); + + Annex annex = new Annex() + .setId(3L) + .setDetails("Spring 6 Migration Training") + .setContract(contract); + + entityManager.persist(annex); + }); + + doInJPA(entityManager -> { + Annex annex = entityManager.getReference(Annex.class, 3L); + entityManager.remove(annex); + }); + } + + public interface RootAware { + T root(); + } + + @Entity(name = "Contract") + @Table(name = "contract") + public static class Contract { + + @Id + private Long id; + + private String title; + + @Version + private Short version; + + public Long getId() { + return id; + } + + public Contract setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Contract setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "Annex") + @Table(name = "annex") + public static class Annex implements RootAware { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Contract contract; + + private String details; + + public Long getId() { + return id; + } + + public Annex setId(Long id) { + this.id = id; + return this; + } + + public Contract getContract() { + return contract; + } + + public Annex setContract(Contract post) { + this.contract = post; + return this; + } + + public String getDetails() { + return details; + } + + public Annex setDetails(String review) { + this.details = review; + return this; + } + + @Override + public Contract root() { + return contract; + } + } + + @Entity(name = "Signature") + @Table(name = "signature") + public static class Signature implements RootAware { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @OnDelete( action = OnDeleteAction.CASCADE ) + private Annex annex; + + private String userName; + + public Long getId() { + return id; + } + + public Signature setId(Long id) { + this.id = id; + return this; + } + + public Annex getAnnex() { + return annex; + } + + public Signature setAnnex(Annex comment) { + this.annex = comment; + return this; + } + + public String getUserName() { + return userName; + } + + public Signature setUserName(String userName) { + this.userName = userName; + return this; + } + + @Override + public Contract root() { + return annex.root(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingOneRootDirtyVersioningSelectBeforeUpdateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingOneRootDirtyVersioningSelectBeforeUpdateTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingOneRootDirtyVersioningSelectBeforeUpdateTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingOneRootDirtyVersioningSelectBeforeUpdateTest.java index c21194469..bb90c4a11 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingOneRootDirtyVersioningSelectBeforeUpdateTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingOneRootDirtyVersioningSelectBeforeUpdateTest.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.Session; import org.hibernate.annotations.DynamicUpdate; import org.hibernate.annotations.OptimisticLockType; @@ -8,9 +8,9 @@ import org.hibernate.annotations.SelectBeforeUpdate; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; /** * OptimisticLockingOneRootDirtyVersioningTest - Test to check optimistic checking on a single entity being updated by many threads diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingOneRootDirtyVersioningTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingOneRootDirtyVersioningTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingOneRootDirtyVersioningTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingOneRootDirtyVersioningTest.java index 6b626bb1e..d4b1079d3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingOneRootDirtyVersioningTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingOneRootDirtyVersioningTest.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.Session; import org.hibernate.StaleObjectStateException; import org.hibernate.annotations.DynamicUpdate; @@ -8,9 +8,9 @@ import org.hibernate.annotations.OptimisticLocking; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.util.concurrent.CountDownLatch; /** diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingOneRootEntityMultipleVersionsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingOneRootEntityMultipleVersionsTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingOneRootEntityMultipleVersionsTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingOneRootEntityMultipleVersionsTest.java index f5274f662..d8c4179b0 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingOneRootEntityMultipleVersionsTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingOneRootEntityMultipleVersionsTest.java @@ -1,7 +1,7 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.transaction.VoidCallable; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.transaction.VoidCallable; import org.hamcrest.core.IsInstanceOf; import org.junit.Before; import org.junit.Rule; @@ -9,7 +9,7 @@ import org.junit.internal.matchers.ThrowableCauseMatcher; import org.junit.rules.ExpectedException; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.LinkedList; import java.util.List; import java.util.concurrent.*; @@ -201,7 +201,7 @@ public static class PostViews { private long views; @Version - private int version; + private short version; public Long getId() { return id; @@ -238,7 +238,7 @@ public static class PostLikes { private int likes; @Version - private int version; + private short version; public Long getId() { return id; @@ -276,7 +276,7 @@ public static class Post { private PostLikes likes; @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingOneRootOneVersionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingOneRootOneVersionTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingOneRootOneVersionTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingOneRootOneVersionTest.java index 6bf2957c1..e4b4b5503 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingOneRootOneVersionTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingOneRootOneVersionTest.java @@ -1,10 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.StaleObjectStateException; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.concurrent.CountDownLatch; /** @@ -126,7 +126,7 @@ public static class Post { private int likes; @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingRepeatableReadTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingRepeatableReadTest.java similarity index 94% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingRepeatableReadTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingRepeatableReadTest.java index d034de299..cb721483e 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/concurrency/OptimisticLockingRepeatableReadTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/OptimisticLockingRepeatableReadTest.java @@ -1,16 +1,16 @@ -package com.vladmihalcea.book.hpjp.hibernate.concurrency; +package com.vladmihalcea.hpjp.hibernate.concurrency; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.hibernate.Session; import org.hibernate.StaleObjectStateException; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.persistence.Version; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; import javax.sql.DataSource; import java.sql.Connection; import java.util.concurrent.CountDownLatch; @@ -136,7 +136,7 @@ public static class Post { private int likes; @Version - private int version; + private short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/SkipLockJobQueueTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/SkipLockJobQueueTest.java new file mode 100644 index 000000000..34142de82 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/SkipLockJobQueueTest.java @@ -0,0 +1,261 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.query.Query; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; + +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class SkipLockJobQueueTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + public void afterInit() { + doInJPA(entityManager -> { + for (long i = 1; i <= 10; i++) { + Post post = new Post(); + post.setId(i); + post.setTitle("High-Performance Java Persistence"); + post.setBody(String.format("Chapter %d summary", i)); + post.setStatus(PostStatus.PENDING); + entityManager.persist(post); + } + }); + } + + @Test + public void testLockContention() { + LOGGER.info("Test lock contention"); + + final int postCount = 2; + + doInJPA(entityManager -> { + assertEquals( + postCount, + getAndLockPosts( + entityManager, + PostStatus.PENDING, + postCount + ).size() + ); + + try { + executeSync(() -> { + doInJPA(_entityManager -> { + assertEquals( + postCount, + getAndLockPosts( + _entityManager, + PostStatus.PENDING, + postCount + ).size() + ); + }); + }); + } catch (Exception e) { + assertTrue(ExceptionUtil.rootCause(e).getMessage().contains("could not obtain lock on row in relation \"post\"")); + } + }); + } + + public List getAndLockPosts( + EntityManager entityManager, + PostStatus status, + int postCount) { + return entityManager.createQuery(""" + select p + from Post p + where p.status = :status + order by p.id + """, Post.class) + .setParameter("status", status) + .setMaxResults(postCount) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .setHint( + "jakarta.persistence.lock.timeout", + LockOptions.NO_WAIT + ) + .getResultList(); + } + + @Test + public void testSkipLocked() { + LOGGER.info("Test lock contention"); + + final int postCount = 2; + + doInJPA(entityManager -> { + LOGGER.debug("Alice wants to moderate {} Post(s)", postCount); + List pendingPosts = getAndLockPostsWithSkipLocked(entityManager, PostStatus.PENDING, postCount); + List ids = pendingPosts.stream().map(Post::getId).toList(); + assertTrue(ids.size() == 2 && ids.contains(1L) && ids.contains(2L)); + + executeSync(() -> { + doInJPA(_entityManager -> { + LOGGER.debug("Bob wants to moderate {} Post(s)", postCount); + List _pendingPosts = getAndLockPostsWithSkipLocked(_entityManager, PostStatus.PENDING, postCount); + List _ids = _pendingPosts.stream().map(Post::getId).toList(); + assertTrue(_ids.size() == 2 && _ids.contains(3L) && _ids.contains(4L)); + }); + }); + }); + } + + @Test + public void testAliceLocksAll() { + LOGGER.info("Test lock contention"); + doInJPA(entityManager -> { + List pendingPosts = getAndLockPostsWithSkipLocked(entityManager, PostStatus.PENDING, 10); + assertTrue(pendingPosts.size() == 10); + + executeSync(() -> { + doInJPA(_entityManager -> { + List _pendingPosts = getAndLockPostsWithSkipLocked(_entityManager, PostStatus.PENDING, 2); + assertTrue(_pendingPosts.size() == 0); + }); + }); + }); + } + + @Test + public void testSkipLockedMaxCountLessThanLockCount() { + LOGGER.info("Test lock contention"); + doInJPA(entityManager -> { + List pendingPosts = getAndLockPostsWithSkipLocked(entityManager, PostStatus.PENDING, 11); + assertEquals(10, pendingPosts.size()); + }); + } + + public List getAndLockPostsWithSkipLocked( + EntityManager entityManager, + PostStatus status, + int postCount) { + return entityManager.createQuery(""" + select p + from Post p + where p.status = :status + order by p.id + """, Post.class) + .setParameter("status", status) + .setMaxResults(postCount) + .unwrap(Query.class) + .setHibernateLockMode(LockMode.UPGRADE_SKIPLOCKED) + .getResultList(); + } + + private List getAndLockPostsWithSkipLockedOracle( + EntityManager entityManager, + int lockCount, + int maxResults, + Integer maxCount) { + LOGGER.debug("Attempting to lock {} Post(s) entities", maxResults); + List posts= entityManager.createQuery(""" + select p + from Post p + where p.status = :status + order by p.id + """, Post.class) + .setParameter("status", PostStatus.PENDING) + .setMaxResults(maxResults) + .unwrap(org.hibernate.query.Query.class) + //Legacy hack - UPGRADE_SKIPLOCKED bypasses follow-on-locking + //.setLockOptions(new LockOptions(LockMode.UPGRADE_SKIPLOCKED)) + .setLockOptions(new LockOptions(LockMode.PESSIMISTIC_WRITE) + .setTimeOut(LockOptions.SKIP_LOCKED) + //This is not really needed for this query but shows that you can control the follow-on locking mechanism + .setFollowOnLocking(false) + ) + .list(); + + if(posts.isEmpty()) { + if(maxCount == null) { + maxCount = pendingPostCount(entityManager); + } + if(maxResults < maxCount || maxResults == lockCount) { + maxResults += lockCount; + return getAndLockPostsWithSkipLockedOracle(entityManager, lockCount, maxResults, maxCount); + } + } + LOGGER.debug("{} Post(s) entities have been locked", posts.size()); + return posts; + } + + private int pendingPostCount(EntityManager entityManager) { + int postCount = ((Number) entityManager.createQuery( + "select count(*) from Post where status = :status") + .setParameter("status", PostStatus.PENDING) + .getSingleResult()).intValue(); + + LOGGER.debug("There are {} PENDING Post(s)", postCount); + return postCount; + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + private String body; + + @Enumerated + private PostStatus status; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public PostStatus getStatus() { + return status; + } + + public void setStatus(PostStatus status) { + this.status = status; + } + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/VersionWrapperOptimisticLockingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/VersionWrapperOptimisticLockingTest.java new file mode 100644 index 000000000..b4fdd34c7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/VersionWrapperOptimisticLockingTest.java @@ -0,0 +1,104 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.StaleStateException; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class VersionWrapperOptimisticLockingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testOptimisticLocking() { + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + entityManager.flush(); + post.setTitle("High-Performance Hibernate"); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, post.getVersion()); + }); + } + + @Test + public void testStaleStateException() { + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + }); + + try { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + executeSync(() -> { + doInJPA(_entityManager -> { + Post _post = _entityManager.find(Post.class, 1L); + _post.setTitle("High-Performance JDBC"); + }); + }); + + post.setTitle("High-Performance Hibernate"); + }); + } catch (Exception expected) { + LOGGER.error("Throws", expected); + assertEquals(OptimisticLockException.class, expected.getCause().getClass()); + assertTrue(StaleStateException.class.isAssignableFrom(expected.getCause().getCause().getClass())); + } + } + + @Entity(name = "Post") @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private Short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getVersion() { + return version; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/ACIDRaceConditionIsolationLevelTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/ACIDRaceConditionIsolationLevelTest.java new file mode 100644 index 000000000..39bdba2ee --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/ACIDRaceConditionIsolationLevelTest.java @@ -0,0 +1,236 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.acid; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.transaction.ConnectionCallable; +import com.vladmihalcea.hpjp.util.transaction.ConnectionVoidCallable; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.concurrent.CountDownLatch; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ACIDRaceConditionIsolationLevelTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Account.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected boolean connectionPooling() { + return true; + } + + @Override + protected int connectionPoolSize() { + return threadCount(); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Account from = new Account(); + from.setId("Alice-123"); + from.setOwner("Alice"); + from.setBalance(10); + + entityManager.persist(from); + + Account to = new Account(); + to.setId("Bob-456"); + to.setOwner("Bob"); + to.setBalance(0L); + + entityManager.persist(to); + }); + } + + private int threadCount() { + return 16; + } + + @Test + public void testParallelExecution() { + assertEquals(10L, getAccountBalance("Alice-123")); + assertEquals(0L, getAccountBalance("Bob-456")); + + int threadCount = threadCount(); + + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch endLatch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + new Thread(() -> { + try { + doInJDBC(connection -> { + preventLostUpdate(connection, false); + + awaitOnLatch(startLatch); + + transfer(connection, "Alice-123", "Bob-456", 5L); + }); + } catch (Exception e) { + LOGGER.error("Transfer failure", e); + } + + endLatch.countDown(); + }).start(); + } + LOGGER.info("Starting threads"); + startLatch.countDown(); + awaitOnLatch(endLatch); + + LOGGER.info("Alice's balance: {}", getAccountBalance("Alice-123")); + LOGGER.info("Bob's balance: {}", getAccountBalance("Bob-456")); + } + + private void preventLostUpdate(Connection connection, boolean prevent) throws SQLException { + if(prevent) { + Database database = database(); + if (database == Database.POSTGRESQL || database == Database.SQLSERVER) { + connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); + } else if (database == Database.MYSQL || database == Database.ORACLE) { + connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); + } + } + printIsolationLevel(connection); + } + + public static void transfer( + Connection connection, + String sourceAccount, + String destinationAccount, + long amount) { + + if(getAccountBalance(connection, sourceAccount) >= amount) { + addToAccountBalance(connection, sourceAccount, (-1) * amount); + + addToAccountBalance(connection, destinationAccount, amount); + } + } + + private static long getAccountBalance(Connection connection, final String id) { + try(PreparedStatement statement = connection.prepareStatement(""" + SELECT balance + FROM account + WHERE id = ? + """) + ) { + statement.setString(1, id); + ResultSet resultSet = statement.executeQuery(); + if(resultSet.next()) { + return resultSet.getLong(1); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + throw new IllegalArgumentException("Can't find account with id: " + id); + } + + private static void addToAccountBalance(Connection connection, final String id, long amount) { + try(PreparedStatement statement = connection.prepareStatement(""" + UPDATE account + SET balance = balance + ? + WHERE id = ? + """) + ) { + statement.setLong(1, amount); + statement.setString(2, id); + + statement.executeUpdate(); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + private long getAccountBalance(final String id) { + return doInJDBC(connection -> { + return getAccountBalance(connection, id); + }); + } + + protected void doInJDBC(ConnectionVoidCallable callable) { + try { + Connection connection = null; + try { + connection = dataSource().getConnection(); + connection.setAutoCommit(false); + callable.execute(connection); + connection.commit(); + } catch (SQLException e) { + if(connection != null) { + connection.rollback(); + } + throw e; + } finally { + if(connection != null) { + connection.close(); + } + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + protected T doInJDBC(ConnectionCallable callable) { + try { + Connection connection = null; + try { + connection = dataSource().getConnection(); + connection.setAutoCommit(false); + T result = callable.execute(connection); + connection.commit(); + return result; + } catch (SQLException e) { + if(connection != null) { + connection.rollback(); + } + throw e; + } finally { + if(connection != null) { + connection.close(); + } + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + private void printIsolationLevel(Connection connection) throws SQLException { + int isolationLevelIntegerValue = connection.getTransactionIsolation(); + + String isolationLevelStringValue = null; + + switch (isolationLevelIntegerValue) { + case Connection.TRANSACTION_READ_UNCOMMITTED: + isolationLevelStringValue = "READ_UNCOMMITTED"; + break; + case Connection.TRANSACTION_READ_COMMITTED: + isolationLevelStringValue = "READ_COMMITTED"; + break; + case Connection.TRANSACTION_REPEATABLE_READ: + isolationLevelStringValue = "REPEATABLE_READ"; + break; + case Connection.TRANSACTION_SERIALIZABLE: + isolationLevelStringValue = "SERIALIZABLE"; + break; + } + + LOGGER.info("Transaction isolation level: {}", isolationLevelStringValue); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/ACIDRaceConditionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/ACIDRaceConditionTest.java new file mode 100644 index 000000000..735a2aaa2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/ACIDRaceConditionTest.java @@ -0,0 +1,208 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.acid; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.transaction.ConnectionCallable; +import com.vladmihalcea.hpjp.util.transaction.ConnectionVoidCallable; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.concurrent.CountDownLatch; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ACIDRaceConditionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Account.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected boolean connectionPooling() { + return true; + } + + @Override + protected int connectionPoolSize() { + return threadCount(); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Account from = new Account(); + from.setId("Alice-123"); + from.setOwner("Alice"); + from.setBalance(10); + + entityManager.persist(from); + + Account to = new Account(); + to.setId("Bob-456"); + to.setOwner("Bob"); + to.setBalance(0L); + + entityManager.persist(to); + }); + } + + public void transfer( + String sourceAccount, + String destinationAccount, + long amount) { + + if(getAccountBalance(sourceAccount) >= amount) { + addToAccountBalance(sourceAccount, (-1) * amount); + + addToAccountBalance(destinationAccount, amount); + } + } + + private long getAccountBalance(final String id) { + return doInJDBC(connection -> { + try(PreparedStatement statement = connection.prepareStatement(""" + SELECT balance + FROM account + WHERE id = ? + """) + ) { + statement.setString(1, id); + ResultSet resultSet = statement.executeQuery(); + if(resultSet.next()) { + return resultSet.getLong(1); + } + } + throw new IllegalArgumentException("Can't find account with id: " + id); + }); + } + + private void addToAccountBalance(final String id, long amount) { + doInJDBC(connection -> { + try(PreparedStatement statement = connection.prepareStatement(""" + UPDATE account + SET balance = balance + ? + WHERE id = ? + """) + ) { + statement.setLong(1, amount); + statement.setString(2, id); + + statement.executeUpdate(); + } + }); + } + + protected void doInJDBC(ConnectionVoidCallable callable) { + try { + Connection connection = null; + try { + connection = dataSource().getConnection(); + connection.setAutoCommit(false); + callable.execute(connection); + connection.commit(); + } catch (SQLException e) { + if(connection != null) { + connection.rollback(); + } + throw e; + } finally { + if(connection != null) { + connection.close(); + } + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + protected T doInJDBC(ConnectionCallable callable) { + try { + Connection connection = null; + try { + connection = dataSource().getConnection(); + connection.setAutoCommit(false); + T result = callable.execute(connection); + connection.commit(); + return result; + } catch (SQLException e) { + if(connection != null) { + connection.rollback(); + } + throw e; + } finally { + if(connection != null) { + connection.close(); + } + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + @Test + public void testSerialExecution() { + assertEquals(10L, getAccountBalance("Alice-123")); + assertEquals(0L, getAccountBalance("Bob-456")); + + transfer("Alice-123", "Bob-456", 5L); + + assertEquals(5L, getAccountBalance("Alice-123")); + assertEquals(5L, getAccountBalance("Bob-456")); + + transfer("Alice-123", "Bob-456", 5L); + + assertEquals(0L, getAccountBalance("Alice-123")); + assertEquals(10L, getAccountBalance("Bob-456")); + + transfer("Alice-123", "Bob-456", 5L); + + assertEquals(0L, getAccountBalance("Alice-123")); + assertEquals(10L, getAccountBalance("Bob-456")); + } + + private int threadCount() { + return 16; + } + + @Test + public void testParallelExecution() { + assertEquals(10L, getAccountBalance("Alice-123")); + assertEquals(0L, getAccountBalance("Bob-456")); + + int threadCount = threadCount(); + + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch endLatch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + new Thread(() -> { + awaitOnLatch(startLatch); + + transfer("Alice-123", "Bob-456", 5L); + + endLatch.countDown(); + }).start(); + } + LOGGER.info("Starting threads"); + startLatch.countDown(); + awaitOnLatch(endLatch); + + LOGGER.info("Alice's balance: {}", getAccountBalance("Alice-123")); + LOGGER.info("Bob's balance: {}", getAccountBalance("Bob-456")); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/Account.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/Account.java new file mode 100644 index 000000000..61a89a72e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/Account.java @@ -0,0 +1,44 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.acid; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Account") +@Table(name = "account") +public class Account { + + @Id + private String id; + + private String owner; + + private long balance; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public long getAccountBalance() { + return balance; + } + + public void setBalance(long balance) { + this.balance = balance; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/check/ACIDRaceConditionPositiveBalanceCheckFirstOperationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/check/ACIDRaceConditionPositiveBalanceCheckFirstOperationTest.java new file mode 100644 index 000000000..90e41c517 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/check/ACIDRaceConditionPositiveBalanceCheckFirstOperationTest.java @@ -0,0 +1,156 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.acid.check; + +import com.vladmihalcea.hpjp.hibernate.concurrency.acid.Account; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.concurrent.CountDownLatch; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ACIDRaceConditionPositiveBalanceCheckFirstOperationTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Account.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + executeStatement(""" + ALTER TABLE account + ADD CONSTRAINT account_balance_check + CHECK (balance >= 0) + """ + ); + + doInJPA(entityManager -> { + Account from = new Account(); + from.setId("Alice-123"); + from.setOwner("Alice"); + from.setBalance(10); + + entityManager.persist(from); + + Account to = new Account(); + to.setId("Bob-456"); + to.setOwner("Bob"); + to.setBalance(0L); + + entityManager.persist(to); + }); + } + + @Test + public void testSerialExecution() { + assertEquals(10L, getAccountBalance("Alice-123")); + assertEquals(0L, getAccountBalance("Bob-456")); + + transfer("Alice-123", "Bob-456", 5L); + + assertEquals(5L, getAccountBalance("Alice-123")); + assertEquals(5L, getAccountBalance("Bob-456")); + + transfer("Alice-123", "Bob-456", 5L); + + assertEquals(0L, getAccountBalance("Alice-123")); + assertEquals(10L, getAccountBalance("Bob-456")); + + transfer("Alice-123", "Bob-456", 5L); + + assertEquals(0L, getAccountBalance("Alice-123")); + assertEquals(10L, getAccountBalance("Bob-456")); + } + + int threadCount = 8; + + @Test + public void testParallelExecution() { + assertEquals(10L, getAccountBalance("Alice-123")); + assertEquals(0L, getAccountBalance("Bob-456")); + + parallelExecution(); + + LOGGER.info("Alice's balance {}", getAccountBalance("Alice-123")); + LOGGER.info("Bob's balance {}", getAccountBalance("Bob-456")); + } + + public void parallelExecution() { + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch endLatch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + new Thread(() -> { + awaitOnLatch(startLatch); + try { + transfer("Alice-123", "Bob-456", 5L); + } finally { + endLatch.countDown(); + } + }).start(); + } + + startLatch.countDown(); + awaitOnLatch(endLatch); + } + + public void transfer(String fromIban, String toIban, long transferCents) { + long fromBalance = getAccountBalance(fromIban); + + if(fromBalance >= transferCents) { + addToAccountBalance(fromIban, (-1) * transferCents); + + addToAccountBalance(toIban, transferCents); + } + } + + private long getAccountBalance(final String id) { + return doInJDBC(connection -> { + try(PreparedStatement statement = connection.prepareStatement(""" + SELECT balance + FROM account + WHERE id = ? + """) + ) { + statement.setString(1, id); + ResultSet resultSet = statement.executeQuery(); + if(resultSet.next()) { + return resultSet.getLong(1); + } + } + throw new IllegalArgumentException("Can't find account with id: " + id); + }); + } + + private void addToAccountBalance(final String id, long amount) { + doInJDBC(connection -> { + try(PreparedStatement statement = connection.prepareStatement(""" + UPDATE account + SET balance = balance + ? + WHERE id = ? + """) + ) { + statement.setLong(1, amount); + statement.setString(2, id); + + statement.executeUpdate(); + } + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/check/ACIDRaceConditionPositiveBalanceCheckLastOperationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/check/ACIDRaceConditionPositiveBalanceCheckLastOperationTest.java new file mode 100644 index 000000000..5b4d6efa7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/acid/check/ACIDRaceConditionPositiveBalanceCheckLastOperationTest.java @@ -0,0 +1,153 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.acid.check; + +import com.vladmihalcea.hpjp.hibernate.concurrency.acid.Account; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.concurrent.CountDownLatch; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ACIDRaceConditionPositiveBalanceCheckLastOperationTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Account.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + executeStatement(""" + ALTER TABLE account + ADD CONSTRAINT account_balance_check + CHECK (balance >= 0) + """ + ); + + doInJPA(entityManager -> { + Account from = new Account(); + from.setId("Alice-123"); + from.setOwner("Alice"); + from.setBalance(10); + + entityManager.persist(from); + + Account to = new Account(); + to.setId("Bob-456"); + to.setOwner("Bob"); + to.setBalance(0L); + + entityManager.persist(to); + }); + } + + @Test + public void testSerialExecution() { + assertEquals(10L, getAccountBalance("Alice-123")); + assertEquals(0L, getAccountBalance("Bob-456")); + + transfer("Alice-123", "Bob-456", 5L); + + assertEquals(5L, getAccountBalance("Alice-123")); + assertEquals(5L, getAccountBalance("Bob-456")); + + transfer("Alice-123", "Bob-456", 5L); + + assertEquals(0L, getAccountBalance("Alice-123")); + assertEquals(10L, getAccountBalance("Bob-456")); + + transfer("Alice-123", "Bob-456", 5L); + + assertEquals(0L, getAccountBalance("Alice-123")); + assertEquals(10L, getAccountBalance("Bob-456")); + } + + int threadCount = 8; + + @Test + public void testParallelExecution() { + assertEquals(10L, getAccountBalance("Alice-123")); + assertEquals(0L, getAccountBalance("Bob-456")); + + parallelExecution(); + + LOGGER.info("Alice's balance {}", getAccountBalance("Alice-123")); + LOGGER.info("Bob's balance {}", getAccountBalance("Bob-456")); + } + + public void parallelExecution() { + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch endLatch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + new Thread(() -> { + awaitOnLatch(startLatch); + try { + transfer("Alice-123", "Bob-456", 5L); + } finally { + endLatch.countDown(); + } + }).start(); + } + + startLatch.countDown(); + awaitOnLatch(endLatch); + } + + public void transfer(String fromIban, String toIban, long transferCents) { + long fromBalance = getAccountBalance(fromIban); + + if(fromBalance >= transferCents) { + addToAccountBalance(toIban, transferCents); + + addToAccountBalance(fromIban, (-1) * transferCents); + } + } + + private long getAccountBalance(final String id) { + return doInJDBC(connection -> { + try(PreparedStatement statement = connection.prepareStatement(""" + SELECT balance + FROM account + WHERE id = ? + """) + ) { + statement.setString(1, id); + ResultSet resultSet = statement.executeQuery(); + if(resultSet.next()) { + return resultSet.getLong(1); + } + } + throw new IllegalArgumentException("Can't find account with id: " + id); + }); + } + + private void addToAccountBalance(final String id, long amount) { + doInJDBC(connection -> { + try(PreparedStatement statement = connection.prepareStatement(""" + UPDATE account + SET balance = balance + ? + WHERE id = ? + """) + ) { + statement.setLong(1, amount); + statement.setString(2, id); + + statement.executeUpdate(); + } + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/DeadLockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/DeadLockTest.java new file mode 100644 index 000000000..1ee82163c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/DeadLockTest.java @@ -0,0 +1,139 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.deadlock; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.testing.util.ExceptionUtil; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class DeadLockTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + PostComment comment = new PostComment(); + comment.setId(1L); + comment.setReview("Awesome!"); + comment.setPost(post); + entityManager.persist(comment); + }); + } + + @Test + public void testDeadLock() { + CountDownLatch bobStart = new CountDownLatch(1); + try { + doInJPA(entityManager -> { + LOGGER.info("Alice locks the Post entity"); + Post post = entityManager.find(Post.class, 1L, LockModeType.PESSIMISTIC_WRITE); + + Future future = executeAsync(() -> { + doInJPA(_entityManager -> { + LOGGER.info("Bob locks the PostComment entity"); + PostComment _comment = _entityManager.find(PostComment.class, 1L, LockModeType.PESSIMISTIC_WRITE); + bobStart.countDown(); + LOGGER.info("Bob wants to lock the Post entity"); + Post _post = _entityManager.find(Post.class, 1L, LockModeType.PESSIMISTIC_WRITE); + }); + }); + + awaitOnLatch(bobStart); + LOGGER.info("Alice wants to lock the PostComment entity"); + PostComment comment = entityManager.find(PostComment.class, 1L, LockModeType.PESSIMISTIC_WRITE); + + try { + future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } catch (Exception e) { + LOGGER.info("Deadlock detected", ExceptionUtil.rootCause(e)); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/SQLServerDeadLockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/SQLServerDeadLockTest.java new file mode 100644 index 000000000..db9e22894 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/SQLServerDeadLockTest.java @@ -0,0 +1,304 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.deadlock; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import io.hypersistence.utils.common.StringUtils; +import io.hypersistence.utils.hibernate.query.ListResultTransformer; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.testing.util.ExceptionUtil; +import org.junit.Test; + +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.stream.Collectors; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerDeadLockTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class + }; + } + + @Override + protected Database database() { + return Database.SQLSERVER; + } + + @Override + public void afterInit() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + PostDetails details = new PostDetails(); + details.setId(1L); + details.setPost(post); + entityManager.persist(details); + }); + executeStatement("ALTER DATABASE [high_performance_java_persistence] SET READ_COMMITTED_SNAPSHOT ON"); + } + + @Override + public void destroy() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + executeStatement("ALTER DATABASE [high_performance_java_persistence] SET READ_COMMITTED_SNAPSHOT OFF"); + super.destroy(); + } + + /** + * For SQL Server, enable the following trace options: + * + * DBCC TRACEON (1204, 1222, -1) + * + * And, read the error log using the following SP: + * + * sp_readerrorlog + */ + @Test + public void testDeadLock_1204() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + LOGGER.info("Check flag: 1204"); + List beforeDeadLockErrorLogLines = errorLogMessages(); + + try { + CountDownLatch bobStart = new CountDownLatch(1); + + executeStatement("DBCC TRACEON (1204, -1)"); + + doInJPA(entityManager -> { + LOGGER.info("Alice updates the PostDetails entity"); + PostDetails details = entityManager.find(PostDetails.class, 1L); + details.setUpdatedBy("Alice"); + entityManager.flush(); + + Future future = executeAsync(() -> { + doInJPA(_entityManager -> { + LOGGER.info("Bob updates the Post entity"); + Post _post = _entityManager.find(Post.class, 1L); + _post.setTitle("ACID"); + _entityManager.flush(); + + bobStart.countDown(); + LOGGER.info("Bob wants to update the PostDetails entity"); + PostDetails _details = _entityManager.find(PostDetails.class, 1L); + _details.setUpdatedBy("Bob"); + _entityManager.flush(); + }); + }); + + awaitOnLatch(bobStart); + LOGGER.info("Alice wants to update the Post entity"); + Post post = entityManager.find(Post.class, 1L); + post.setTitle("BASE"); + entityManager.flush(); + + try { + future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } catch (Exception e) { + LOGGER.info("Deadlock detected", ExceptionUtil.rootCause(e)); + List afterDeadLockErrorLogLines = errorLogMessages(); + afterDeadLockErrorLogLines.removeAll(beforeDeadLockErrorLogLines); + + LOGGER.info( + "Deadlock trace info for flag 1204: {}", + afterDeadLockErrorLogLines.stream().map(ErrorLogMessage::getMessage).collect(Collectors.joining(StringUtils.LINE_SEPARATOR)) + ); + } finally { + executeStatement("DBCC TRACEOFF (1204, -1)"); + } + } + + @Test + public void testDeadLock_1222() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + LOGGER.info("Check flag: 1222"); + List beforeDeadLockErrorLogLines = errorLogMessages(); + + try { + CountDownLatch bobStart = new CountDownLatch(1); + + executeStatement("DBCC TRACEON (1222, -1)"); + + doInJPA(entityManager -> { + LOGGER.info("Alice updates the PostDetails entity"); + PostDetails details = entityManager.find(PostDetails.class, 1L); + details.setUpdatedBy("Alice"); + entityManager.flush(); + + Future future = executeAsync(() -> { + doInJPA(_entityManager -> { + LOGGER.info("Bob updates the Post entity"); + Post _post = _entityManager.find(Post.class, 1L); + _post.setTitle("ACID"); + _entityManager.flush(); + + bobStart.countDown(); + LOGGER.info("Bob wants to update the PostDetails entity"); + PostDetails _details = _entityManager.find(PostDetails.class, 1L); + _details.setUpdatedBy("Bob"); + _entityManager.flush(); + }); + }); + + awaitOnLatch(bobStart); + LOGGER.info("Alice wants to update the Post entity"); + Post post = entityManager.find(Post.class, 1L); + post.setTitle("BASE"); + entityManager.flush(); + + try { + future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } catch (Exception e) { + LOGGER.info("Deadlock detected", ExceptionUtil.rootCause(e)); + List afterDeadLockErrorLogLines = errorLogMessages(); + afterDeadLockErrorLogLines.removeAll(beforeDeadLockErrorLogLines); + + LOGGER.info( + "Deadlock trace info for flag 1222: {}", + afterDeadLockErrorLogLines.stream().map(ErrorLogMessage::getMessage).collect(Collectors.joining(StringUtils.LINE_SEPARATOR)) + ); + } finally { + executeStatement("DBCC TRACEOFF (1222, -1)"); + } + } + + private List errorLogMessages() { + try(Session session = entityManagerFactory().createEntityManager().unwrap(Session.class)) { + return session + .createNativeQuery("sp_readerrorlog") + .setResultTransformer((ListResultTransformer) (tuple, aliases) -> new ErrorLogMessage((Date) tuple[0], (String) tuple[2])) + .getResultList(); + } + } + + private static class ErrorLogMessage { + private final Date timestamp; + private final String message; + + public ErrorLogMessage(Date timestamp, String message) { + this.timestamp = timestamp; + this.message = message; + } + + public Date getTimestamp() { + return timestamp; + } + + public String getMessage() { + return message; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ErrorLogMessage)) return false; + ErrorLogMessage that = (ErrorLogMessage) o; + return Objects.equals(timestamp, that.timestamp) && Objects.equals(message, that.message); + } + + @Override + public int hashCode() { + return Objects.hash(timestamp, message); + } + } + + @Entity(name = "Post") + @Table(name = "post") + @DynamicUpdate + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + @DynamicUpdate + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "updated_by") + private String updatedBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public PostDetails setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/MySQLFKNoParentLockRRTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/MySQLFKNoParentLockRRTest.java new file mode 100644 index 000000000..c30f7e354 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/MySQLFKNoParentLockRRTest.java @@ -0,0 +1,168 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.deadlock.fk; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Connection; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; + +/** + * @author Vlad Mihalcea + */ +public class MySQLFKNoParentLockRRTest extends AbstractTest { + + private final int ISOLATION_LEVEL = Connection.TRANSACTION_REPEATABLE_READ; + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + long postId = 1; + long commentId = 1; + for (long i = 0; i < 1000; i++) { + Post post = new Post(); + post.setId(postId++); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + for (long j = 0; j < 10; j++) { + PostComment comment = new PostComment(); + comment.setId(commentId++); + comment.setReview("Awesome!"); + comment.setPost(post); + + entityManager.persist(comment); + } + + if(i > 0 && i % 100 == 0) { + entityManager.flush(); + } + } + }); + } + + @Test + public void test() { + CountDownLatch bobStart = new CountDownLatch(1); + doInJPA(entityManager -> { + prepareConnection(entityManager); + + LOGGER.info("Alice updates the Post entity"); + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Java Persistence 2nd edition"); + entityManager.flush(); + + Future future = executeAsync(() -> { + doInJPA(_entityManager -> { + prepareConnection(_entityManager); + + LOGGER.info("Bob updates the PostComment entity"); + PostComment _comment = _entityManager.find(PostComment.class, 1L); + _comment.setReview("Great!"); + bobStart.countDown(); + _entityManager.flush(); + }); + }); + + awaitOnLatch(bobStart); + + try { + future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + protected void prepareConnection(EntityManager entityManager) { + entityManager.unwrap(Session.class).doWork(connection -> { + connection.setTransactionIsolation(ISOLATION_LEVEL); + setJdbcTimeout(connection); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table( + name = "post_comment", + indexes = @Index( + name = "FK_post_comment_post_id", + columnList = "post_id" + ) + ) + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/MySQLFKNoParentLockSerializableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/MySQLFKNoParentLockSerializableTest.java new file mode 100644 index 000000000..a30c2a794 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/MySQLFKNoParentLockSerializableTest.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.deadlock.fk; + +import org.hibernate.Session; + +import jakarta.persistence.EntityManager; +import java.sql.Connection; + +/** + * @author Vlad Mihalcea + */ +public class MySQLFKNoParentLockSerializableTest extends MySQLFKNoParentLockRRTest { + + protected void prepareConnection(EntityManager entityManager) { + entityManager.unwrap(Session.class).doWork(connection -> { + connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); + setJdbcTimeout(connection); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/OracleNoFKNoParentLockSerializableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/OracleNoFKNoParentLockSerializableTest.java new file mode 100644 index 000000000..4f9752abb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/OracleNoFKNoParentLockSerializableTest.java @@ -0,0 +1,168 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.deadlock.fk; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Connection; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; + +/** + * @author Vlad Mihalcea + */ +public class OracleNoFKNoParentLockSerializableTest extends AbstractTest { + + private final int ISOLATION_LEVEL = Connection.TRANSACTION_SERIALIZABLE; + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.ORACLE; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + long postId = 1; + long commentId = 1; + for (long i = 0; i < 1000; i++) { + Post post = new Post(); + post.setId(postId++); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + for (long j = 0; j < 10; j++) { + PostComment comment = new PostComment(); + comment.setId(commentId++); + comment.setReview("Awesome!"); + comment.setPost(post); + + entityManager.persist(comment); + } + + if(i > 0 && i % 100 == 0) { + entityManager.flush(); + } + } + }); + } + + @Test + public void test() { + CountDownLatch bobStart = new CountDownLatch(1); + doInJPA(entityManager -> { + prepareConnection(entityManager); + + LOGGER.info("Alice updates the Post entity"); + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Java Persistence 2nd edition"); + entityManager.flush(); + + Future future = executeAsync(() -> { + doInJPA(_entityManager -> { + prepareConnection(_entityManager); + + LOGGER.info("Bob updates the PostComment entity"); + PostComment _comment = _entityManager.find(PostComment.class, 1L); + _comment.setReview("Great!"); + bobStart.countDown(); + _entityManager.flush(); + }); + }); + + awaitOnLatch(bobStart); + + try { + future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + protected void prepareConnection(EntityManager entityManager) { + entityManager.unwrap(Session.class).doWork(connection -> { + connection.setTransactionIsolation(ISOLATION_LEVEL); + setJdbcTimeout(connection); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table( + name = "post_comment", + indexes = @Index( + name = "FK_post_comment_post_id", + columnList = "post_id" + ) + ) + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/PostgreSQLNoFKNoParentLockRRTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/PostgreSQLNoFKNoParentLockRRTest.java new file mode 100644 index 000000000..000e9355d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/PostgreSQLNoFKNoParentLockRRTest.java @@ -0,0 +1,168 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.deadlock.fk; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Connection; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLNoFKNoParentLockRRTest extends AbstractTest { + + private final int ISOLATION_LEVEL = Connection.TRANSACTION_REPEATABLE_READ; + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + long postId = 1; + long commentId = 1; + for (long i = 0; i < 1000; i++) { + Post post = new Post(); + post.setId(postId++); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + for (long j = 0; j < 10; j++) { + PostComment comment = new PostComment(); + comment.setId(commentId++); + comment.setReview("Awesome!"); + comment.setPost(post); + + entityManager.persist(comment); + } + + if(i > 0 && i % 100 == 0) { + entityManager.flush(); + } + } + }); + } + + @Test + public void test() { + CountDownLatch bobStart = new CountDownLatch(1); + doInJPA(entityManager -> { + prepareConnection(entityManager); + + LOGGER.info("Alice updates the Post entity"); + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Java Persistence 2nd edition"); + entityManager.flush(); + + Future future = executeAsync(() -> { + doInJPA(_entityManager -> { + prepareConnection(_entityManager); + + LOGGER.info("Bob updates the PostComment entity"); + PostComment _comment = _entityManager.find(PostComment.class, 1L); + _comment.setReview("Great!"); + bobStart.countDown(); + _entityManager.flush(); + }); + }); + + awaitOnLatch(bobStart); + + try { + future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + protected void prepareConnection(EntityManager entityManager) { + entityManager.unwrap(Session.class).doWork(connection -> { + connection.setTransactionIsolation(ISOLATION_LEVEL); + setJdbcTimeout(connection); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table( + name = "post_comment", + indexes = @Index( + name = "FK_post_comment_post_id", + columnList = "post_id" + ) + ) + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/SQLServerFKParentLockRCSITest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/SQLServerFKParentLockRCSITest.java new file mode 100644 index 000000000..5f6488a30 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/SQLServerFKParentLockRCSITest.java @@ -0,0 +1,273 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.deadlock.fk; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Connection; +import java.util.List; +import java.util.concurrent.*; + +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerFKParentLockRCSITest extends AbstractTest { + + private final int ISOLATION_LEVEL = Connection.TRANSACTION_READ_COMMITTED; + + public static final String LOCK_TABLE_TEMPLATE = """ + | table_name | blocking_session_id | wait_type | resource_type | request_status | request_mode | request_session_id | + |------------|---------------------|-----------|---------------|----------------|--------------|--------------------| + |%1$12s|%2$21s|%3$11s|%4$15s|%5$16s|%6$14s|%7$20s| + """; + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.SQLSERVER; + } + + @Override + public void afterInit() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJPA(entityManager -> { + long postId = 1; + long commentId = 1; + for (long i = 0; i < 1000; i++) { + Post post = new Post(); + post.setId(postId++); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + for (long j = 0; j < 10; j++) { + PostComment comment = new PostComment(); + comment.setId(commentId++); + comment.setReview("Awesome!"); + comment.setPost(post); + + entityManager.persist(comment); + } + + if(i > 0 && i % 100 == 0) { + entityManager.flush(); + } + } + }); + executeStatement("ALTER DATABASE [high_performance_java_persistence] SET READ_COMMITTED_SNAPSHOT ON"); + } + + @Override + public void destroy() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + executeStatement("ALTER DATABASE [high_performance_java_persistence] SET READ_COMMITTED_SNAPSHOT OFF"); + super.destroy(); + } + + protected final ExecutorService monitoringExecutorService = Executors.newSingleThreadExecutor(r -> { + Thread bob = new Thread(r); + bob.setName("Monitoring"); + return bob; + }); + + /* + */ + @Test + public void test() { + if (!ENABLE_LONG_RUNNING_TESTS) { + return; + } + CountDownLatch bobStart = new CountDownLatch(1); + CountDownLatch monitoringStart = new CountDownLatch(1); + try { + doInJPA(entityManager -> { + LOGGER.info( + "Alice session id: {}", + entityManager.createNativeQuery("SELECT @@SPID").getSingleResult() + ); + LOGGER.info("Alice updates the Post entity"); + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Java Persistence 2nd edition"); + entityManager.flush(); + + Future bobFuture = executeAsync(() -> { + doInJPA(_entityManager -> { + prepareConnection(_entityManager); + LOGGER.info( + "Bob session id: {}", + _entityManager.createNativeQuery("SELECT @@SPID").getSingleResult() + ); + LOGGER.info("Bob updates the PostComment entity"); + PostComment _comment = _entityManager.find(PostComment.class, 1L); + _comment.setReview("Great!"); + bobStart.countDown(); + try { + _entityManager.flush(); + } catch (Exception e) { + Exception rootException = ExceptionUtil.rootCause(e); + if(ExceptionUtil.isLockTimeout(rootException)) { + LOGGER.info("Lock timeout detected", rootException); + } + } + }); + }); + + Future monitoringFuture = monitoringExecutorService.submit(() -> { + doInJPA(_entityManager -> { + awaitOnLatch(monitoringStart); + + List lockInfo = _entityManager.createNativeQuery(""" + SELECT + table_name = schema_name(o.schema_id) + '.' + o.name, + wt.blocking_session_id, + wt.wait_type, + tm.resource_type, + tm.request_status, + tm.request_mode, + tm.request_session_id + FROM sys.dm_tran_locks AS tm + INNER JOIN sys.dm_os_waiting_tasks as wt ON tm.lock_owner_address = wt.resource_address + LEFT OUTER JOIN sys.partitions AS p ON p.hobt_id = tm.resource_associated_entity_id + LEFT OUTER JOIN sys.objects o ON o.object_id = p.object_id OR tm.resource_associated_entity_id = o.object_id + WHERE resource_database_id = DB_ID() + """, Tuple.class) + .getResultList(); + + if (!lockInfo.isEmpty()) { + Tuple result = lockInfo.get(0); + + int i = 0; + LOGGER.info( + "Lock waiting info: \n{}", + String.format( + LOCK_TABLE_TEMPLATE, + result.get(i++), + result.get(i++), + result.get(i++), + result.get(i++), + result.get(i++), + result.get(i++), + result.get(i) + ) + ); + } + }); + }); + + awaitOnLatch(bobStart); + + try { + monitoringStart.countDown(); + bobFuture.get(); + monitoringFuture.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } catch (RuntimeException e) { + Exception rootException = ExceptionUtil.rootCause(e); + if(!ExceptionUtil.isLockTimeout(rootException)) { + fail("Expected a lock timeout exception"); + } + } finally { + monitoringExecutorService.shutdownNow(); + executorService.shutdownNow(); + } + } + + protected void prepareConnection(EntityManager entityManager) { + entityManager.unwrap(Session.class).doWork(connection -> { + connection.setTransactionIsolation(ISOLATION_LEVEL); + setJdbcTimeout(connection); + }); + } + + @Entity(name = "Post") + @Table(name = "Post") + public static class Post { + + @Id + @Column(name = "PostID") + private Long id; + + @Column(name = "Title") + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table( + name = "PostComment", + indexes = @Index( + name = "FK_PostComment_PostID", + columnList = "PostID" + ) + ) + public static class PostComment { + + @Id + @Column(name = "PostCommentID") + private Long id; + + @Column(name = "Review") + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "PostID") + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/SQLServerNoFKParentLockRCSIDynamicUpdateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/SQLServerNoFKParentLockRCSIDynamicUpdateTest.java new file mode 100644 index 000000000..699d5ac42 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/deadlock/fk/SQLServerNoFKParentLockRCSIDynamicUpdateTest.java @@ -0,0 +1,209 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.deadlock.fk; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicUpdate; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Connection; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; + +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerNoFKParentLockRCSIDynamicUpdateTest extends AbstractTest { + + private final int ISOLATION_LEVEL = Connection.TRANSACTION_READ_COMMITTED; + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.SQLSERVER; + } + + @Override + public void afterInit() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJPA(entityManager -> { + long postId = 1; + long commentId = 1; + for (long i = 0; i < 1000; i++) { + Post post = new Post(); + post.setId(postId++); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + for (long j = 0; j < 10; j++) { + PostComment comment = new PostComment(); + comment.setId(commentId++); + comment.setReview("Awesome!"); + comment.setPost(post); + + entityManager.persist(comment); + } + + if(i > 0 && i % 100 == 0) { + entityManager.flush(); + } + } + }); + executeStatement("ALTER DATABASE [high_performance_java_persistence] SET READ_COMMITTED_SNAPSHOT ON"); + } + + @Override + public void destroy() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + executeStatement("ALTER DATABASE [high_performance_java_persistence] SET READ_COMMITTED_SNAPSHOT OFF"); + super.destroy(); + } + + @Test + public void test() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + CountDownLatch bobStart = new CountDownLatch(1); + try { + doInJPA(entityManager -> { + prepareConnection(entityManager); + LOGGER.info( + "Alice session id: {}", + entityManager.createNativeQuery("SELECT @@SPID").getSingleResult() + ); + LOGGER.info("Alice updates the Post entity"); + Post post = entityManager.find(Post.class, 1L); + post.setTitle("High-Performance Java Persistence 2nd edition"); + entityManager.flush(); + + Future future = executeAsync(() -> { + doInJPA(_entityManager -> { + prepareConnection(_entityManager); + LOGGER.info( + "Bob session id: {}", + _entityManager.createNativeQuery("SELECT @@SPID").getSingleResult() + ); + LOGGER.info("Bob updates the PostComment entity"); + PostComment _comment = _entityManager.find(PostComment.class, 1L); + _comment.setReview("Great!"); + bobStart.countDown(); + _entityManager.flush(); + }); + }); + + awaitOnLatch(bobStart); + + try { + future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } catch (RuntimeException e) { + Exception rootException = ExceptionUtil.rootCause(e); + if (ExceptionUtil.isLockTimeout(rootException)) { + fail("Should not throw a lock timeout exception"); + } else { + throw e; + } + } + } + + protected void prepareConnection(EntityManager entityManager) { + entityManager.unwrap(Session.class).doWork(connection -> { + connection.setTransactionIsolation(ISOLATION_LEVEL); + setJdbcTimeout(connection); + }); + } + + @Entity(name = "Post") + @Table(name = "Post") + public static class Post { + + @Id + @Column(name = "PostID") + private Long id; + + @Column(name = "Title") + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table( + name = "PostComment", + indexes = @Index( + name = "FK_PostComment_PostID", + columnList = "PostID" + ) + ) + @DynamicUpdate + public static class PostComment { + + @Id + @Column(name = "PostCommentID") + private Long id; + + @Column(name = "Review") + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "PostID") + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/latch/CountDownLatchTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/latch/CountDownLatchTest.java new file mode 100644 index 000000000..5e17df5f0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/latch/CountDownLatchTest.java @@ -0,0 +1,84 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.latch; + +import com.vladmihalcea.hpjp.hibernate.concurrency.acid.Account; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.transaction.ConnectionCallable; +import com.vladmihalcea.hpjp.util.transaction.ConnectionVoidCallable; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class CountDownLatchTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + protected final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + @Test + public void testNoCoordination() { + LOGGER.info("Main thread starts"); + + int workerThreadCount = 5; + + for (int i = 1; i <= workerThreadCount; i++) { + String threadId = String.valueOf(i); + new Thread( + () -> LOGGER.info("Worker thread {} runs", threadId), + "Thread-" + threadId + ).start(); + } + + LOGGER.info("Main thread finishes"); + } + + @Test + public void testNoCoordinationExecutorService() { + LOGGER.info("Main thread starts"); + + executorService.submit(() -> { + LOGGER.info("Worker thread runs"); + }); + + LOGGER.info("Main thread finishes"); + } + + @Test + public void testCountDownLatch() throws InterruptedException { + LOGGER.info("Main thread starts"); + + int workerThreadCount = 5; + + CountDownLatch endLatch = new CountDownLatch(workerThreadCount); + + for (int i = 1; i <= workerThreadCount; i++) { + String threadId = String.valueOf(i); + new Thread( + () -> { + LOGGER.info("Worker thread {} runs", threadId); + + endLatch.countDown(); + }, + "Thread-" + threadId + ).start(); + } + + LOGGER.info("Main thread waits for the worker threads to finish"); + endLatch.await(); + + LOGGER.info("Main thread finishes"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/linearizable/JavaLinearizableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/linearizable/JavaLinearizableTest.java new file mode 100644 index 000000000..5ce419df0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/linearizable/JavaLinearizableTest.java @@ -0,0 +1,111 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.linearizable; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class JavaLinearizableTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Account.class + }; + } + + int threadCount = 32; + + public static ForkJoinPool forkJoinPool = ForkJoinPool.commonPool(); + + public void transfer(Account from, Account to, long transferCents) { + long fromBalance = from.getAccountBalance(); + + if(fromBalance >= transferCents) { + from.addToAccountBalance(-1 * transferCents); + to.addToAccountBalance(transferCents); + } + } + + @Test + public void testParallelExecution() { + + Account fromAccount = new Account(); + fromAccount.setId("Alice-123"); + fromAccount.setOwner("Alice"); + fromAccount.setBalance(10); + + Account toAccount = new Account(); + toAccount.setId("Bob-456"); + toAccount.setOwner("Bob"); + toAccount.setBalance(0L); + + List> callables = new ArrayList<>(); + + for (int i = 0; i < threadCount; i++) { + callables.add(() -> { + transfer(fromAccount, toAccount, 5); + + return null; + }); + } + + LOGGER.info("Starting threads"); + List> futures = forkJoinPool.invokeAll(callables); + for (Future future : futures) { + try { + future.get(); + } catch (InterruptedException|ExecutionException e) { + fail(e.getMessage()); + } + } + + LOGGER.info("Alice's balance {}", fromAccount.getAccountBalance()); + LOGGER.info("Bob's balance {}", toAccount.getAccountBalance()); + } + + public static class Account { + + private String id; + + private String owner; + + private long balance; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public long getAccountBalance() { + return balance; + } + + public void setBalance(long balance) { + this.balance = balance; + } + + public void addToAccountBalance(long amount) { + this.balance += amount; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/updates/PostgreSQLRowLevelLockingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/updates/PostgreSQLRowLevelLockingTest.java new file mode 100644 index 000000000..eb640332c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/updates/PostgreSQLRowLevelLockingTest.java @@ -0,0 +1,158 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.updates; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.annotations.DynamicUpdate; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLRowLevelLockingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setPriceCents(4495) + ); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book post = entityManager.find(Book.class, 1L); + post.setPriceCents(3995); + + entityManager.flush(); + + CountDownLatch bobThreadStartLatch = new CountDownLatch(1); + CountDownLatch aliceThreadSleepStartLatch = new CountDownLatch(1); + + executeAsync(() -> { + doInJPA(_entityManager -> { + awaitOnLatch(bobThreadStartLatch); + + LOGGER.info("Bob updates the book record"); + Book _post = _entityManager.find(Book.class, 1L); + _post.setTitle("High-Performance Java Persistence, 2nd edition"); + + aliceThreadSleepStartLatch.countDown(); + _entityManager.flush(); + }); + }); + + bobThreadStartLatch.countDown(); + awaitOnLatch(aliceThreadSleepStartLatch); + LOGGER.info("Alice's thread sleeps for 1 second"); + sleep(TimeUnit.SECONDS.toMillis(1)); + }); + + doInJPA(entityManager -> { + Book post = entityManager.find(Book.class, 1L); + + assertEquals("High-Performance Java Persistence, 2nd edition", post.getTitle()); + assertEquals(3995, post.getPriceCents()); + }); + } + + @Test + public void testTimeout() { + doInJPA(entityManager -> { + Book post = entityManager.find(Book.class, 1L); + post.setPriceCents(3995); + + LOGGER.info("Alice updates the book record"); + entityManager.flush(); + + executeSync(() -> { + doInJPA(_entityManager -> { + executeStatement(_entityManager, "SET lock_timeout TO '1s'"); + + Book _post = _entityManager.find(Book.class, 1L); + _post.setTitle("High-Performance Java Persistence, 2nd edition"); + + LOGGER.info("Bob updates the book record"); + try { + _entityManager.flush(); + } catch (Exception expected) { + assertTrue( + ExceptionUtil.rootCause(expected) + .getMessage() + .contains("canceling statement due to lock timeout") + ); + LOGGER.error("Lock acquisition failure: ", expected); + } + }); + }); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + @DynamicUpdate + public static class Book { + + @Id + private Long id; + + private String title; + + @Column(name = "price_cents") + private int priceCents; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public int getPriceCents() { + return priceCents; + } + + public Book setPriceCents(int priceCents) { + this.priceCents = priceCents; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/updates/YugabyteDBColumnLevelLockingDefaultUpdateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/updates/YugabyteDBColumnLevelLockingDefaultUpdateTest.java new file mode 100644 index 000000000..3914fa29f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/updates/YugabyteDBColumnLevelLockingDefaultUpdateTest.java @@ -0,0 +1,120 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.updates; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +public class YugabyteDBColumnLevelLockingDefaultUpdateTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected Database database() { + return Database.YUGABYTEDB; + } + + @Override + public void init() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + super.init(); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setPriceCents(4495) + ); + }); + } + + @Test + public void test() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + try { + doInJPA(entityManager -> { + Book post = entityManager.find(Book.class, 1L); + post.setPriceCents(3995); + + LOGGER.info("Alice updates the book record"); + entityManager.flush(); + + executeSync(() -> { + try { + doInJPA(_entityManager -> { + Book _post = _entityManager.find(Book.class, 1L); + _post.setTitle("High-Performance Java Persistence, 2nd edition"); + + LOGGER.info("Bob updates the book record"); + _entityManager.flush(); + }); + } catch (Exception e) { + LOGGER.error("Bob's optimistic locking failure: ", e); + } + }); + }); + } catch (Exception e) { + LOGGER.error("Alice's optimistic locking failure: ", e); + } + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + private Long id; + + private String title; + + @Column(name = "price_cents") + private int priceCents; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public int getPriceCents() { + return priceCents; + } + + public Book setPriceCents(int priceCents) { + this.priceCents = priceCents; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/updates/YugabyteDBColumnLevelLockingReadCommittedTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/updates/YugabyteDBColumnLevelLockingReadCommittedTest.java new file mode 100644 index 000000000..78119be45 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/updates/YugabyteDBColumnLevelLockingReadCommittedTest.java @@ -0,0 +1,157 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.updates; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicUpdate; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class YugabyteDBColumnLevelLockingReadCommittedTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected Database database() { + return Database.YUGABYTEDB; + } + + @Override + public void init() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + super.init(); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setPriceCents(4495) + ); + }); + } + + @Test + public void test() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJPA(entityManager -> { + Book post = entityManager.find(Book.class, 1L); + post.setPriceCents(3995); + + entityManager.flush(); + + executeSync(() -> { + doInJPA(_entityManager -> { + Book _post = _entityManager.find(Book.class, 1L); + _post.setTitle("High-Performance Java Persistence, 2nd edition"); + }); + }); + }); + + doInJPA(entityManager -> { + Book post = entityManager.find(Book.class, 1L); + + assertEquals("High-Performance Java Persistence, 2nd edition", post.getTitle()); + assertEquals(3995, post.getPriceCents()); + }); + } + + @Test + public void testUpdateSameColumn() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJPA(entityManager -> { + executeStatement(entityManager, "set default_transaction_isolation to \"read committed\";"); + + Book post = entityManager.find(Book.class, 1L); + post.setTitle("High-Performance Java Persistence, 2022 edition"); + + LOGGER.info("Alice updates the book record"); + entityManager.flush(); + + executeSync(() -> { + doInJPA(_entityManager -> { + executeStatement(_entityManager, "set default_transaction_isolation to \"read committed\";"); + _entityManager.unwrap(Session.class).doWork(this::setJdbcTimeout); + + Book _post = _entityManager.find(Book.class, 1L); + _post.setTitle("High-Performance Java Persistence, 2nd edition"); + + LOGGER.info("Bob updates the book record"); + try { + _entityManager.flush(); + } catch (Exception expected) { + assertTrue( + ExceptionUtil.rootCause(expected) + .getMessage() + .contains("Read timeout") + ); + LOGGER.error("Lock acquisition failure: ", expected); + } + }); + }); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + @DynamicUpdate + public static class Book { + + @Id + private Long id; + + private String title; + + @Column(name = "price_cents") + private int priceCents; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public int getPriceCents() { + return priceCents; + } + + public Book setPriceCents(int priceCents) { + this.priceCents = priceCents; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/updates/YugabyteDBColumnLevelLockingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/updates/YugabyteDBColumnLevelLockingTest.java new file mode 100644 index 000000000..320ff8ce4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/updates/YugabyteDBColumnLevelLockingTest.java @@ -0,0 +1,148 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.updates; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.annotations.DynamicUpdate; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class YugabyteDBColumnLevelLockingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected Database database() { + return Database.YUGABYTEDB; + } + + @Override + public void init() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + super.init(); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setPriceCents(4495) + ); + }); + } + + @Test + public void test() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJPA(entityManager -> { + Book post = entityManager.find(Book.class, 1L); + post.setPriceCents(3995); + + entityManager.flush(); + + executeSync(() -> { + doInJPA(_entityManager -> { + Book _post = _entityManager.find(Book.class, 1L); + _post.setTitle("High-Performance Java Persistence, 2nd edition"); + }); + }); + }); + + doInJPA(entityManager -> { + Book post = entityManager.find(Book.class, 1L); + + assertEquals("High-Performance Java Persistence, 2nd edition", post.getTitle()); + assertEquals(3995, post.getPriceCents()); + }); + } + + @Test + public void testUpdateSameColumn() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + try { + doInJPA(entityManager -> { + Book post = entityManager.find(Book.class, 1L); + post.setTitle("High-Performance Java Persistence, 2022 edition"); + + LOGGER.info("Alice updates the book record"); + entityManager.flush(); + + + executeSync(() -> { + doInJPA(_entityManager -> { + Book _post = _entityManager.find(Book.class, 1L); + _post.setTitle("High-Performance Java Persistence, 2nd edition"); + + LOGGER.info("Bob updates the book record"); + _entityManager.flush(); + }); + }); + }); + } catch (Exception expected) { + LOGGER.error("Lock acquisition failure: ", expected); + assertTrue(ExceptionUtil.isCausedBy(expected, OptimisticLockException.class)); + } + } + + @Entity(name = "Book") + @Table(name = "book") + @DynamicUpdate + public static class Book { + + @Id + private Long id; + + private String title; + + @Column(name = "price_cents") + private int priceCents; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public int getPriceCents() { + return priceCents; + } + + public Book setPriceCents(int priceCents) { + this.priceCents = priceCents; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/upsert/SQLServerUpsertAlternativeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/upsert/SQLServerUpsertAlternativeTest.java new file mode 100644 index 000000000..30fed4cd4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/upsert/SQLServerUpsertAlternativeTest.java @@ -0,0 +1,106 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.upsert; + +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.LockModeType; +import jakarta.persistence.Table; +import java.util.concurrent.CountDownLatch; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerUpsertAlternativeTest extends AbstractSQLServerIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void testUpsertWithInsertAndUpdateLock() { + CountDownLatch aliceCountDownLatch = new CountDownLatch(1); + + doInJPA(entityManager -> { + Book book = new Book(); + book.setId(1L); + book.setTitle("Book 1"); + + entityManager.persist(book); + entityManager.flush(); + + executeAsync(() -> { + doInJPA(_entityManager -> { + try { + LOGGER.info("Bob tries to lock the record"); + Book _book = _entityManager.find(Book.class, 1L, LockModeType.PESSIMISTIC_READ); + if (_book == null) { + LOGGER.info("Bob inserts"); + _book = new Book(); + _book.setId(1L); + _book.setTitle("Book 2"); + _entityManager.persist(_book); + } else { + LOGGER.info("Bob updates"); + _book.setTitle("Book 2"); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + aliceCountDownLatch.countDown(); + } + }); + }); + + LOGGER.info("Alice starts sleeping"); + sleep(5000); + LOGGER.info("Alice woke up and releases the logs after she commits"); + }); + + LOGGER.info(""); + awaitOnLatch(aliceCountDownLatch); + } + + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + private Long id; + + @NaturalId + private String isbn; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/DefaultMinValueShortVersionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/DefaultMinValueShortVersionTest.java new file mode 100644 index 000000000..15a92931d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/DefaultMinValueShortVersionTest.java @@ -0,0 +1,97 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.version; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DefaultMinValueShortVersionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Product.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Test + public void testOptimisticLocking() { + + doInJPA(entityManager -> { + entityManager.persist( + new Product() + .setId(1L) + .setQuantity(10) + .setVersion(Short.MAX_VALUE) + ); + }); + + doInJPA(entityManager -> { + Product product = entityManager.find(Product.class, 1L); + + assertEquals(Short.MAX_VALUE, product.getVersion()); + + product.setQuantity(9); + }); + + doInJPA(entityManager -> { + Product product = entityManager.find(Product.class, 1L); + + assertEquals(Short.MIN_VALUE, product.getVersion()); + }); + } + + @Entity(name = "Product") + @Table(name = "product") + public static class Product { + + @Id + private Long id; + + private int quantity; + + @Version + private short version; + + public Long getId() { + return id; + } + + public Product setId(Long id) { + this.id = id; + return this; + } + + public int getQuantity() { + return quantity; + } + + public Product setQuantity(int quantity) { + this.quantity = quantity; + return this; + } + + public short getVersion() { + return version; + } + + public Product setVersion(short version) { + this.version = version; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/DefaultMinValueVersionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/DefaultMinValueVersionTest.java new file mode 100644 index 000000000..4b1114d42 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/DefaultMinValueVersionTest.java @@ -0,0 +1,93 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.version; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +/** + * @author Vlad Mihalcea + */ +public class DefaultMinValueVersionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testOptimisticLocking() { + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + entityManager.flush(); + + int updateCount = entityManager.createNativeQuery( + "UPDATE post SET version = :version WHERE id = :id") + .setParameter("version", Short.MAX_VALUE) + .setParameter("id", 1L) + .executeUpdate(); + + assertSame(1, updateCount); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(Short.MAX_VALUE, post.getVersion()); + + post.setTitle("High-Performance Java Persistence, 2nd edition"); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(Short.MIN_VALUE, post.getVersion()); + + post.setTitle("High-Performance Java Persistence, 3nd edition"); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private Short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getVersion() { + return version; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/MinValueIntegerVersionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/MinValueIntegerVersionTest.java new file mode 100644 index 000000000..f614b4c85 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/MinValueIntegerVersionTest.java @@ -0,0 +1,90 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.version; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MinValueIntegerVersionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Product.class + }; + } + + @Test + public void testOptimisticLocking() { + + doInJPA(entityManager -> { + entityManager.persist( + new Product() + .setId(1L) + .setQuantity(10) + .setVersion(Integer.MAX_VALUE) + ); + }); + + doInJPA(entityManager -> { + Product product = entityManager.find(Product.class, 1L); + + assertEquals(Integer.MAX_VALUE, product.getVersion()); + + product.setQuantity(9); + }); + + doInJPA(entityManager -> { + Product product = entityManager.find(Product.class, 1L); + + assertEquals(Integer.MIN_VALUE, product.getVersion()); + }); + } + + @Entity(name = "Product") + @Table(name = "product") + public static class Product { + + @Id + private Long id; + + private int quantity; + + @Version + private int version; + + public Long getId() { + return id; + } + + public Product setId(Long id) { + this.id = id; + return this; + } + + public int getQuantity() { + return quantity; + } + + public Product setQuantity(int quantity) { + this.quantity = quantity; + return this; + } + + public int getVersion() { + return version; + } + + public Product setVersion(int version) { + this.version = version; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/MinValueVersionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/MinValueVersionTest.java new file mode 100644 index 000000000..8c378ea8b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/MinValueVersionTest.java @@ -0,0 +1,96 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.version; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MinValueVersionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testOptimisticLocking() { + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + entityManager.flush(); + + int updateCount = entityManager.createNativeQuery(""" + UPDATE post + SET version = :version + WHERE id = :id + """) + .setParameter("version", Short.MAX_VALUE) + .setParameter("id", post.getId()) + .executeUpdate(); + + assertEquals(1, updateCount); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(Short.MAX_VALUE, post.getVersion()); + + post.setTitle("High-Performance Hibernate"); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(Short.MIN_VALUE, post.getVersion()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private Short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getVersion() { + return version; + } + + public void setVersion(Short version) { + this.version = version; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/ShortVersionType.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/ShortVersionType.java new file mode 100644 index 000000000..f1ced854d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/ShortVersionType.java @@ -0,0 +1,15 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.version; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.type.descriptor.java.ShortJavaType; + +/** + * @author Vlad Mihalcea + */ +public class ShortVersionType extends ShortJavaType { + + @Override + public Short seed(Long length, Integer precision, Integer scale, SharedSessionContractImplementor session) { + return Short.MIN_VALUE; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/TimestampVersionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/TimestampVersionTest.java new file mode 100644 index 000000000..c04dcd1e9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/TimestampVersionTest.java @@ -0,0 +1,83 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.version; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import org.junit.Test; + +import java.time.LocalDateTime; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class TimestampVersionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testOptimisticLocking() { + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + entityManager.flush(); + post.setTitle("High-Performance Hibernate"); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNotNull(post.getVersion()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private LocalDateTime version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDateTime getVersion() { + return version; + } + + public void setVersion(LocalDateTime version) { + this.version = version; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/VersionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/VersionTest.java new file mode 100644 index 000000000..6690a5541 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/concurrency/version/VersionTest.java @@ -0,0 +1,215 @@ +package com.vladmihalcea.hpjp.hibernate.concurrency.version; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import org.hibernate.Session; +import org.hibernate.StaleStateException; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class VersionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Product.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Product product = new Product(); + product.setId(1L); + + entityManager.persist(product); + }); + + doInJPA(entityManager -> { + Product product = entityManager.find(Product.class, 1L); + assertEquals(0, product.getVersion()); + + product.setQuantity(5); + }); + + doInJPA(entityManager -> { + Product product = entityManager.find(Product.class, 1L); + assertEquals(1, product.getVersion()); + }); + } + + @Test + public void testOptimisticLockingException() { + try { + doInJPA(entityManager -> { + Product product = entityManager.find(Product.class, 1L); + + executeSync(() -> doInJPA(_entityManager -> { + LOGGER.info("Batch processor updates product stock"); + + Product _product = _entityManager.find(Product.class, 1L); + _product.setQuantity(0); + })); + + LOGGER.info("Changing the previously loaded Product entity"); + product.setQuantity(4); + }); + } catch (Exception expected) { + LOGGER.error("Throws", expected); + + assertEquals(OptimisticLockException.class, expected.getCause().getClass()); + assertTrue(ExceptionUtil.rootCause(expected) instanceof StaleStateException); + } + } + + @Test + public void testDelete() { + doInJPA(entityManager -> { + Product product = entityManager.getReference(Product.class, 1L); + + entityManager.remove(product); + }); + } + + @Test + public void testChangeVersion() { + doInJPA(entityManager -> { + Product product = entityManager.find(Product.class, 1L); + + product.setVersion(100); + }); + } + + @Test + public void testChangeVersionAfterDetachUsingMerge() { + AtomicBoolean lostUpdatePrevented = new AtomicBoolean(); + doInJPA(entityManager -> { + LOGGER.info("Test optimistic locking for detached entity when using merge"); + Product product = entityManager.find(Product.class, 1L); + entityManager.detach(product); + + product.setQuantity(12); + product.setVersion(0); + + try { + entityManager.merge(product); + } catch (Exception expected) { + assertTrue(expected instanceof OptimisticLockException); + lostUpdatePrevented.set(true); + } + }); + assertTrue(lostUpdatePrevented.get()); + } + + @Test + public void testChangeVersionAfterDetachUsingUpdate() { + AtomicBoolean lostUpdatePrevented = new AtomicBoolean(); + doInJPA(entityManager -> { + LOGGER.info("Test optimistic locking for detached entity when using update"); + Product product = entityManager.find(Product.class, 1L); + entityManager.detach(product); + + product.setQuantity(12); + product.setVersion(0); + + entityManager.unwrap(Session.class).update(product); + try { + entityManager.flush(); + } catch (Exception expected) { + assertTrue(expected instanceof OptimisticLockException); + lostUpdatePrevented.set(true); + } + }); + assertTrue(lostUpdatePrevented.get()); + } + + + @Test + public void testMerge() { + + String productJsonString = doInJPA(entityManager -> { + return JacksonUtil.toString( + entityManager.find(Product.class, 1L) + ); + }); + + executeSync(() -> doInJPA(entityManager -> { + LOGGER.info("Batch processor updates product stock"); + + Product product = entityManager.find(Product.class, 1L); + product.setQuantity(0); + })); + + LOGGER.info("Changing the previously loaded Product entity"); + ObjectNode productJsonNode = (ObjectNode) JacksonUtil.toJsonNode(productJsonString); + int quantity = productJsonNode.get("quantity").asInt(); + productJsonNode.put("quantity", String.valueOf(--quantity)); + + try { + doInJPA(entityManager -> { + LOGGER.info("Merging the Product entity"); + + Product product = JacksonUtil.fromString( + productJsonNode.toString(), + Product.class + ); + entityManager.merge(product); + }); + } catch (Exception expected) { + LOGGER.error("Throws", expected); + assertEquals(OptimisticLockException.class, expected.getClass()); + assertTrue(ExceptionUtil.rootCause(expected) instanceof StaleStateException); + } + } + + @Entity(name = "Product") + @Table(name = "product") + public static class Product { + + @Id + private Long id; + + private int quantity; + + @Version + private short version; + + public Long getId() { + return id; + } + + public Product setId(Long id) { + this.id = id; + return this; + } + + public int getQuantity() { + return quantity; + } + + public Product setQuantity(int quantity) { + this.quantity = quantity; + return this; + } + + public int getVersion() { + return version; + } + + public Product setVersion(int version) { + this.version = (short) version; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/AbstractConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/AbstractConnectionProviderTest.java new file mode 100644 index 000000000..02cad1945 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/AbstractConnectionProviderTest.java @@ -0,0 +1,47 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.junit.Test; + +import javax.sql.DataSource; +import java.util.Properties; + +import static org.junit.Assert.assertTrue; + +public abstract class AbstractConnectionProviderTest extends AbstractTest { + + private BlogEntityProvider entityProvider = new BlogEntityProvider(); + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + protected DataSource newDataSource() { + return null; + } + + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.hbm2ddl.auto", "create-drop"); + appendDriverProperties(properties); + return properties; + } + + protected abstract void appendDriverProperties(Properties properties); + + @Test + public void testConnectionProvider() { + SessionFactoryImplementor sessionFactory = entityManagerFactory() + .unwrap(SessionFactoryImplementor.class); + + ConnectionProvider connectionProvider = sessionFactory.getServiceRegistry() + .getService(ConnectionProvider.class); + assertTrue(expectedConnectionProviderClass().isInstance(connectionProvider)); + } + + public abstract Class expectedConnectionProviderClass(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/C3P0ConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/C3P0ConnectionProviderTest.java new file mode 100644 index 000000000..16f7b85e1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/C3P0ConnectionProviderTest.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import org.hibernate.c3p0.internal.C3P0ConnectionProvider; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; + +import java.util.Properties; + +public class C3P0ConnectionProviderTest extends JPADriverConnectionProviderTest { + + @Override + protected void appendDriverProperties(Properties properties) { + super.appendDriverProperties(properties); + int maxPoolSize = 5; + properties.put("hibernate.c3p0.min_size", 1); + properties.put("hibernate.c3p0.max_size", maxPoolSize); + } + + @Override + public Class expectedConnectionProviderClass() { + return C3P0ConnectionProvider.class; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/C3P0JPAConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/C3P0JPAConnectionProviderTest.java new file mode 100644 index 000000000..aaa4675d9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/C3P0JPAConnectionProviderTest.java @@ -0,0 +1,21 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import org.hibernate.c3p0.internal.C3P0ConnectionProvider; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; + +import java.util.Properties; + +public class C3P0JPAConnectionProviderTest extends DriverManagerConnectionProviderTest { + + @Override + protected void appendDriverProperties(Properties properties) { + super.appendDriverProperties(properties); + properties.put("hibernate.c3p0.min_size", 1); + properties.put("hibernate.c3p0.max_size", 5); + } + + @Override + public Class expectedConnectionProviderClass() { + return C3P0ConnectionProvider.class; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/DataSourceProxyConnectionProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/DataSourceProxyConnectionProvider.java similarity index 85% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/DataSourceProxyConnectionProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/DataSourceProxyConnectionProvider.java index b5415f26d..ace6ef64f 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/DataSourceProxyConnectionProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/DataSourceProxyConnectionProvider.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; +package com.vladmihalcea.hpjp.hibernate.connection; -import net.ttddyy.dsproxy.listener.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; import org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/DriverManagerConnectionProviderJakartaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/DriverManagerConnectionProviderJakartaTest.java new file mode 100644 index 000000000..c1a8c9479 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/DriverManagerConnectionProviderJakartaTest.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; + +import java.util.Properties; + +public class DriverManagerConnectionProviderJakartaTest extends DriverManagerConnectionProviderTest { + + protected void appendDriverProperties(Properties properties) { + DataSourceProvider dataSourceProvider = dataSourceProvider(); + + String url = dataSourceProvider.url(); + String username = dataSourceProvider.username(); + String password = dataSourceProvider.password(); + + properties.put( + "jakarta.persistence.jdbc.driver", + dataSourceProvider.driverClassName().getName() + ); + properties.put("jakarta.persistence.jdbc.url", url); + properties.put("jakarta.persistence.jdbc.user", username); + properties.put("jakarta.persistence.jdbc.password", password); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/DriverManagerConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/DriverManagerConnectionProviderTest.java new file mode 100644 index 000000000..774cf68bb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/DriverManagerConnectionProviderTest.java @@ -0,0 +1,59 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.SQLServerDataSourceProvider; +import org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; +import org.junit.Test; + +import java.util.Properties; +import java.util.concurrent.atomic.AtomicLong; + +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.Post; +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.PostComment; + +public class DriverManagerConnectionProviderTest extends AbstractConnectionProviderTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new SQLServerDataSourceProvider(); + } + + protected void appendDriverProperties(Properties properties) { + DataSourceProvider dataSourceProvider = dataSourceProvider(); + + String url = dataSourceProvider.url(); + String username = dataSourceProvider.username(); + String password = dataSourceProvider.password(); + + properties.put("hibernate.connection.driver_class", dataSourceProvider.driverClassName().getName()); + properties.put("hibernate.connection.url", url); + properties.put("hibernate.connection.username", username); + properties.put("hibernate.connection.password", password); + } + + @Override + public Class expectedConnectionProviderClass() { + return DriverManagerConnectionProviderImpl.class; + } + + @Test + public void testConnection() { + for (final AtomicLong i = new AtomicLong(); i.get() < 5; i.incrementAndGet()) { + doInJPA(em -> { + em.persist(new Post(i.get())); + }); + } + + doInJPA(em -> { + Post post = em.find(Post.class, 1L); + PostComment comment = new PostComment("abc"); + comment.setId(1L); + post.addComment(comment); + em.persist(comment); + }); + doInJPA(em -> { + em.createQuery("select p from Post p join fetch p.comments", Post.class).getResultList(); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/FlexyPoolTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/FlexyPoolTest.java new file mode 100644 index 000000000..7e5900ae1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/FlexyPoolTest.java @@ -0,0 +1,119 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import com.vladmihalcea.flexypool.FlexyPoolDataSource; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; + +import static com.vladmihalcea.hpjp.hibernate.connection.jta.FlexyPoolEntities.Post; +import static com.vladmihalcea.hpjp.util.AbstractTest.ENABLE_LONG_RUNNING_TESTS; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = FlexyPoolTestConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public class FlexyPoolTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private TransactionTemplate transactionTemplate; + + @Autowired + private DataSource dataSource; + + private int threadCount = 6; + + private int seconds = 120; + + private ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + + @Before + public void init() { + FlexyPoolDataSource flexyPoolDataSource = (FlexyPoolDataSource) dataSource; + flexyPoolDataSource.start(); + } + + @After + public void destroy() { + executorService.shutdownNow(); + FlexyPoolDataSource flexyPoolDataSource = (FlexyPoolDataSource) dataSource; + flexyPoolDataSource.stop(); + } + + @Test + public void test() throws InterruptedException, ExecutionException { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + long startNanos = System.nanoTime(); + + CountDownLatch awaitTermination = new CountDownLatch(threadCount); + List> tasks = new ArrayList<>(); + + AtomicLong postCount = new AtomicLong(); + + for (int i = 0; i < threadCount; i++) { + tasks.add( + () -> { + LOGGER.info("Starting worker thread"); + + while (TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startNanos) < seconds) { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + for (int j = 0; j < 100; j++) { + entityManager.persist(new Post()); + } + + postCount.set( + entityManager.createQuery( + "select count(p) " + + "from Post p ", Number.class) + .getSingleResult() + .longValue() + ); + + if (postCount.get() % 1000 == 0) { + LOGGER.info("Post entity count: {}", postCount); + sleep(250, TimeUnit.MILLISECONDS); + } + + return null; + }); + } + awaitTermination.countDown(); + return null; + } + ); + } + + executorService.invokeAll(tasks); + awaitTermination.await(); + } + + private void sleep(long duration, TimeUnit timeUnit) { + try { + Thread.sleep(timeUnit.toMillis(duration)); + } catch (InterruptedException e) { + Thread.interrupted(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/FlexyPoolTestConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/FlexyPoolTestConfiguration.java new file mode 100644 index 000000000..37f1526d9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/FlexyPoolTestConfiguration.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import com.vladmihalcea.flexypool.FlexyPoolDataSource; +import com.vladmihalcea.flexypool.adaptor.HikariCPPoolAdapter; +import com.vladmihalcea.flexypool.config.FlexyPoolConfiguration; +import com.vladmihalcea.hpjp.hibernate.connection.jta.FlexyPoolEntities; +import com.vladmihalcea.hpjp.util.spring.config.jpa.HikariCPJPAConfiguration; +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; + +@Configuration +public class FlexyPoolTestConfiguration extends HikariCPJPAConfiguration { + + @Override + protected Class configurationClass() { + return FlexyPoolEntities.class; + } + + @Override + public DataSource actualDataSource() { + final HikariDataSource dataSource = (HikariDataSource) super.actualDataSource(); + + FlexyPoolConfiguration configuration = + new FlexyPoolConfiguration.Builder<>( + "flexy-pool-test", + dataSource, + HikariCPPoolAdapter.FACTORY + ) + .build(); + + return new FlexyPoolDataSource<>( + configuration + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/HikariCPConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/HikariCPConnectionProviderTest.java new file mode 100644 index 000000000..812ccbc89 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/HikariCPConnectionProviderTest.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; +import org.hibernate.hikaricp.internal.HikariCPConnectionProvider; + +import java.util.Properties; + +public class HikariCPConnectionProviderTest extends DriverManagerConnectionProviderTest { + + @Override + protected void appendDriverProperties(Properties properties) { + super.appendDriverProperties(properties); + String maxPoolSize = String.valueOf(5); + properties.put("hibernate.hikari.maximumPoolSize", maxPoolSize); + properties.put("hibernate.hikari.minimumIdle", maxPoolSize); + } + + @Override + public Class expectedConnectionProviderClass() { + return HikariCPConnectionProvider.class; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/HikariConnectionThreadBoundTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/HikariConnectionThreadBoundTest.java new file mode 100644 index 000000000..c024df450 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/HikariConnectionThreadBoundTest.java @@ -0,0 +1,32 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Session; +import org.junit.Test; + +import java.sql.Connection; +import java.util.concurrent.ExecutionException; + +public class HikariConnectionThreadBoundTest extends AbstractTest { + + @Test + public void test() throws InterruptedException, ExecutionException { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Connection anotherConnection = dataSource().getConnection()) { + LOGGER.info("Connections got from RESOURCE_LOCAL transactions are{} bound to thread", connection == anotherConnection ? "" : " not"); + } + }); + }); + } + + @Override + protected Class[] entities() { + return new Class[]{}; + } + + @Override + protected boolean connectionPooling() { + return true; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/JPADataSourceConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/JPADataSourceConnectionProviderTest.java new file mode 100644 index 000000000..55f46475f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/JPADataSourceConnectionProviderTest.java @@ -0,0 +1,26 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import com.vladmihalcea.hpjp.util.PersistenceUnitInfoImpl; +import org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; + +import java.util.Properties; + +public class JPADataSourceConnectionProviderTest extends DriverManagerConnectionProviderTest { + + protected void appendDriverProperties(Properties properties) { + + } + + @Override + public Class expectedConnectionProviderClass() { + return DatasourceConnectionProviderImpl.class; + } + + @Override + protected PersistenceUnitInfoImpl persistenceUnitInfo(String name) { + PersistenceUnitInfoImpl persistenceUnitInfo = super.persistenceUnitInfo(name); + return persistenceUnitInfo.setNonJtaDataSource(dataSourceProvider().dataSource()); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/JPADataSourceProxyConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/JPADataSourceProxyConnectionProviderTest.java similarity index 84% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/JPADataSourceProxyConnectionProviderTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/JPADataSourceProxyConnectionProviderTest.java index 89fdf9a3c..ac7f8b140 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/JPADataSourceProxyConnectionProviderTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/JPADataSourceProxyConnectionProviderTest.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection; +package com.vladmihalcea.hpjp.hibernate.connection; import java.util.Properties; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/JPADriverConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/JPADriverConnectionProviderTest.java new file mode 100644 index 000000000..12d4c2a67 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/JPADriverConnectionProviderTest.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; + +import java.util.Properties; + +public class JPADriverConnectionProviderTest extends DriverManagerConnectionProviderTest { + + protected void appendDriverProperties(Properties properties) { + DataSourceProvider dataSourceProvider = dataSourceProvider(); + properties.put("jakarta.persistence.jdbc.driver", dataSourceProvider.driverClassName().getName()); + properties.put("jakarta.persistence.jdbc.url", dataSourceProvider.url()); + properties.put("jakarta.persistence.jdbc.user", dataSourceProvider.username()); + properties.put("jakarta.persistence.jdbc.password", dataSourceProvider.password()); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/ResourceLocalDelayConnectionAcquisitionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/ResourceLocalDelayConnectionAcquisitionTest.java new file mode 100644 index 000000000..9b0bcc4ac --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/ResourceLocalDelayConnectionAcquisitionTest.java @@ -0,0 +1,245 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import com.vladmihalcea.flexypool.FlexyPoolDataSource; +import com.vladmihalcea.flexypool.adaptor.DataSourcePoolAdapter; +import com.vladmihalcea.flexypool.config.FlexyPoolConfiguration; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import jakarta.persistence.*; +import org.junit.Ignore; +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.sql.DataSource; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +public class ResourceLocalDelayConnectionAcquisitionTest extends AbstractTest { + + private static final String DATA_FILE_PATH = "data/weather.xml"; + + private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + + private long warmUpDuration = TimeUnit.SECONDS.toNanos(5); + + private long measurementsDuration = TimeUnit.SECONDS.toNanos(15); + + private int parseCount = 100; + + private FlexyPoolDataSource flexyPoolDataSource; + + @Override + protected Class[] entities() { + return new Class[]{ + Forecast.class + }; + } + + protected boolean connectionPooling() { + return false; + } + + protected HikariConfig hikariConfig(DataSource dataSource) { + HikariConfig hikariConfig = super.hikariConfig(dataSource); + hikariConfig.setAutoCommit(false); + return hikariConfig; + } + + protected HikariDataSource connectionPoolDataSource(DataSource dataSource) { + HikariConfig hikariConfig = new HikariConfig(); + int cpuCores = Runtime.getRuntime().availableProcessors(); + hikariConfig.setMaximumPoolSize(cpuCores * 4); + hikariConfig.setDataSource(dataSource); + + return new HikariDataSource(hikariConfig); + } + + @Override + protected DataSource newDataSource() { + DataSource dataSource = super.newDataSource(); + + FlexyPoolConfiguration configuration = new FlexyPoolConfiguration.Builder<>( + getClass().getSimpleName(), dataSource, DataSourcePoolAdapter.FACTORY) + .setMetricLogReporterMillis(TimeUnit.SECONDS.toMillis(15)) + .build(); + flexyPoolDataSource = new FlexyPoolDataSource<>(configuration); + return flexyPoolDataSource; + } + + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.generate_statistics", Boolean.FALSE.toString()); + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.connection.provider_disables_autocommit", "true"); + return properties; + } + + @Test + @Ignore + public void testConnectionLeaseTime() { + long warmUpThreshold = System.nanoTime() + warmUpDuration; + LOGGER.info("Warming up"); + + while (System.nanoTime() < warmUpThreshold) { + importForecasts(); + } + + long measurementsThreshold = System.nanoTime() + measurementsDuration; + + LOGGER.info("Measuring connection lease time"); + flexyPoolDataSource.start(); + while (System.nanoTime() < measurementsThreshold) { + importForecasts(); + } + flexyPoolDataSource.stop(); + sleep(500); + } + + private void importForecasts() { + doInJPA(entityManager -> { + List forecasts = null; + + for (int i = 0; i < parseCount; i++) { + Document forecastXmlDocument = readXmlDocument(DATA_FILE_PATH); + forecasts = parseForecasts(forecastXmlDocument); + } + + if (forecasts != null) { + for (Forecast forecast : forecasts.subList(0, 50)) { + entityManager.persist(forecast); + } + } + }); + } + + private Document readXmlDocument(String filePath) { + try (InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath)) { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + Document doc = dBuilder.parse(inputStream); + doc.getDocumentElement().normalize(); + return doc; + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + + private List parseForecasts(Document xmlDocument) { + NodeList cityNodes = xmlDocument.getElementsByTagName("localitate"); + List forecasts = new ArrayList<>(); + for (int i = 0; i < cityNodes.getLength(); i++) { + Node cityNode = cityNodes.item(i); + String city = cityNode.getAttributes().getNamedItem("nume").getNodeValue(); + + NodeList forecastNodes = cityNode.getChildNodes(); + for (int j = 0; j < forecastNodes.getLength(); j++) { + Node forecastNode = forecastNodes.item(j); + if (!"prognoza".equals(forecastNode.getNodeName())) { + continue; + } + + Forecast forecast = new Forecast(); + forecast.setCity(city); + + String dateValue = forecastNode.getAttributes().getNamedItem("data").getNodeValue(); + try { + forecast.setDate(simpleDateFormat.parse(dateValue)); + } catch (ParseException e) { + throw new IllegalArgumentException(e); + } + + NodeList forecastDetailsNodes = forecastNode.getChildNodes(); + for (int k = 0; k < forecastDetailsNodes.getLength(); k++) { + Node forecastDetailsNode = forecastDetailsNodes.item(k); + switch (forecastDetailsNode.getNodeName()) { + case "temp_min": + forecast.setTemperatureMin(Byte.valueOf(forecastDetailsNode.getTextContent())); + break; + case "temp_max": + forecast.setTemperatureMax(Byte.valueOf(forecastDetailsNode.getTextContent())); + break; + case "fenomen_descriere": + forecast.setDescription(forecastDetailsNode.getTextContent()); + break; + } + } + + forecasts.add(forecast); + } + } + return forecasts; + } + + @Entity(name = "Forecast") + public static class Forecast { + + @Id + @GeneratedValue + private Long id; + + private String city; + + @Temporal(TemporalType.DATE) + @Column(name = "forecast_date") + private Date date; + + @Column(name = "temperature_min") + private byte temperatureMin; + + @Column(name = "temperature_max") + private byte temperatureMax; + + private String description; + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public byte getTemperatureMin() { + return temperatureMin; + } + + public void setTemperatureMin(byte temperatureMin) { + this.temperatureMin = temperatureMin; + } + + public byte getTemperatureMax() { + return temperatureMax; + } + + public void setTemperatureMax(byte temperatureMax) { + this.temperatureMax = temperatureMax; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/SessionDoWorkTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/SessionDoWorkTest.java new file mode 100644 index 000000000..8a339b047 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/SessionDoWorkTest.java @@ -0,0 +1,98 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.Session; +import org.junit.Test; + +import java.net.SocketTimeoutException; +import java.sql.Connection; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class SessionDoWorkTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + private int connectionPoolSize = 4; + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testDoWork() { + Executor executor = Executors.newFixedThreadPool(connectionPoolSize); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + session.doWork(connection -> { + connection.setNetworkTimeout( + executor, + (int) TimeUnit.SECONDS.toMillis(1) + ); + }); + + try { + entityManager.createNativeQuery("select pg_sleep(2)").getResultList(); + } catch (Exception e) { + assertTrue(SocketTimeoutException.class.isInstance(ExceptionUtil.rootCause(e))); + } + }); + } + + @Test + public void testDoReturningWork() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + int isolationLevel = session.doReturningWork( + connection -> connection.getTransactionIsolation() + ); + + assertEquals(Connection.TRANSACTION_READ_COMMITTED, isolationLevel); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/UserSuppliedConnectionProviderTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/UserSuppliedConnectionProviderTest.java new file mode 100644 index 000000000..98dd507b5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/UserSuppliedConnectionProviderTest.java @@ -0,0 +1,93 @@ +package com.vladmihalcea.hpjp.hibernate.connection; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.Session; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class UserSuppliedConnectionProviderTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{Post.class}; + } + + protected Properties properties() { + Properties properties = new Properties(); + properties.put("hibernate.dialect", dataSourceProvider().hibernateDialect()); + properties.put("hibernate.boot.allow_jdbc_metadata_access", "false"); + return properties; + } + + @Test + public void testConnection() { + try(Connection connection = dataSource().getConnection()) { + executeStatement("create table post (id bigint not null, title varchar(255), primary key (id))"); + Session session = sessionFactory().withOptions().connection(connection).openSession(); + + session.getTransaction().begin(); + + int postCount = 5; + for (final AtomicLong i = new AtomicLong(); i.get() < postCount; i.incrementAndGet()) { + session.persist(new Post().setId(i.get())); + } + session.getTransaction().commit(); + + session.getTransaction().begin(); + Post post = session.find(Post.class, 1L); + post.setTitle("High-Performance Java Persistence"); + session.getTransaction().commit(); + + session.getTransaction().begin(); + Post _post = session.createQuery(""" + select p + from Post p + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + assertEquals("High-Performance Java Persistence", _post.getTitle()); + session.getTransaction().commit(); + } catch (SQLException e) { + fail(e.getMessage()); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/AtomikosJTAConnectionReleaseConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/AtomikosJTAConnectionReleaseConfiguration.java new file mode 100644 index 000000000..271bcb252 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/AtomikosJTAConnectionReleaseConfiguration.java @@ -0,0 +1,29 @@ +package com.vladmihalcea.hpjp.hibernate.connection.jta; + +import com.vladmihalcea.hpjp.util.spring.config.jta.PostgreSQLJTATransactionManagerConfiguration; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + +@Configuration +public class AtomikosJTAConnectionReleaseConfiguration extends PostgreSQLJTATransactionManagerConfiguration { + + @Override + protected Class configurationClass() { + return this.getClass(); + } + + @Override + protected Properties additionalProperties() { + Properties properties = super.additionalProperties(); + //properties.put("hibernate.generate_statistics", "true"); + //properties.put("hibernate.stats.factory", TransactionStatisticsFactory.class.getName()); + + properties.setProperty( AvailableSettings.CONNECTION_HANDLING, PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION.name()); + //properties.setProperty( AvailableSettings.CONNECTION_HANDLING, PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT.name()); + return properties; + } +} + diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/FlexyPoolEntities.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/FlexyPoolEntities.java similarity index 85% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/FlexyPoolEntities.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/FlexyPoolEntities.java index 4b7549f09..bc5354405 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/connection/jta/FlexyPoolEntities.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/FlexyPoolEntities.java @@ -1,6 +1,7 @@ -package com.vladmihalcea.book.hpjp.hibernate.connection.jta; +package com.vladmihalcea.hpjp.hibernate.connection.jta; + +import jakarta.persistence.*; -import javax.persistence.*; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -15,7 +16,8 @@ public class FlexyPoolEntities { public static class Post { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "hibernate_sequence") + @SequenceGenerator(name = "hibernate_sequence", allocationSize = 1) private Long id; private String title; @@ -94,7 +96,6 @@ public void removeDetails() { public static class PostDetails { @Id - @GeneratedValue private Long id; @Column(name = "created_on") @@ -108,7 +109,6 @@ public PostDetails() { } @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") @MapsId private Post post; @@ -150,7 +150,8 @@ public void setCreatedBy(String createdBy) { public static class PostComment { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "hibernate_sequence") + @SequenceGenerator(name = "hibernate_sequence", allocationSize = 1) private Long id; @ManyToOne @@ -194,7 +195,8 @@ public void setReview(String review) { public static class Tag { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "hibernate_sequence") + @SequenceGenerator(name = "hibernate_sequence", allocationSize = 1) private Long id; private String name; @@ -213,7 +215,8 @@ public void setName(String name) { public static class User { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "hibernate_sequence") + @SequenceGenerator(name = "hibernate_sequence", allocationSize = 1) private Long id; private String name; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/HandlingModeJTAConnectionReleaseConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/HandlingModeJTAConnectionReleaseConfiguration.java new file mode 100644 index 000000000..e3b68cff2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/HandlingModeJTAConnectionReleaseConfiguration.java @@ -0,0 +1,27 @@ +package com.vladmihalcea.hpjp.hibernate.connection.jta; + +import com.vladmihalcea.hpjp.hibernate.statistics.TransactionStatisticsFactory; +import com.vladmihalcea.hpjp.util.spring.config.jta.PostgreSQLJTATransactionManagerConfiguration; +import org.hibernate.cfg.AvailableSettings; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + +@Configuration +public class HandlingModeJTAConnectionReleaseConfiguration extends PostgreSQLJTATransactionManagerConfiguration { + + @Override + protected Class configurationClass() { + return this.getClass(); + } + + @Override + protected Properties additionalProperties() { + Properties properties = super.additionalProperties(); + properties.put("hibernate.generate_statistics", "true"); + properties.put("hibernate.stats.factory", TransactionStatisticsFactory.class.getName()); + + properties.setProperty( AvailableSettings.CONNECTION_HANDLING, "delayed_acquisition_and_release_after_transaction"); + return properties; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/HandlingModeJtaConnectionReleaseTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/HandlingModeJtaConnectionReleaseTest.java new file mode 100644 index 000000000..116994b49 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/HandlingModeJtaConnectionReleaseTest.java @@ -0,0 +1,62 @@ +package com.vladmihalcea.hpjp.hibernate.connection.jta; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.hibernate.Session; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertNotNull; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = HandlingModeJTAConnectionReleaseConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public class HandlingModeJtaConnectionReleaseTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private TransactionTemplate transactionTemplate; + + private int[] batches = {10, 50, 100, 500, 1000, 5000, 10000}; + + @Test + @Ignore + public void test() { + //Warming up + for (int i = 0; i < 100; i++) { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + assertNotNull(entityManager.createNativeQuery("select now()").getSingleResult()); + return null; + }); + } + for (int batch : batches) { + long startNanos = System.nanoTime(); + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + for (int i = 0; i < batch; i++) { + assertNotNull(entityManager.createNativeQuery("select now()").getSingleResult()); + } + return null; + }); + LOGGER.info("Transaction took {} millis", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + } + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + entityManager.unwrap(Session.class).getSessionFactory().getStatistics().logSummary(); + return null; + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/JTAConnectionReleaseTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/JTAConnectionReleaseTest.java new file mode 100644 index 000000000..8a9b3b6b7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/JTAConnectionReleaseTest.java @@ -0,0 +1,64 @@ +package com.vladmihalcea.hpjp.hibernate.connection.jta; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.hibernate.Session; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertNotNull; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = AtomikosJTAConnectionReleaseConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public class JTAConnectionReleaseTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private TransactionTemplate transactionTemplate; + + private int[] batches = {10, 50, 100, 500, 1000, 5000}; + + @Test + public void test() { + //Warming up + for (int i = 0; i < 1000; i++) { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + assertNotNull(entityManager.createNativeQuery("select now()").getSingleResult()); + return null; + }); + } + for (int batch : batches) { + long startNanos = System.nanoTime(); + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + for (int i = 0; i < batch; i++) { + assertNotNull(entityManager.createNativeQuery("select now()").getSingleResult()); + } + return null; + }); + LOGGER.info( + "Transaction took {} millis for {} statements", + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos), + batch + ); + } + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + entityManager.unwrap(Session.class).getSessionFactory().getStatistics().logSummary(); + return null; + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/JTAConnectionThreadBoundTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/JTAConnectionThreadBoundTest.java new file mode 100644 index 000000000..9bb078319 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/JTAConnectionThreadBoundTest.java @@ -0,0 +1,48 @@ +package com.vladmihalcea.hpjp.hibernate.connection.jta; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.hibernate.Session; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.sql.Connection; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = AtomikosJTAConnectionReleaseConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public class JTAConnectionThreadBoundTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private TransactionTemplate transactionTemplate; + + @Autowired + private DataSource dataSource; + + @Test + public void test() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Connection anotherConnection = dataSource.getConnection()) { + LOGGER.info("Connections got from JTA transactions are{} bound to thread", connection == anotherConnection ? "" : " not"); + } + }); + + return null; + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/NarayanaJTAConnectionReleaseConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/NarayanaJTAConnectionReleaseConfiguration.java new file mode 100644 index 000000000..af5e6c713 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/connection/jta/NarayanaJTAConnectionReleaseConfiguration.java @@ -0,0 +1,32 @@ +package com.vladmihalcea.hpjp.hibernate.connection.jta; + +import com.vladmihalcea.hpjp.spring.transaction.jta.narayana.config.NarayanaJTATransactionManagerConfiguration; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + +@Configuration +public class NarayanaJTAConnectionReleaseConfiguration extends NarayanaJTATransactionManagerConfiguration { + + protected String[] packagesToScan() { + return new String[]{ + this.getClass().getPackage().getName() + }; + } + + @Override + protected Properties additionalProperties() { + Properties properties = super.additionalProperties(); + properties.remove( + "hibernate.session_factory.statement_inspector" + ); + //properties.put("hibernate.generate_statistics", "true"); + //properties.put("hibernate.stats.factory", TransactionStatisticsFactory.class.getName()); + + //properties.setProperty( AvailableSettings.CONNECTION_HANDLING, PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION.name()); + properties.setProperty( AvailableSettings.CONNECTION_HANDLING, PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT.name()); + return properties; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/CriteriaBulkUpdateDeleteTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/CriteriaBulkUpdateDeleteTest.java new file mode 100644 index 000000000..a1816d826 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/CriteriaBulkUpdateDeleteTest.java @@ -0,0 +1,251 @@ +package com.vladmihalcea.hpjp.hibernate.criteria; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import jakarta.persistence.criteria.*; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class CriteriaBulkUpdateDeleteTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + }; + } + + @Test + public void testBulk() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setStatus(PostStatus.APPROVED) + ); + }); + + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(2L) + .setTitle("Spam title") + ); + + entityManager.persist( + new Post() + .setId(3L) + .setMessage("Spam message") + ); + + entityManager.persist( + new PostComment() + .setId(1L) + .setPost(entityManager.getReference(Post.class, 1L)) + .setMessage("Spam comment") + ); + }); + + doInJPA(entityManager -> { + assertEquals(2, flagSpam(entityManager, Post.class)); + assertEquals(1, flagSpam(entityManager, PostComment.class)); + }); + + doInJPA(entityManager -> { + assertEquals(2, + entityManager.createQuery(""" + update Post + set updatedOn = :timestamp + where status = :status + """) + .setParameter("timestamp", Timestamp.valueOf(LocalDateTime.now().minusDays(7))) + .setParameter("status", PostStatus.SPAM) + .executeUpdate() + ); + + assertEquals(1, + entityManager.createQuery(""" + update PostComment + set updatedOn = :timestamp + where status = :status + """) + .setParameter("timestamp", Timestamp.valueOf(LocalDateTime.now().minusDays(3))) + .setParameter("status", PostStatus.SPAM) + .executeUpdate() + ); + }); + + doInJPA(entityManager -> { + assertEquals(2, deleteSpam(entityManager, Post.class)); + assertEquals(1, deleteSpam(entityManager, PostComment.class)); + }); + } + + public int flagSpam(EntityManager entityManager, Class postModerateClass) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaUpdate update = builder.createCriteriaUpdate(postModerateClass); + + Root root = update.from(postModerateClass); + + Expression filterPredicate = + builder.like(builder.lower(root.get("message")), "%spam%"); + + if(Post.class.isAssignableFrom(postModerateClass)) { + filterPredicate = builder.or( + filterPredicate, + builder.like(builder.lower(root.get("title")), "%spam%") + ); + } + + update + .set(root.get("status"), PostStatus.SPAM) + .set(root.get("updatedOn"), new Date()) + .where(filterPredicate); + + return entityManager.createQuery(update).executeUpdate(); + } + + public int deleteSpam(EntityManager entityManager, Class postModerateClass) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaDelete delete = builder.createCriteriaDelete(postModerateClass); + + Root root = delete.from(postModerateClass); + + int daysValidityThreshold = (Post.class.isAssignableFrom(postModerateClass)) ? 7 : 3; + + delete + .where( + builder.and( + builder.equal(root.get("status"), PostStatus.SPAM), + builder.lessThanOrEqualTo(root.get("updatedOn"), Timestamp.valueOf(LocalDateTime.now().minusDays(daysValidityThreshold))) + ) + ); + + return entityManager.createQuery(delete).executeUpdate(); + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM + } + + @MappedSuperclass + public static abstract class PostModerate { + + @Enumerated(EnumType.ORDINAL) + @Column(columnDefinition = "tinyint") + private PostStatus status = PostStatus.PENDING; + + @Column(name = "updated_on") + private Date updatedOn = new Date(); + + public PostStatus getStatus() { + return status; + } + + public T setStatus(PostStatus status) { + this.status = status; + return (T) this; + } + + public Date getUpdatedOn() { + return updatedOn; + } + + public T setUpdatedOn(Date updatedOn) { + this.updatedOn = updatedOn; + return (T) this; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post extends PostModerate { + + @Id + private Long id; + + private String title; + + private String message; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public String getMessage() { + return message; + } + + public Post setMessage(String message) { + this.message = message; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment extends PostModerate { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String message; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getMessage() { + return message; + } + + public PostComment setMessage(String message) { + this.message = message; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/criteria/CriteriaCountGroupByTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/CriteriaCountGroupByTest.java similarity index 83% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/criteria/CriteriaCountGroupByTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/CriteriaCountGroupByTest.java index 8ec9a1665..10be7e155 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/criteria/CriteriaCountGroupByTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/CriteriaCountGroupByTest.java @@ -1,15 +1,15 @@ -package com.vladmihalcea.book.hpjp.hibernate.criteria; +package com.vladmihalcea.hpjp.hibernate.criteria; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.persistence.Tuple; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Root; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; import java.util.List; import static org.junit.Assert.assertEquals; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/criteria/TupleTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/TupleTest.java similarity index 77% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/criteria/TupleTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/TupleTest.java index 96a899a42..c437a889e 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/criteria/TupleTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/TupleTest.java @@ -1,14 +1,14 @@ -package com.vladmihalcea.book.hpjp.hibernate.criteria; +package com.vladmihalcea.hpjp.hibernate.criteria; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; import org.junit.Test; -import javax.persistence.Tuple; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Path; -import javax.persistence.criteria.Root; +import jakarta.persistence.Tuple; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Root; import java.util.List; /** diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/BlazePersistenceCriteriaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/BlazePersistenceCriteriaTest.java new file mode 100644 index 000000000..bf6e3484b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/BlazePersistenceCriteriaTest.java @@ -0,0 +1,456 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze; + +import com.blazebit.persistence.CTE; +import com.blazebit.persistence.Criteria; +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.JoinType; +import com.blazebit.persistence.spi.CriteriaBuilderConfiguration; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import io.hypersistence.utils.hibernate.query.ListResultTransformer; +import jakarta.persistence.*; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.ParameterExpression; +import jakarta.persistence.criteria.Root; +import org.hibernate.Session; +import org.hibernate.query.NativeQuery; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.junit.Test; + +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BlazePersistenceCriteriaTest extends AbstractTest { + + private CriteriaBuilderFactory cbf; + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostDetails.class, + PostComment.class, + Tag.class, + PostCommentCountCTE.class, + LatestPostCommentCTE.class, + PostCommentMaxIdCTE.class + }; + } + + @Override + protected EntityManagerFactory newEntityManagerFactory() { + EntityManagerFactory entityManagerFactory = super.newEntityManagerFactory(); + CriteriaBuilderConfiguration config = Criteria.getDefault(); + cbf = config.createCriteriaBuilderFactory(entityManagerFactory); + return entityManagerFactory; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("A great reference book.") + ) + .addComment( + new PostComment() + .setId(3L) + .setReview("A must-read for every Java developer!") + ); + + entityManager.persist(post); + + entityManager.persist( + new PostDetails() + .setPost(post) + .setCreatedBy("Vlad Mihalcea") + ); + + Tag java = new Tag().setName("Java"); + Tag hibernate = new Tag().setName("Hibernate"); + + entityManager.persist(java); + entityManager.persist(hibernate); + + post.getTags().add(java); + post.getTags().add(hibernate); + }); + } + + @Test + public void testLateralJoinBlaze() { + doInJPA(entityManager -> { + List tuples = entityManager.createNativeQuery(""" + SELECT + p.id AS "p.id", + p.title AS "p.title", + pc3.latest_comment_id AS "pc.id", + pc3.latest_comment_review AS "pc.review" + FROM + post p, + LATERAL ( + SELECT + pc2.post_comment_id AS latest_comment_id, + pc2.post_comment_review AS latest_comment_review + FROM ( + SELECT + pc1.id AS post_comment_id, + pc1.review AS post_comment_review, + pc1.post_id AS post_comment_post_id, + MAX(pc1.id) OVER (PARTITION BY pc1.post_id) AS max_post_comment_id + FROM post_comment pc1 + ) pc2 + WHERE + pc2.post_comment_id = pc2.max_post_comment_id AND + pc2.post_comment_post_id = p.id + ) pc3 + """, Tuple.class) + .unwrap(NativeQuery.class) + .setResultTransformer(new ListResultTransformer() { + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + return new PostComment() + .setId(((Number) tuple[2]).longValue()) + .setReview((String) tuple[3]) + .setPost( + new Post() + .setId(((Number) tuple[0]).longValue()) + .setTitle((String) tuple[1]) + ); + } + }) + .getResultList(); + + assertEquals(1, tuples.size()); + }); + } + + @Test + public void testDerivedTableJoinBlaze() { + doInJPA(entityManager -> { + List tuples = entityManager.createNativeQuery(""" + SELECT + p.id AS post_id, + p.title AS post_title, + pc2.review AS comment_review + FROM ( + SELECT + pc1.id AS id, + pc1.review AS review, + pc1.post_id AS post_id, + MAX(pc1.id) OVER (PARTITION BY pc1.post_id) AS max_id + FROM post_comment pc1 + ) pc2 + JOIN post p ON p.id = pc2.post_id + WHERE + pc2.id = pc2.max_id + """, Tuple.class) + .getResultList(); + + assertEquals(1, tuples.size()); + Tuple tuple = tuples.get(0); + assertEquals(1L, longValue(tuple.get("post_id"))); + assertEquals("High-Performance Java Persistence", tuple.get("post_title")); + assertEquals("A must-read for every Java developer!", tuple.get("comment_review")); + }); + + doInJPA(entityManager -> { + List tuples = cbf + .create(entityManager, Tuple.class) + .fromSubquery(PostCommentMaxIdCTE.class, "pc2") + .from(PostComment.class, "pc1") + .bind("id").select("pc1.id") + .bind("review").select("pc1.review") + .bind("postId").select("pc1.post.id") + .bind("maxId").select("MAX(pc1.id) OVER (PARTITION BY pc1.post.id)") + .end() + .joinOn(Post.class, "p", JoinType.INNER).onExpression("p.id = pc2.postId").end() + .where("pc2.id").eqExpression("pc2.maxId") + .select("p.id", "post_id") + .select("p.title", "post_title") + .select("pc2.review", "comment_review") + .getResultList(); + + assertEquals(1, tuples.size()); + Tuple tuple = tuples.get(0); + assertEquals(1L, longValue(tuple.get("post_id"))); + assertEquals("High-Performance Java Persistence", tuple.get("post_title")); + assertEquals("A must-read for every Java developer!", tuple.get("comment_review")); + }); + } + + @Test + public void testGroupBy() { + doInJPA(entityManager -> { + List tuples = entityManager + .createNativeQuery(""" + select + p.title as post_title, + count(pc.id) as comment_count + from post p + left join post_comment pc on pc.post_id = p.id + join post_details pd on p.id = pd.id + where pd.created_by = :createdBy + group by p.title + """, Tuple.class) + .setParameter("createdBy", "Vlad Mihalcea") + .getResultList(); + + assertEquals(1, tuples.size()); + }); + + doInJPA(entityManager -> { + List tuples = cbf + .create(entityManager, Tuple.class) + .from(Post.class, "p") + .leftJoinOn(PostComment.class, "pc").onExpression("pc.post = p").end() + .joinOn(PostDetails.class, "pd", JoinType.INNER).onExpression("pd.post = p").end() + .where("pd.createdBy").eqExpression(":createdBy") + .groupBy("p.title") + .select("p.title", "post_title") + .select("count(pc.id)", "comment_count") + .setParameter("createdBy", "Vlad Mihalcea") + .getResultList(); + + assertEquals(1, tuples.size()); + }); + } + + @Test + public void testJoinGroupBy() { + doInJPA(entityManager -> { + List tuples = entityManager + .createNativeQuery(""" + select + p1.title, + p_c.comment_count + from post p1 + join ( + select + p.title as post_title, + count(pc.id) as comment_count + from post p + left join post_comment pc on pc.post_id = p.id + join post_details pd on p.id = pd.id + where pd.created_by = :createdBy + group by p.title + ) p_c on p1.title = p_c.post_title + """, Tuple.class) + .setParameter("createdBy", "Vlad Mihalcea") + .getResultList(); + + assertEquals(1, tuples.size()); + }); + + doInJPA(entityManager -> { + List tuples = cbf + .create(entityManager, Tuple.class) + .from(Post.class, "p1") + .leftJoinOnSubquery(PostCommentCountCTE.class, "p_c") + .from(Post.class, "p") + .bind("id").select("p.id") + .bind("postTitle").select("p.title") + .bind("commentCount").select("count(pc.id)") + .leftJoinOn(PostComment.class, "pc").onExpression("pc.post = p").end() + .joinOn(PostDetails.class, "pd", JoinType.INNER).onExpression("pd.post = p").end() + .where("pd.createdBy").eqExpression(":createdBy") + .groupBy("p.title", "p.id") + .end() + .onExpression("p_c.id = p1.id") + .end() + .select("p1.title", "post_title") + .select("p_c.commentCount", "comment_count") + .setParameter("createdBy", "Vlad Mihalcea") + .getResultList(); + + assertEquals(1, tuples.size()); + }); + } + + @Test + public void testCriteriaAPIAlternative() { + final int maxCount = 50; + final String titlePattern = "High-Performance Java Persistence"; + + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteria = builder.createQuery(Post.class); + Root post = criteria.from(Post.class); + ParameterExpression parameterExpression = builder.parameter(String.class); + List posts = entityManager.createQuery( + criteria + .where(builder.like(post.get(Post_.TITLE), parameterExpression)) + .orderBy(builder.asc(post.get(Post_.ID))) + ) + .setParameter(parameterExpression, titlePattern) + .setMaxResults(maxCount) + .getResultList(); + + assertEquals(1, posts.size()); + }); + + doInJPA(entityManager -> { + List tuples = cbf.create(entityManager, Post.class) + .from(Post.class, "p") + .where(Post_.TITLE).like().expression(":titlePattern").noEscape() + .orderBy(Post_.ID, true) + .setParameter("titlePattern", titlePattern) + .setMaxResults(maxCount) + .getResultList(); + + assertEquals(1, tuples.size()); + }); + } + + @Test + public void testHibernateCriteriaAPI() { + final int maxCount = 50; + final String titlePattern = "High-Performance Java Persistence"; + + doInJPA(entityManager -> { + HibernateCriteriaBuilder builder = entityManager + .unwrap(Session.class) + .getCriteriaBuilder(); + + CriteriaQuery criteria = builder.createQuery(Post.class); + Root post = criteria.from(Post.class); + ParameterExpression parameterExpression = builder.parameter(String.class); + List posts = entityManager.createQuery( + criteria + .where(builder.ilike(post.get(Post_.TITLE), parameterExpression)) + .orderBy(builder.asc(post.get(Post_.ID))) + ) + .setParameter(parameterExpression, titlePattern) + .setMaxResults(maxCount) + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + @CTE + @Entity + public static class PostCommentMaxIdCTE { + @Id + private Long id; + private String review; + private Long postId; + private Long maxId; + } + + @CTE + @Entity + public static class LatestPostCommentCTE { + @Id + private Long id; + private String review; + } + + @CTE + @Entity + public static class PostCommentCountCTE { + @Id + private Long id; + private String postTitle; + private Long commentCount; + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + public PostDetails() { + createdOn = new Date(); + } + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + private String name; + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/Post.java new file mode 100644 index 000000000..0bf187333 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/Post.java @@ -0,0 +1,94 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * The {@link com.vladmihalcea.hpjp.hibernate.criteria.blaze.Post} + * + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true, fetch = FetchType.LAZY) + private BlazePersistenceCriteriaTest.PostDetails details; + + @ManyToMany + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + private Post setComments(List comments) { + this.comments = comments; + return this; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + + return this; + } + + public Post addDetails(BlazePersistenceCriteriaTest.PostDetails details) { + this.details = details; + details.setPost(this); + + return this; + } + + public Post removeDetails() { + this.details.setPost(null); + this.details = null; + + return this; + } + + public List getTags() { + return tags; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/PostComment.java new file mode 100644 index 000000000..cf267d02d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/PostComment.java @@ -0,0 +1,61 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze; + +import jakarta.persistence.*; + +/** + * The {@link com.vladmihalcea.hpjp.hibernate.criteria.blaze.PostComment} + * + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostComment)) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/BlazePersistenceBulkUpdateDeleteTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/BlazePersistenceBulkUpdateDeleteTest.java new file mode 100644 index 000000000..5bad5177d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/BlazePersistenceBulkUpdateDeleteTest.java @@ -0,0 +1,157 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze.bulk; + +import com.blazebit.persistence.*; +import com.blazebit.persistence.spi.CriteriaBuilderConfiguration; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BlazePersistenceBulkUpdateDeleteTest extends AbstractTest { + + private CriteriaBuilderFactory cbf; + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected EntityManagerFactory newEntityManagerFactory() { + EntityManagerFactory entityManagerFactory = super.newEntityManagerFactory(); + CriteriaBuilderConfiguration config = Criteria.getDefault(); + cbf = config.createCriteriaBuilderFactory(entityManagerFactory); + return entityManagerFactory; + } + + @Test + public void testBulk() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setStatus(PostStatus.APPROVED) + ); + + entityManager.persist( + new Post() + .setId(2L) + .setTitle("Spam title") + ); + + entityManager.persist( + new Post() + .setId(3L) + .setMessage("Spam message") + ); + + entityManager.persist( + new PostComment() + .setId(1L) + .setPost(entityManager.getReference(Post.class, 1L)) + .setMessage("Spam comment") + ); + }); + + doInJPA(entityManager -> { + assertEquals(2, flagSpam(entityManager, Post.class)); + assertEquals(1, flagSpam(entityManager, PostComment.class)); + }); + + doInJPA(entityManager -> { + assertEquals(2, + entityManager.createQuery(""" + update Post + set updatedOn = :timestamp + where status = :status + """) + .setParameter("timestamp", Timestamp.valueOf(LocalDateTime.now().minusDays(7))) + .setParameter("status", PostStatus.SPAM) + .executeUpdate() + ); + + assertEquals(1, + entityManager.createQuery(""" + update PostComment + set updatedOn = :timestamp + where status = :status + """) + .setParameter("timestamp", Timestamp.valueOf(LocalDateTime.now().minusDays(3))) + .setParameter("status", PostStatus.SPAM) + .executeUpdate() + ); + }); + + doInJPA(entityManager -> { + assertEquals(2, deleteSpam(entityManager, Post.class)); + assertEquals(1, deleteSpam(entityManager, PostComment.class)); + }); + } + + public int flagSpam( + EntityManager entityManager, + Class postModerateClass) { + + UpdateCriteriaBuilder builder = cbf + .update(entityManager, postModerateClass) + .set(PostModerate_.STATUS, PostStatus.SPAM) + .set(PostModerate_.UPDATED_ON, new Date()); + + String spamToken = "%spam%"; + + if(Post.class.isAssignableFrom(postModerateClass)) { + builder + .whereOr() + .where(lower(Post_.MESSAGE)) + .like().value(spamToken).noEscape() + .where(lower(Post_.TITLE)) + .like().value(spamToken).noEscape() + .endOr(); + } else if(PostComment.class.isAssignableFrom(postModerateClass)) { + builder + .where(lower(PostComment_.MESSAGE)) + .like().value(spamToken).noEscape(); + } + + return builder.executeUpdate(); + } + + public int deleteSpam( + EntityManager entityManager, + Class postModerateClass) { + + return cbf + .delete(entityManager, postModerateClass) + .where(PostModerate_.STATUS).eq(PostStatus.SPAM) + .where(PostModerate_.UPDATED_ON).le( + Timestamp.valueOf( + LocalDateTime.now().minusDays( + (Post.class.isAssignableFrom(postModerateClass)) ? 7 : 3 + ) + ) + ) + .executeUpdate(); + } + + private static String lower(String property) { + return String.format("lower(%s)", property); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/Post.java new file mode 100644 index 000000000..4d3e12566 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/Post.java @@ -0,0 +1,47 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze.bulk; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post extends PostModerate { + + @Id + private Long id; + + private String title; + + private String message; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public String getMessage() { + return message; + } + + public Post setMessage(String message) { + this.message = message; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/PostComment.java new file mode 100644 index 000000000..16c37fab7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/PostComment.java @@ -0,0 +1,46 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze.bulk; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment extends PostModerate { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String message; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getMessage() { + return message; + } + + public PostComment setMessage(String message) { + this.message = message; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/PostModerate.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/PostModerate.java new file mode 100644 index 000000000..e5cc55025 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/PostModerate.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze.bulk; + +import jakarta.persistence.Column; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.MappedSuperclass; +import java.util.Date; + +/** + * @author Vlad Mihalcea + */ +@MappedSuperclass +public abstract class PostModerate { + + @Enumerated(EnumType.ORDINAL) + @Column(columnDefinition = "tinyint") + private PostStatus status = PostStatus.PENDING; + + @Column(name = "updated_on") + private Date updatedOn = new Date(); + + public PostStatus getStatus() { + return status; + } + + public T setStatus(PostStatus status) { + this.status = status; + return (T) this; + } + + public Date getUpdatedOn() { + return updatedOn; + } + + public T setUpdatedOn(Date updatedOn) { + this.updatedOn = updatedOn; + return (T) this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/PostStatus.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/PostStatus.java new file mode 100644 index 000000000..567b0ea88 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/bulk/PostStatus.java @@ -0,0 +1,10 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze.bulk; + +/** + * @author Vlad Mihalcea + */ +public enum PostStatus { + PENDING, + APPROVED, + SPAM +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/AbstractEntity.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/AbstractEntity.java new file mode 100644 index 000000000..ef1361081 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/AbstractEntity.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze.tab; + +import org.springframework.lang.Nullable; + +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PostLoad; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Transient; +import java.io.Serializable; + +@MappedSuperclass +public abstract class AbstractEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Transient + private boolean isNew = true; + + public boolean isNew() { + return isNew; + } + + @Nullable + public abstract I getId(); + + @PrePersist + @PostLoad + void markNotNew() { + this.isNew = false; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/BlazePersistenceTabInstanceTest-new.sql b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/BlazePersistenceTabInstanceTest-new.sql new file mode 100644 index 000000000..e4c10e37b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/BlazePersistenceTabInstanceTest-new.sql @@ -0,0 +1,52 @@ +SELECT tabinstanc0_.tab_key AS tab_key1_0_, + tabinstanc0_.tab_ver AS tab_ver2_0_, + tabinstanc0_.tab_acronym AS tab_acronym3_0_, + tabinstanc0_.tab_additional_data AS tab_additional_dat4_0_, + tabinstanc0_.tab_additional_data_number AS tab_additional_dat5_0_ +FROM tab_instance tabinstanc0_ +INNER JOIN tab_object tabobject1_ ON tabinstanc0_.tab_key = tabobject1_.tab_key +INNER JOIN ( + SELECT + NULL tabKey, + NULL tabVer + FROM dual + WHERE 1 = 0 + UNION ALL ( + SELECT + tabobject0_.tab_key AS col_0_0_, + nvl(tabinstanc1_.tab_ver, tabkeyver4_.tabVer) AS col_1_0_ + FROM tab_object tabobject0_ + LEFT OUTER JOIN tab_instance tabinstanc1_ ON tabobject0_.tab_key = tabinstanc1_.tab_key + INNER JOIN tab_version tabversion2_ ON tabinstanc1_.tab_ver = tabversion2_.tab_key + INNER JOIN tab_source tabsource3_ + ON (tabversion2_.tab_source = tabsource3_.tab_key + AND tabsource3_.tab_acronym = ? + AND (tabinstanc1_.tab_ver in (?, ?, ?)) + ) + LEFT OUTER JOIN ( + SELECT + NULL tabKey, + NULL tabVer + FROM dual + WHERE 1 = 0 + UNION ALL ( + SELECT + tabinstanc0_.tab_key AS col_0_0_, + max(tabinstanc0_.tab_ver) AS col_1_0_ + FROM tab_instance tabinstanc0_ + INNER JOIN tab_version tabversion1_ ON tabinstanc0_.tab_ver = tabversion1_.tab_key + INNER JOIN tab_source tabsource2_ ON tabversion1_.tab_source = tabsource2_.tab_key + WHERE tabsource2_.tab_acronym <> ? + AND (tabinstanc0_.tab_ver in (?, ?, ?)) + GROUP BY tabinstanc0_.tab_key) + ) tabkeyver4_ + ON ((NULL IS NULL) + AND tabkeyver4_.tabKey = tabobject0_.tab_key) + WHERE tabinstanc1_.tab_ver IS NOT NULL + OR tabkeyver4_.tabVer IS NOT NULL + ) +) tabkeyver2_ ON ((NULL IS NULL) + AND tabinstanc0_.tab_ver = tabkeyver2_.tabVer + AND tabinstanc0_.tab_key = tabkeyver2_.tabKey) +WHERE tabinstanc0_.tab_ver in (?, ?, ?) +order by tabobject1_.tab_acronym ASC \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/BlazePersistenceTabInstanceTest-old.sql b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/BlazePersistenceTabInstanceTest-old.sql new file mode 100644 index 000000000..491b32955 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/BlazePersistenceTabInstanceTest-old.sql @@ -0,0 +1,36 @@ +SELECT + tabinstanc0_.tab_key AS tab_key1_0_, + tabinstanc0_.tab_ver AS tab_ver2_0_, + tabinstanc0_.tab_acronym AS tab_acronym3_0_, + tabinstanc0_.tab_additional_data AS tab_additional_dat4_0_, + tabinstanc0_.tab_additional_data_number AS tab_additional_dat5_0_ +FROM tab_instance tabinstanc0_ +INNER JOIN tab_object tabobject1_ ON tabinstanc0_.tab_key = tabobject1_.tab_key +JOIN ( + SELECT + o.tab_key AS tab_key, + nvl(bf.tab_ver, a.tab_ver) AS tab_ver + FROM tab_object o + LEFT OUTER JOIN tab_instance bf ON bf.tab_key = o.tab_key + JOIN tab_version vf ON bf.tab_ver = vf.tab_key + JOIN tab_source df + ON vf.tab_source = df.tab_key + AND df.tab_acronym = 'Central' + AND bf.tab_ver IN (3, 4, 5) + LEFT OUTER JOIN ( + SELECT + ba.tab_key AS tab_key, + max(ba.tab_ver) AS tab_ver + FROM tab_instance ba + JOIN tab_version va ON ba.tab_ver = va.tab_key + JOIN tab_source da ON va.tab_source = da.tab_key + WHERE da.tab_acronym != 'Central' + AND ba.tab_ver IN (3, 4, 5) + GROUP BY ba.tab_key + ) a ON a.tab_key = o.tab_key + WHERE bf.tab_ver IS NOT NULL + OR a.tab_ver IS NOT NULL +) o2 ON tabinstanc0_.tab_ver = o2.tab_ver + AND tabinstanc0_.tab_key = o2.tab_key +WHERE tabinstanc0_.tab_ver IN (3, 4, 5) +ORDER BY tabobject1_.tab_acronym ASC \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/BlazePersistenceTabInstanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/BlazePersistenceTabInstanceTest.java new file mode 100644 index 000000000..47de997a8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/BlazePersistenceTabInstanceTest.java @@ -0,0 +1,307 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze.tab; + +import com.blazebit.persistence.Criteria; +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.spi.CriteriaBuilderConfiguration; +import com.vladmihalcea.hpjp.hibernate.criteria.blaze.tab.cte.TabKeyVer; +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import org.junit.Before; +import org.junit.Test; + +import jakarta.persistence.EntityManagerFactory; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class BlazePersistenceTabInstanceTest extends AbstractOracleIntegrationTest { + + private CriteriaBuilderFactory cbf; + + @Override + protected Class[] entities() { + return new Class[]{ + TabInstance.class, + TabObject.class, + TabSource.class, + TabVersion.class, + TabKeyVer.class + }; + } + + @Override + protected EntityManagerFactory newEntityManagerFactory() { + EntityManagerFactory entityManagerFactory = super.newEntityManagerFactory(); + CriteriaBuilderConfiguration config = Criteria.getDefault(); + cbf = config.createCriteriaBuilderFactory(entityManagerFactory); + return entityManagerFactory; + } + + @Before + public void init() { + super.init(); + executeStatement("DROP table tab_instance"); + executeStatement("DROP table tab_object"); + executeStatement("DROP table tab_version"); + executeStatement("DROP table tab_source"); + + executeStatement(""" + CREATE TABLE tab_source ( + tab_key INTEGER, + tab_acronym VARCHAR(10), + PRIMARY KEY (tab_key) + ) + """); + executeStatement(""" + CREATE TABLE tab_object ( + tab_key INTEGER, + tab_acronym VARCHAR(10), + PRIMARY KEY (tab_key) + ) + """); + executeStatement(""" + CREATE TABLE tab_version ( + tab_key INTEGER, + tab_source INTEGER, + tab_time_stamp INTEGER, + tab_acronym VARCHAR(10), + PRIMARY KEY (tab_key), + FOREIGN KEY (tab_source) REFERENCES tab_source(tab_key) + ) + """); + executeStatement(""" + CREATE TABLE tab_instance ( + tab_key INTEGER, + tab_ver INTEGER, + tab_acronym VARCHAR(50), + tab_additional_data VARCHAR(50), + tab_additional_data_number NUMBER(10), + PRIMARY KEY (tab_key, tab_ver), + FOREIGN KEY (tab_key) REFERENCES tab_object(tab_key), + FOREIGN KEY (tab_ver) REFERENCES tab_version(tab_key) + ) + """); + + insertData(); + } + + private void insertData() { + executeStatement("INSERT INTO tab_source VALUES (983, 'Region 1')"); + executeStatement("INSERT INTO tab_source VALUES (984, 'Central')"); + executeStatement("INSERT INTO tab_source VALUES (985, 'Region 2')"); + + executeStatement("INSERT INTO tab_version VALUES (1, 983, 20, 'R1.1')"); + executeStatement("INSERT INTO tab_version VALUES (2, 983, 21, 'R1.2')"); + executeStatement("INSERT INTO tab_version VALUES (3, 985, 22, 'R2.1')"); + executeStatement("INSERT INTO tab_version VALUES (4, 983, 23, 'R1.3')"); + executeStatement("INSERT INTO tab_version VALUES (5, 984, 24, 'Central')"); + executeStatement("INSERT INTO tab_version VALUES (6, 983, 25, 'R1.4')"); + executeStatement("INSERT INTO tab_version VALUES (7, 985, 26, 'R2.2')"); + executeStatement("INSERT INTO tab_version VALUES (8, 984, 27, 'Central')"); + executeStatement("INSERT INTO tab_version VALUES (9, 985, 28, 'R2.3')"); + + executeStatement("INSERT INTO tab_object VALUES (100, 'Object 1')"); + executeStatement("INSERT INTO tab_object VALUES (200, 'Object 2')"); + executeStatement("INSERT INTO tab_object VALUES (300, 'Object 3')"); + + executeStatement("INSERT INTO tab_instance VALUES (100, 3, 'O1 [R2.1] - R1/R2/C --> C', 'AD1', 1050)"); + executeStatement("INSERT INTO tab_instance VALUES (100, 4, 'O1 [R1.3] - R1/R2/C --> C', 'AD1', 1051)"); + executeStatement("INSERT INTO tab_instance VALUES (100, 5, 'O1 [C] - R1/R2/C --> C', 'AD2', 1050)"); + executeStatement("INSERT INTO tab_instance VALUES (200, 5, 'O2 [C] - R1/C --> C', 'AD4', 1250)"); + executeStatement("INSERT INTO tab_instance VALUES (200, 3, 'O2 [R2.1] - R1/C --> C', 'AD4', 1250)"); + executeStatement("INSERT INTO tab_instance VALUES (300, 4, 'O3 [R1.3] - R1/R2 --> R1', 'AD8', 1300)"); + executeStatement("INSERT INTO tab_instance VALUES (300, 3, 'O3 [R2.1] - R1/R2 --> R1', 'AD9', 1301)"); + } + + @Test + public void testTwoLevelLeftJoinSubqueryWithGroupByQuery() { + doInJPA(entityManager -> { + /* + SELECT + tabinstanc0_.tab_key AS tab_key1_0_, + tabinstanc0_.tab_ver AS tab_ver2_0_, + tabinstanc0_.tab_acronym AS tab_acronym3_0_, + tabinstanc0_.tab_additional_data AS tab_additional_dat4_0_, + tabinstanc0_.tab_additional_data_number AS tab_additional_dat5_0_ + FROM tab_instance tabinstanc0_ + INNER JOIN tab_object tabobject1_ ON tabinstanc0_.tab_key = tabobject1_.tab_key + JOIN ( + SELECT + o.tab_key AS tab_key, + coalesce(bf.tab_ver, a.tab_ver) AS tab_ver + FROM tab_object o + LEFT OUTER JOIN tab_instance bf ON bf.tab_key = o.tab_key + JOIN tab_version vf ON bf.tab_ver = vf.tab_key + JOIN tab_source df ON vf.tab_source = df.tab_key + AND df.tab_acronym = 'Central' + AND bf.tab_ver IN (3, 4, 5) + LEFT OUTER JOIN ( + SELECT + ba.tab_key AS tab_key, + max(ba.tab_ver) AS tab_ver + FROM tab_instance ba + JOIN tab_version va ON ba.tab_ver = va.tab_key + JOIN tab_source da ON va.tab_source = da.tab_key + WHERE da.tab_acronym != 'Central' + AND ba.tab_ver IN (3, 4, 5) + GROUP BY ba.tab_key + ) a ON a.tab_key = o.tab_key + WHERE bf.tab_ver IS NOT NULL + OR a.tab_ver IS NOT NULL + ) o2 ON tabinstanc0_.tab_ver = o2.tab_ver + AND tabinstanc0_.tab_key = o2.tab_key + WHERE tabinstanc0_.tab_ver IN (3, 4, 5) + ORDER BY tabobject1_.tab_acronym ASC + */ + + List tabVer = List.of(3L, 4L, 5L); + + List tabInstances = cbf.create(entityManager, TabInstance.class) + .from(TabInstance.class, "tabinstanc0_") + .innerJoin("tabinstanc0_.tabObject", "tabobject1_") + .innerJoinOnSubquery(TabKeyVer.class, "o2") + .from(TabObject.class, "o") + .bind("tabKey").select("o.tabKey") + .bind("tabVer").select("coalesce(bf.tabVersion.tabKey, a.tabVer)") + .leftJoin("o.tabInstances", "bf") + .innerJoin("bf.tabVersion", "vf") + .innerJoinOn(TabSource.class, "df") + .onExpression("vf.tabSource = df") + .on("df.tabAcronym").eqExpression(":tabAcronym") + .on("bf.id.tabVer").in(tabVer) + .end() + .leftJoinOnSubquery(TabKeyVer.class, "a") + .from(TabInstance.class, "ba") + .bind("tabKey").select("ba.tabObject.tabKey") + .bind("tabVer").select("max(ba.id.tabVer)") + .innerJoin("ba.tabVersion", "va") + .innerJoin("va.tabSource", "da") + .where("da.tabAcronym").notEqExpression(":tabAcronym") + .where("ba.id.tabVer").in(tabVer) + .groupBy("ba.tabObject.tabKey") + .end() + .onExpression("a.tabKey = o.tabKey") + .end() + .whereOr() + .where("bf.id.tabVer").isNotNull() + .where("a.tabVer").isNotNull() + .endOr() + .end() + .onExpression("tabinstanc0_.id.tabVer = o2.tabVer") + .onExpression("tabinstanc0_.id.tabKey = o2.tabKey") + .end() + .where("tabinstanc0_.id.tabVer").in(tabVer) + .orderByAsc("tabobject1_.tabAcronym") + .setParameter("tabAcronym", "Central") + .getResultList(); + + assertEquals(2, tabInstances.size()); + assertEquals("O1 [C] - R1/R2/C --> C", tabInstances.get(0).getTabAcronym()); + assertEquals("O2 [C] - R1/C --> C", tabInstances.get(1).getTabAcronym()); + /*assertThat(tabInstances).extracting(TabInstance::getTabAcronym) + .containsExactlyInAnyOrder("O1 [C] - R1/R2/C --> C", "O2 [C] - R1/C --> C", "O3 [R1.3] - R1/R2 --> R1"); + */ + }); + } + + @Test + public void testLeftJoinWithGroupByQuery() { + doInJPA(entityManager -> { + /* + * SELECT + * o.tab_key AS tab_key, + * coalesce(bf.tab_ver, a.tab_ver) AS tab_ver + * FROM tab_object o + * LEFT OUTER JOIN tab_instance bf ON bf.tab_key = o.tab_key + * JOIN tab_version vf ON bf.tab_ver = vf.tab_key + * JOIN tab_source df ON vf.tab_source = df.tab_key + * AND df.tab_acronym = 'Central' + * AND bf.tab_ver IN (3, 4, 5) + * LEFT OUTER JOIN ( + * SELECT + * ba.tab_key AS tab_key, + * max(ba.tab_ver) AS tab_ver + * FROM tab_instance ba + * JOIN tab_version va ON ba.tab_ver = va.tab_key + * JOIN tab_source da ON va.tab_source = da.tab_key + * WHERE da.tab_acronym != 'Central' + * AND ba.tab_ver IN (3, 4, 5) + * GROUP BY ba.tab_key + * ) a ON a.tab_key = o.tab_key + * WHERE bf.tab_ver IS NOT NULL + * OR a.tab_ver IS NOT NULL + */ + + List tabVer = List.of(3L, 4L, 5L); + + List tabObjects = cbf.create(entityManager, TabObject.class) + .from(TabObject.class, "o") + .leftJoin("o.tabInstances", "bf") + .innerJoin("bf.tabVersion", "vf") + .innerJoinOn(TabSource.class, "df") + .onExpression("vf.tabSource = df") + .on("df.tabAcronym").eqExpression(":tabAcronym") + .on("bf.id.tabVer").in(tabVer) + .end() + .leftJoinOnSubquery(TabKeyVer.class, "a") + .from(TabInstance.class, "ba") + .bind("tabKey").select("ba.tabObject.tabKey") + .bind("tabVer").select("max(ba.id.tabVer)") + .innerJoin("ba.tabVersion", "va") + .innerJoin("va.tabSource", "da") + .where("da.tabAcronym").notEqExpression(":tabAcronym") + .where("ba.id.tabVer").in(tabVer) + .groupBy("ba.tabObject.tabKey") + .end() + .onExpression("a.tabKey = o.tabKey") + .end() + .whereOr() + .where("bf.id.tabVer").isNotNull() + .where("a.tabVer").isNotNull() + .endOr() + .select("o.tabKey", "tabKey") + .select("coalesce(bf.tabVersion.tabKey, a.tabVer)", "tabVer") + .setParameter("tabAcronym", "Central") + .getResultList(); + + assertTrue(tabObjects.size() > 0); + }); + } + + @Test + public void testGroupByQuery() { + doInJPA(entityManager -> { + /* + * SELECT + * ba.tab_key AS tab_key, + * max(ba.tab_ver) AS tab_ver + * FROM tab_instance ba + * JOIN tab_version va ON ba.tab_ver = va.tab_key + * JOIN tab_source da ON va.tab_source = da.tab_key + * WHERE da.tab_acronym != 'Central' + * AND ba.tab_ver IN (3, 4, 5) + * GROUP BY ba.tab_key + */ + + List tabVer = List.of(3L, 4L, 5L); + + List tabInstances = cbf.create(entityManager, TabInstance.class) + .from(TabInstance.class, "ba") + .innerJoin("ba.tabVersion", "va") + .innerJoin("va.tabSource", "da") + .where("da.tabAcronym").notEqExpression(":tabAcronym") + .where("ba.id.tabVer").in(tabVer) + .groupBy("ba.tabObject.tabKey") + .select("ba.tabObject.tabKey", "tab_key") + .select("max(ba.id.tabVer)", "tab_ver") + .setParameter("tabAcronym", "Central") + .getResultList(); + + assertTrue(tabInstances.size() > 0); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabInstance.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabInstance.java new file mode 100644 index 000000000..e599ba9bd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabInstance.java @@ -0,0 +1,82 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze.tab; + +import jakarta.persistence.*; +import java.math.BigDecimal; + +@Entity +@Table(name = "tab_instance") +public class TabInstance extends AbstractEntity { + + private static final long serialVersionUID = 1L; + + @EmbeddedId + private TabInstancePK id; + + @Column(name = "tab_acronym", nullable = false, length = 50) + private String tabAcronym; + + @Column(name = "tab_additional_data", nullable = false, length = 50) + private String tabAdditionalData; + + @Column(name = "tab_additional_data_number", nullable = false, precision = 10) + private BigDecimal tabAdditionalDataNumber; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tab_key", nullable = false, insertable = false, updatable = false) + private TabObject tabObject; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tab_ver", nullable = false, insertable = false, updatable = false) + private TabVersion tabVersion; + + public TabInstance() { + } + + public TabInstancePK getId() { + return this.id; + } + + public void setId(TabInstancePK id) { + this.id = id; + } + + public String getTabAcronym() { + return tabAcronym; + } + + public void setTabAcronym(String tabAcronym) { + this.tabAcronym = tabAcronym; + } + + public String getTabAdditionalData() { + return tabAdditionalData; + } + + public void setTabAdditionalData(String tabAdditionalData) { + this.tabAdditionalData = tabAdditionalData; + } + + public BigDecimal getTabAdditionalDataNumber() { + return tabAdditionalDataNumber; + } + + public void setTabAdditionalDataNumber(BigDecimal tabAdditionalDataNumber) { + this.tabAdditionalDataNumber = tabAdditionalDataNumber; + } + + public TabObject getTabObject() { + return tabObject; + } + + public void setTabObject(TabObject tabObject) { + this.tabObject = tabObject; + } + + public TabVersion getTabVersion() { + return tabVersion; + } + + public void setTabVersion(TabVersion tabVersion) { + this.tabVersion = tabVersion; + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabInstancePK.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabInstancePK.java new file mode 100644 index 000000000..1d2053995 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabInstancePK.java @@ -0,0 +1,56 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze.tab; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import java.io.Serializable; + +@Embeddable +public class TabInstancePK implements Serializable { + + private static final long serialVersionUID = 1L; + + @Column(name = "tab_key", insertable = false, updatable = false) + private long tabKey; + + @Column(name = "tab_ver", insertable = false, updatable = false) + private long tabVer; + + public TabInstancePK() { + } + + public long getTabKey() { + return this.tabKey; + } + + public void setTabKey(long tabKey) { + this.tabKey = tabKey; + } + + public long getTabVer() { + return this.tabVer; + } + + public void setTabVer(long tabVer) { + this.tabVer = tabVer; + } + + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof TabInstancePK)) { + return false; + } + TabInstancePK castOther = (TabInstancePK) other; + return (this.tabKey == castOther.tabKey) && (this.tabVer == castOther.tabVer); + } + + public int hashCode() { + final int prime = 31; + int hash = 17; + hash = hash * prime + ((int) (this.tabKey ^ (this.tabKey >>> 32))); + hash = hash * prime + ((int) (this.tabVer ^ (this.tabVer >>> 32))); + + return hash; + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabObject.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabObject.java new file mode 100644 index 000000000..ee77ad0e9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabObject.java @@ -0,0 +1,68 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze.tab; + +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "tab_object") +public class TabObject extends AbstractEntity { + + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "tab_key", unique = true, nullable = false, precision = 20) + private long tabKey; + + @Column(name = "tab_acronym", nullable = false, length = 5) + private String tabAcronym; + + @OneToMany(mappedBy = "tabObject") + private List tabInstances; + + public TabObject() { + } + + @Override + public Long getId() { + return tabKey; + } + + public long getTabKey() { + return this.tabKey; + } + + public void setTabKey(long tabKey) { + this.tabKey = tabKey; + } + + public String getTabAcronym() { + return this.tabAcronym; + } + + public void setTabAcronym(String tabAcronym) { + this.tabAcronym = tabAcronym; + } + + public List getBasaBetriebsstelles() { + return this.tabInstances; + } + + public void setBasaBetriebsstelles(List tabInstances) { + this.tabInstances = tabInstances; + } + + public TabInstance addBasaBetriebsstelle(TabInstance tabInstance) { + getBasaBetriebsstelles().add(tabInstance); + tabInstance.setTabObject(this); + + return tabInstance; + } + + public TabInstance removeBasaBetriebsstelle(TabInstance tabInstance) { + getBasaBetriebsstelles().remove(tabInstance); + tabInstance.setTabObject(null); + + return tabInstance; + } + +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabSource.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabSource.java new file mode 100644 index 000000000..c4cbe5ce4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabSource.java @@ -0,0 +1,65 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze.tab; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.util.List; + +@Entity +@Table(name = "tab_source") +public class TabSource implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "tab_key", unique = true, nullable = false, precision = 20) + private long tabKey; + + @Column(name = "tab_acronym", length = 10) + private String tabAcronym; + + @OneToMany(mappedBy = "tabSource") + private List tabVersions; + + public TabSource() { + } + + public long getTabKey() { + return this.tabKey; + } + + public void setTabKey(long tabKey) { + this.tabKey = tabKey; + } + + public String getTabAcronym() { + return this.tabAcronym; + } + + public void setTabAcronym(String tabAcronym) { + this.tabAcronym = tabAcronym; + } + + public List getTabVersions() { + return this.tabVersions; + } + + public void setTabVersions(List tabVersions) { + this.tabVersions = tabVersions; + } + + public TabVersion addTabVersion(TabVersion tabVersion) { + getTabVersions().add(tabVersion); + tabVersion.setTabSource(this); + + return tabVersion; + } + + public TabVersion removeTabVersion(TabVersion tabVersion) { + getTabVersions().remove(tabVersion); + tabVersion.setTabSource(null); + + return tabVersion; + } + +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabVersion.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabVersion.java new file mode 100644 index 000000000..0ecd7135d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/TabVersion.java @@ -0,0 +1,89 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze.tab; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +@Entity +@Table(name = "tab_version") +public class TabVersion implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "tab_key", unique = true, nullable = false, precision = 20) + private long tabKey; + + @Column(name = "tab_time_stamp", nullable = false, precision = 10) + private BigDecimal tabTimeStamp; + + @Column(name = "tab_acronym", nullable = false, length = 120) + private String tabAcronym; + + @OneToMany(mappedBy = "tabVersion") + private List tabInstances; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tab_source", nullable = false) + private TabSource tabSource; + + public TabVersion() { + } + + public long getTabKey() { + return this.tabKey; + } + + public void setTabKey(long tabKey) { + this.tabKey = tabKey; + } + + public BigDecimal getTabTimeStamp() { + return this.tabTimeStamp; + } + + public void setTabTimeStamp(BigDecimal tabTimeStamp) { + this.tabTimeStamp = tabTimeStamp; + } + + public String getTabAcronym() { + return this.tabAcronym; + } + + public void setTabAcronym(String tabAcronym) { + this.tabAcronym = tabAcronym; + } + + public List getTabInstances() { + return this.tabInstances; + } + + public void setTabInstances(List tabInstances) { + this.tabInstances = tabInstances; + } + + public TabInstance addTabInstance(TabInstance tabInstance) { + getTabInstances().add(tabInstance); + tabInstance.setTabVersion(this); + + return tabInstance; + } + + public TabInstance removeTabInstance(TabInstance tabInstance) { + getTabInstances().remove(tabInstance); + tabInstance.setTabVersion(null); + + return tabInstance; + } + + public TabSource getTabSource() { + return this.tabSource; + } + + public void setTabSource(TabSource tabSource) { + this.tabSource = tabSource; + } + +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/cte/TabKeyVer.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/cte/TabKeyVer.java new file mode 100644 index 000000000..7717a510d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/blaze/tab/cte/TabKeyVer.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.blaze.tab.cte; + +import com.blazebit.persistence.CTE; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import java.io.Serializable; + +@CTE +@Entity +public class TabKeyVer implements Serializable { + @Id + private Long tabKey; + @Id + private Long tabVer; +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/literal/BindCriteriaLiteralTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/literal/BindCriteriaLiteralTest.java new file mode 100644 index 000000000..7909dcc6c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/literal/BindCriteriaLiteralTest.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.literal; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.query.criteria.ValueHandlingMode; + +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class BindCriteriaLiteralTest extends DefaultCriteriaLiteralTest { + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.CRITERIA_VALUE_HANDLING_MODE, ValueHandlingMode.BIND); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/literal/DefaultCriteriaLiteralTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/literal/DefaultCriteriaLiteralTest.java new file mode 100644 index 000000000..95b1aa4b1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/literal/DefaultCriteriaLiteralTest.java @@ -0,0 +1,149 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.literal; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.ParameterExpression; +import jakarta.persistence.criteria.Root; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DefaultCriteriaLiteralTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Book.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setId(1L); + book.setName("High-Performance Java Persistence"); + book.setIsbn(978_9730228236L); + book.setActive(true); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Book.class); + Root root = cq.from(Book.class); + + Book book = entityManager.createQuery(cq + .select(root) + .where(cb.equal(root.get("name"), "High-Performance Java Persistence"))) + .getSingleResult(); + assertEquals("High-Performance Java Persistence", book.getName()); + assertEquals(978_9730228236L, book.getIsbn()); + }); + + doInJPA(entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Book.class); + Root root = cq.from(Book.class); + + Book book = entityManager.createQuery(cq + .select(root) + .where(cb.equal(root.get("isbn"), 978_9730228236L))) + .getSingleResult(); + assertEquals("High-Performance Java Persistence", book.getName()); + }); + + doInJPA(entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Book.class); + Root root = cq.from(Book.class); + ParameterExpression isbn = cb.parameter(Long.class); + cq.select(root) + .where(cb.equal(root.get("isbn"), isbn)); + + Book book = entityManager.createQuery(cq + .select(root) + .where(cb.equal(root.get("isbn"), isbn))) + .setParameter(isbn, 978_9730228236L) + .getSingleResult(); + assertEquals("High-Performance Java Persistence", book.getName()); + }); + + doInJPA(entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery(Book.class); + Root root = cq.from(Book.class); + cq.select(root); + cq.where(cb.equal(root.get("active"), true)); + + Book book = entityManager.createQuery(cq).getSingleResult(); + assertEquals(978_9730228236L, book.getIsbn()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + private Long id; + + private String name; + + @NaturalId + private long isbn; + + private boolean active; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getIsbn() { + return isbn; + } + + public void setIsbn(long isbn) { + this.isbn = isbn; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/literal/InlineCriteriaLiteralTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/literal/InlineCriteriaLiteralTest.java new file mode 100644 index 000000000..0b7f8105c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/criteria/literal/InlineCriteriaLiteralTest.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hpjp.hibernate.criteria.literal; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.query.criteria.ValueHandlingMode; + +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class InlineCriteriaLiteralTest extends DefaultCriteriaLiteralTest { + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.CRITERIA_VALUE_HANDLING_MODE, ValueHandlingMode.INLINE); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/AbstractEqualityCheckTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/AbstractEqualityCheckTest.java new file mode 100644 index 000000000..5eb42cc55 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/AbstractEqualityCheckTest.java @@ -0,0 +1,96 @@ +package com.vladmihalcea.hpjp.hibernate.equality; + +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Session; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public abstract class AbstractEqualityCheckTest> extends AbstractTest { + + protected void assertEqualityConsistency(Class clazz, T entity) { + Set tuples = new HashSet<>(); + tuples.add(entity); + assertTrue(tuples.contains(entity)); + + doInJPA(entityManager -> { + entityManager.persist(entity); + entityManager.flush(); + assertTrue( + "The entity is not found in the Set after it's persisted.", + tuples.contains(entity) + ); + }); + + assertTrue(tuples.contains(entity)); + + doInJPA(entityManager -> { + T _entity = entityManager.merge(entity); + assertTrue( + "The entity is not found in the Set after it's merged.", + tuples.contains(_entity) + ); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).update(entity); + assertTrue( + "The entity is not found in the Set after it's reattached.", + tuples.contains(entity) + ); + }); + + doInJPA(entityManager -> { + T _entity = entityManager.find(clazz, entity.getId()); + assertTrue( + "The entity is not found in the Set after it's loaded in a different Persistence Context.", + tuples.contains(_entity) + ); + }); + + doInJPA(entityManager -> { + T _entity = entityManager.getReference(clazz, entity.getId()); + assertTrue( + "The entity is not found in the Set after it's loaded as a proxy in a different Persistence Context.", + tuples.contains(_entity) + ); + }); + + doInJPA(entityManager -> { + T entityProxy = entityManager.getReference( + clazz, + entity.getId() + ); + assertTrue( + "The entity is not equal with the entity proxy.", + entity.equals(entityProxy) + ); + assertEquals( + "The entity hashCode is different than the entity proxy.", + entity.hashCode(), + entityProxy.hashCode() + ); + }); + + T deletedEntity = doInJPA(entityManager -> { + T _entity = entityManager.find( + clazz, + entity.getId() + ); + entityManager.remove(_entity); + return _entity; + }); + + assertTrue( + "The entity is not found in the Set even after it's deleted.", + tuples.contains(deletedEntity) + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/DefaultEqualityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/DefaultEqualityTest.java new file mode 100644 index 000000000..ba4a12573 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/DefaultEqualityTest.java @@ -0,0 +1,60 @@ +package com.vladmihalcea.hpjp.hibernate.equality; + +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +public class DefaultEqualityTest + extends AbstractEqualityCheckTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + @Ignore + public void testEquality() { + Post post = new Post(); + post.setTitle("High-PerformanceJava Persistence"); + + assertEqualityConsistency(Post.class, post); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post implements Identifiable { + + @Id + @GeneratedValue + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/DefaultIdEqualityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/DefaultIdEqualityTest.java new file mode 100644 index 000000000..21c02e5fe --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/DefaultIdEqualityTest.java @@ -0,0 +1,72 @@ +package com.vladmihalcea.hpjp.hibernate.equality; + +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Objects; + +/** + * @author Vlad Mihalcea + */ +public class DefaultIdEqualityTest + extends AbstractEqualityCheckTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Test + @Ignore + public void testEquality() { + Post post = new Post(); + post.setTitle("High-PerformanceJava Persistence"); + + assertEqualityConsistency(Post.class, post); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post implements Identifiable { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Post)) return false; + return Objects.equals(id, ((Post) o).getId()); + } + @Override + public int hashCode() { + return Objects.hash(getId()); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/IdEqualityParentChildTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/IdEqualityParentChildTest.java new file mode 100644 index 000000000..0fadafdfe --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/IdEqualityParentChildTest.java @@ -0,0 +1,244 @@ +package com.vladmihalcea.hpjp.hibernate.equality; + +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class IdEqualityParentChildTest + extends AbstractEqualityCheckTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.setProperty("hibernate.jdbc.batch_size", "100"); + properties.setProperty("hibernate.order_inserts", "true"); + return properties; + } + + @Test + public void testEquality() { + Post post = new Post(); + post.setTitle("High-PerformanceJava Persistence"); + + assertEqualityConsistency(Post.class, post); + } + + @Test + public void testCollectionSize() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + int collectionSize = 25_000; + + long createSetStartNanos = System.nanoTime(); + Set postSet = new HashSet<>(); + + for (int i = 0; i < collectionSize; i++) { + Post post = new Post(); + postSet.add(post); + } + + long createSetEndNanos = System.nanoTime(); + LOGGER.info( + "Creating a Set with [{}] elements took : [{}] s", + collectionSize, + TimeUnit.NANOSECONDS.toSeconds(createSetEndNanos - createSetStartNanos) + ); + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testAddAndFetchCollection() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + int collectionSize = 10000; + + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + doInJPA(entityManager -> { + entityManager.persist(post); + + for (int i = 1; i <= collectionSize; i++) { + post.addComment( + new PostComment() + .setReview(String.format("Comment nr. %d", i)) + ); + } + long createSetStartNanos = System.nanoTime(); + entityManager.flush(); + LOGGER.info( + "Creating the Set with [{}] elements took : [{}] ms", + collectionSize, + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - createSetStartNanos) + ); + }); + + doInJPA(entityManager -> { + long fetchSetStartNanos = System.nanoTime(); + Post postWithComments = entityManager.createNamedQuery("POST_WITH_COMMENTS", Post.class) + .setParameter("postId", post.getId()) + .getSingleResult(); + LOGGER.info( + "Fetching the Set with [{}] elements took : [{}] ms", + collectionSize, + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - fetchSetStartNanos) + ); + assertEquals(postWithComments.comments.size(), collectionSize); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @NamedQuery(name = "POST_WITH_COMMENTS", query = """ + select p + from Post p + join fetch p.comments + where p.id = :postId + """) + public static class Post implements Identifiable { + + @Id + @GeneratedValue + private Long id; + + private boolean idWasNull; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) + private Set comments = new HashSet<>(); + + public Post() { + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof Post)) + return false; + + Post other = (Post) o; + + return id != null && id.equals(other.getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Set getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment implements Identifiable { + + @Id + @GeneratedValue + private Long id; + + private boolean idWasNull; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + public PostComment() { + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof PostComment)) + return false; + + PostComment other = (PostComment) o; + + return id != null && id.equals(other.getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String title) { + this.review = title; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/IdEqualityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/IdEqualityTest.java new file mode 100644 index 000000000..51e91941b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/IdEqualityTest.java @@ -0,0 +1,134 @@ +package com.vladmihalcea.hpjp.hibernate.equality; + +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static java.util.stream.Collectors.groupingBy; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class IdEqualityTest + extends AbstractEqualityCheckTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testEquality() { + Post post = new Post(); + post.setTitle("High-PerformanceJava Persistence"); + + assertEqualityConsistency(Post.class, post); + } + + @Test + public void testCollectionSize() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + int collectionSize = 25_000; + + long createListStartNanos = System.nanoTime(); + List postList = new ArrayList<>(collectionSize); + + for (int i = 0; i < collectionSize; i++) { + Post post = new Post().setId((long) i); + postList.add(i, post); + } + + long createListEndNanos = System.nanoTime(); + LOGGER.info( + "Creating a List with [{}] elements took : [{}] μs", + collectionSize, + TimeUnit.NANOSECONDS.toMicros(createListEndNanos - createListStartNanos) + ); + + long createSetStartNanos = System.nanoTime(); + Set postSet = new HashSet<>(); + + for (int i = 0; i < collectionSize; i++) { + Post post = new Post().setId((long) i); + postSet.add(post); + } + + long createSetEndNanos = System.nanoTime(); + LOGGER.info( + "Creating a Set with [{}] elements took : [{}] μs", + collectionSize, + TimeUnit.NANOSECONDS.toMicros(createSetEndNanos - createSetStartNanos) + ); + + Random random = new Random(); + Post randomPost = postList.get(random.nextInt(collectionSize)); + long startNanos = System.nanoTime(); + boolean contained = postList.contains(randomPost); + long endNanos = System.nanoTime(); + assertTrue(contained); + LOGGER.info( + "Calling HashSet contains took : [{}] microseconds", + TimeUnit.NANOSECONDS.toMicros(endNanos - startNanos) + ); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post implements Identifiable { + + @Id + @GeneratedValue + private Long id; + + private String title; + + public Post() { + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof Post)) + return false; + + Post other = (Post) o; + + return id != null && id.equals(other.getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/IdWasNullEqualityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/IdWasNullEqualityTest.java new file mode 100644 index 000000000..4b87d24c9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/IdWasNullEqualityTest.java @@ -0,0 +1,247 @@ +package com.vladmihalcea.hpjp.hibernate.equality; + +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class IdWasNullEqualityTest + extends AbstractEqualityCheckTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.setProperty("hibernate.jdbc.batch_size", "100"); + properties.setProperty("hibernate.order_inserts", "true"); + return properties; + } + + @Test + public void testEquality() { + Post post = new Post(); + post.setTitle("High-PerformanceJava Persistence"); + + assertEqualityConsistency(Post.class, post); + } + + @Test + public void testCollectionSize() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + int collectionSize = 25_000; + + long createSetStartNanos = System.nanoTime(); + Set postSet = new HashSet<>(); + + for (int i = 0; i < collectionSize; i++) { + Post post = new Post(); + postSet.add(post); + } + + long createSetEndNanos = System.nanoTime(); + LOGGER.info( + "Creating a Set with [{}] elements took : [{}] s", + collectionSize, + TimeUnit.NANOSECONDS.toSeconds(createSetEndNanos - createSetStartNanos) + ); + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testAddAndFetchCollection() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + int collectionSize = 10000; + + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + doInJPA(entityManager -> { + entityManager.persist(post); + + for (int i = 1; i <= collectionSize; i++) { + post.addComment( + new PostComment() + .setReview(String.format("Comment nr. %d", i)) + ); + } + long createSetStartNanos = System.nanoTime(); + entityManager.flush(); + LOGGER.info( + "Creating the Set with [{}] elements took : [{}] ms", + collectionSize, + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - createSetStartNanos) + ); + }); + + doInJPA(entityManager -> { + long fetchSetStartNanos = System.nanoTime(); + Post postWithComments = entityManager.createNamedQuery("POST_WITH_COMMENTS", Post.class) + .setParameter("postId", post.getId()) + .getSingleResult(); + LOGGER.info( + "Fetching the Set with [{}] elements took : [{}] ms", + collectionSize, + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - fetchSetStartNanos) + ); + assertEquals(postWithComments.comments.size(), collectionSize); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @NamedQuery(name = "POST_WITH_COMMENTS", query = """ + select p + from Post p + join fetch p.comments + where p.id = :postId + """) + public static class Post implements Identifiable { + + @Id + @GeneratedValue + private Long id; + + private boolean idWasNull; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) + private Set comments = new HashSet<>(); + + public Post() { + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof Post)) + return false; + + Post other = (Post) o; + + return id != null && id.equals(other.getId()); + } + + @Override + public int hashCode() { + Long id = getId(); + if (id == null) idWasNull = true; + return idWasNull ? 0 : id.hashCode(); + } + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Set getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment implements Identifiable { + + @Id + @GeneratedValue + private Long id; + + private boolean idWasNull; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + public PostComment() { + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof PostComment)) + return false; + + PostComment other = (PostComment) o; + + return id != null && id.equals(other.getId()); + } + + @Override + public int hashCode() { + Long id = getId(); + if (id == null) idWasNull = true; + return idWasNull ? 0 : id.hashCode(); + } + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String title) { + this.review = title; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/NaturalIdEqualityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/NaturalIdEqualityTest.java similarity index 79% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/NaturalIdEqualityTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/NaturalIdEqualityTest.java index ce46aa43e..87b090b03 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/equality/NaturalIdEqualityTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/NaturalIdEqualityTest.java @@ -1,19 +1,17 @@ -package com.vladmihalcea.book.hpjp.hibernate.equality; +package com.vladmihalcea.hpjp.hibernate.equality; -import com.vladmihalcea.book.hpjp.hibernate.identifier.Identifiable; +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; import org.hibernate.annotations.NaturalId; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.*; import java.util.Objects; /** * @author Vlad Mihalcea */ -public class NaturalIdEqualityTest extends AbstractEqualityCheckTest { +public class NaturalIdEqualityTest + extends AbstractEqualityCheckTest { @Override protected Class[] entities() { @@ -28,7 +26,7 @@ public void testEquality() { book.setTitle("High-PerformanceJava Persistence"); book.setIsbn("123-456-7890"); - assertEqualityConstraints(Book.class, book); + assertEqualityConsistency(Book.class, book); } @Entity(name = "Book") @@ -42,12 +40,14 @@ public static class Book implements Identifiable { private String title; @NaturalId + @Column(nullable = false) private String isbn; @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Book)) return false; + Book book = (Book) o; return Objects.equals(getIsbn(), book.getIsbn()); } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/UUIDEqualityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/UUIDEqualityTest.java new file mode 100644 index 000000000..f444e8ae3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/equality/UUIDEqualityTest.java @@ -0,0 +1,75 @@ +package com.vladmihalcea.hpjp.hibernate.equality; + +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Objects; +import java.util.UUID; + +/** + * @author Vlad Mihalcea + */ +public class UUIDEqualityTest + extends AbstractEqualityCheckTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testEquality() { + Post post = new Post(); + post.setTitle("High-PerformanceJava Persistence"); + + assertEqualityConsistency(Post.class, post); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post implements Identifiable { + + @Id + private UUID id = UUID.randomUUID(); + + private String title; + + public Post() { + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Post)) return false; + Post post = (Post) o; + return Objects.equals(id, post.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/BatchSizeCollectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/BatchSizeCollectionTest.java new file mode 100644 index 000000000..301fca0cb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/BatchSizeCollectionTest.java @@ -0,0 +1,167 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.annotations.BatchSize; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class BatchSizeCollectionTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + + @Override + public void init() { + super.init(); + + int commentsSize = 2; + + doInJPA(entityManager -> { + LongStream.range(0, 5).forEach(i -> { + Post post = new Post(i); + post.setTitle(String.format("Post nr. %d", i)); + + LongStream.range(0, commentsSize).forEach(j -> { + PostComment comment = new PostComment(); + comment.setId((i * commentsSize) + j); + comment.setReview(String.format("Good review nr. %d", comment.getId())); + post.addComment(comment); + + }); + entityManager.persist(post); + }); + }); + } + + @Test + public void testFind() { + doInJPA(entityManager -> { + Post post1 = entityManager.find(Post.class, 1L); + Post post2 = entityManager.find(Post.class, 2L); + Post post3 = entityManager.find(Post.class, 3L); + + for (PostComment comment : post1.getComments()) { + comment.getReview(); + } + + for (PostComment comment : post2.getComments()) { + comment.getReview(); + } + + for (PostComment comment : post3.getComments()) { + comment.getReview(); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) + @BatchSize(size = 2) + private List comments = new ArrayList<>(); + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/CriteriaAPIEntityTypeJoinedTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/CriteriaAPIEntityTypeJoinedTest.java new file mode 100644 index 000000000..a2c23e5b5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/CriteriaAPIEntityTypeJoinedTest.java @@ -0,0 +1,190 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class CriteriaAPIEntityTypeJoinedTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Topic.class, + Post.class, + Announcement.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setOwner("Vlad"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("Vlad"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + + entityManager.persist(announcement); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery criteria = builder.createQuery(Topic.class); + Root root = criteria.from(Topic.class); + + criteria.where( + builder.equal(root.get("owner"), "Vlad") + ); + + List topics = entityManager + .createQuery(criteria) + .getResultList(); + + assertEquals(2, topics.size()); + }); + + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery criteria = builder.createQuery(Post.class); + Root root = criteria.from(Post.class); + + criteria.where( + builder.equal(root.get("owner"), "Vlad") + ); + + List posts = entityManager + .createQuery(criteria) + .getResultList(); + + assertEquals(1, posts.size()); + }); + + doInJPA(entityManager -> { + Class sublcass = Post.class; + + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery criteria = builder.createQuery(Topic.class); + Root root = criteria.from(Topic.class); + + criteria.where( + builder.and( + builder.equal(root.get("owner"), "Vlad"), + builder.equal(root.type(), sublcass) + ) + ); + + List topics = entityManager + .createQuery(criteria) + .getResultList(); + + assertEquals(1, topics.size()); + }); + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.JOINED) + @DiscriminatorColumn + @DiscriminatorValue("0") + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } + + @Entity(name = "Post") + @DiscriminatorValue("1") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + @DiscriminatorValue("2") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/CriteriaAPIEntityTypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/CriteriaAPIEntityTypeTest.java new file mode 100644 index 000000000..e85cf4137 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/CriteriaAPIEntityTypeTest.java @@ -0,0 +1,185 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import jakarta.persistence.criteria.*; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class CriteriaAPIEntityTypeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Topic.class, + Post.class, + Announcement.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setOwner("Vlad"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("Vlad"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + + entityManager.persist(announcement); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery criteria = builder.createQuery(Topic.class); + Root root = criteria.from(Topic.class); + + criteria.where( + builder.equal(root.get("owner"), "Vlad") + ); + + List topics = entityManager + .createQuery(criteria) + .getResultList(); + + assertEquals(2, topics.size()); + }); + + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery criteria = builder.createQuery(Post.class); + Root root = criteria.from(Post.class); + + criteria.where( + builder.equal(root.get("owner"), "Vlad") + ); + + List posts = entityManager + .createQuery(criteria) + .getResultList(); + + assertEquals(1, posts.size()); + }); + + doInJPA(entityManager -> { + Class sublcass = Post.class; + + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery criteria = builder.createQuery(Topic.class); + Root root = criteria.from(Topic.class); + + criteria.where( + builder.and( + builder.equal(root.get("owner"), "Vlad"), + builder.equal(root.type(), sublcass) + ) + ); + + List topics = entityManager + .createQuery(criteria) + .getResultList(); + + assertEquals(1, topics.size()); + }); + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } + + @Entity(name = "Post") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/CriteriaAPIMemberOfTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/CriteriaAPIMemberOfTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/CriteriaAPIMemberOfTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/CriteriaAPIMemberOfTest.java index a0d404557..126a070bd 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/CriteriaAPIMemberOfTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/CriteriaAPIMemberOfTest.java @@ -1,10 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; +package com.vladmihalcea.hpjp.hibernate.fetching; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; -import javax.persistence.criteria.*; +import jakarta.persistence.*; +import jakarta.persistence.criteria.*; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/CriteriaAPITest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/CriteriaAPITest.java new file mode 100644 index 000000000..fe350dcdd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/CriteriaAPITest.java @@ -0,0 +1,193 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.hibernate.forum.*; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.query.Query; +import org.hibernate.transform.Transformers; +import org.junit.Test; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.criteria.*; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class CriteriaAPITest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + Tag.class, + PostDetails.class + }; + } + + + @Override + public void init() { + super.init(); + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setDetails( + new PostDetails() + .setCreatedOn(new Date()) + .setCreatedBy("Vlad Mihalcea") + ); + + entityManager.persist(post); + + for (long i = 0; i < 5; i++) { + post.addComment( + new PostComment() + .setId(i + 1) + .setReview("Great") + ); + } + }); + } + + @Test + public void testFind() { + doInJPA(entityManager -> { + List posts = filterPosts(entityManager, "High-Performance Java Persistence"); + assertFalse(posts.isEmpty()); + }); + doInJPA(entityManager -> { + List posts = filterPosts(entityManager, null); + assertTrue(posts.isEmpty()); + }); + } + + @Test + public void testFilterChildWithoutMetamodel() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = builder.createQuery(PostComment.class); + Root postComment = query.from(PostComment.class); + + Join post = postComment.join("post"); + + query.where( + builder.equal( + post.get("title"), + "High-Performance Java Persistence" + ) + ); + + List comments = entityManager + .createQuery(query) + .getResultList(); + + assertEquals(5, comments.size()); + }); + } + + @Test + public void testFilterChildWithMetamodel() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = builder.createQuery(PostComment.class); + Root postComment = query.from(PostComment.class); + + Join post = postComment.join(PostComment_.post); + + query.where( + builder.equal( + post.get(Post_.title), + "High-Performance Java Persistence" + ) + ); + + List comments = entityManager + .createQuery(query) + .getResultList(); + + assertEquals(5, comments.size()); + }); + } + + private List filterPosts(EntityManager entityManager, String titlePattern) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteria = builder.createQuery(Post.class); + Root post = criteria.from(Post.class); + + post.fetch(Post_.comments, JoinType.LEFT); + + Predicate titlePredicate = titlePattern == null ? + builder.isNull(post.get(Post_.title)) : + builder.like(post.get(Post_.title), titlePattern); + + criteria.where(titlePredicate); + List posts = entityManager.createQuery(criteria).getResultList(); + + return posts; + } + + @Test + public void testFetchObjectArray() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteria = builder.createQuery(Object[].class); + Root root = criteria.from(PostComment.class); + criteria.multiselect(root.get(PostComment_.id), root.get(PostComment_.review)); + + Join postJoin = root.join("post"); + + criteria.where(builder.like(postJoin.get(Post_.title), "High-Performance Java Persistence")); + List comments = entityManager.createQuery(criteria).getResultList(); + + assertEquals(5, comments.size()); + }); + } + + @Test + public void testFetchObjectArrayToDTO() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = builder.createQuery(Object[].class); + + Root postComment = query.from(PostComment.class); + Join post = postComment.join(PostComment_.post); + + query.multiselect( + postComment.get(PostComment_.id).alias(PostComment_.ID), + postComment.get(PostComment_.review).alias(PostComment_.REVIEW), + post.get(Post_.title).alias(Post_.TITLE) + ); + + query.where( + builder.and( + builder.like( + post.get(Post_.title), + "%Java Persistence%" + ), + builder.equal( + post.get(Post_.details).get(PostDetails_.CREATED_BY), + "Vlad Mihalcea" + ) + ) + ); + + List comments = entityManager + .createQuery(query) + .unwrap(Query.class) + .setResultTransformer(Transformers.aliasToBean(PostCommentSummary.class)) + .getResultList(); + + assertEquals(5, comments.size()); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/DistinctTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/DistinctTest.java new file mode 100644 index 000000000..018bf64b3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/DistinctTest.java @@ -0,0 +1,218 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DistinctTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence eBook has been released!") + .setCreatedOn(LocalDate.of(2016, 8, 30)) + .addComment(new PostComment("Excellent!")) + .addComment(new PostComment("Great!")) + ); + + entityManager.persist( + new Post() + .setId(2L) + .setTitle("High-Performance Java Persistence paperback has been released!") + .setCreatedOn(LocalDate.of(2016, 10, 12)) + ); + + entityManager.persist( + new Post() + .setId(3L) + .setTitle("High-Performance Java Persistence Mach 1 video course has been released!") + .setCreatedOn(LocalDate.of(2018, 1, 30)) + ); + + entityManager.persist( + new Post() + .setId(4L) + .setTitle("High-Performance Java Persistence Mach 2 video course has been released!") + .setCreatedOn(LocalDate.of(2018, 5, 8)) + ); + }); + } + + @Test + public void testWithDistinctScalarQuery() { + doInJPA(entityManager -> { + List publicationYears = entityManager.createQuery(""" + select distinct year(p.createdOn) + from Post p + order by year(p.createdOn) + """, Integer.class) + .getResultList(); + + LOGGER.info("Publication years: {}", publicationYears); + }); + } + + @Test + public void testWithoutDistinct() { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + where p.title = :title + """, Post.class) + .setParameter("title", "High-Performance Java Persistence eBook has been released!") + .getResultList(); + + LOGGER.info("Fetched the following Post entity identifiers: {}", posts.stream().map(Post::getId).collect(Collectors.toList())); + }); + } + + @Test + public void testAutoDeduplication() { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + where p.title = :title + """, Post.class) + .setParameter("title", "High-Performance Java Persistence eBook has been released!") + .getResultList(); + + assertEquals(1, posts.size()); + assertEquals(2, posts.get(0).getComments().size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDate createdOn; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDate getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDate createdOn) { + this.createdOn = createdOn; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public PostComment() {} + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EagerFetchingCollectionsFindVsQueryTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EagerFetchingCollectionsFindVsQueryTest.java new file mode 100644 index 000000000..e13bad74f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EagerFetchingCollectionsFindVsQueryTest.java @@ -0,0 +1,225 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertFalse; + +/** + * @author Vlad Mihalcea + */ +public class EagerFetchingCollectionsFindVsQueryTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + Tag.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + List tags = new ArrayList<>(); + + for (long i = 1; i <= 3; i++) { + Tag tag = new Tag() + .setId(i) + .setName(String.format("Tag nr. %d", i + 1)); + + entityManager.persist(tag); + tags.add(tag); + } + + long commentId = 0; + + Post post = new Post() + .setId(1L) + .setTitle(String.format("Post nr. %d", 1L)); + + + for (long i = 0; i < 2; i++) { + post.addComment( + new PostComment() + .setId(++commentId) + .setReview("Excellent!") + ); + } + + for (int i = 0; i < 3; i++) { + post.getTags().add(tags.get(i)); + } + + entityManager.persist(post); + }); + } + + @Test + public void testFindEntityById() { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertFalse(post.getComments().isEmpty()); + assertFalse(post.getTags().isEmpty()); + }); + } + + @Test + public void testQueryEntityById() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery( + "select p from Post p where p.id = :postId", Post.class) + .setParameter("postId", 1L) + .getSingleResult(); + + assertFalse(post.getComments().isEmpty()); + assertFalse(post.getTags().isEmpty()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.EAGER + ) + private Set comments = new HashSet<>(); + + @ManyToMany( + cascade = { + CascadeType.PERSIST, + CascadeType.MERGE + }, + fetch = FetchType.EAGER + ) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private Set tags = new HashSet<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Set getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Set getTags() { + return tags; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EagerFetchingCollectionsQueryTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EagerFetchingCollectionsQueryTest.java new file mode 100644 index 000000000..052b161a2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EagerFetchingCollectionsQueryTest.java @@ -0,0 +1,238 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.*; + +import static org.junit.Assert.assertFalse; + +/** + * @author Vlad Mihalcea + */ +public class EagerFetchingCollectionsQueryTest extends AbstractTest { + + public static final int POST_COUNT = 50; + public static final int POST_COMMENT_COUNT = 20; + public static final int TAG_COUNT = 10; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + Tag.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + List tags = new ArrayList<>(); + + for (long i = 1; i <= TAG_COUNT; i++) { + Tag tag = new Tag() + .setId(i) + .setName(String.format("Tag nr. %d", i + 1)); + + entityManager.persist(tag); + tags.add(tag); + } + + long commentId = 0; + + for (long postId = 1; postId <= POST_COUNT; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post nr. %d", postId)); + + + for (long i = 0; i < POST_COMMENT_COUNT; i++) { + post.addComment( + new PostComment() + .setId(++commentId) + .setReview("Excellent!") + ); + } + + for (int i = 0; i < TAG_COUNT; i++) { + post.getTags().add(tags.get(i)); + } + + entityManager.persist(post); + } + }); + } + + @Test + public void testJoinFetch() { + List posts = doInJPA(entityManager -> { + return entityManager.createQuery( + "select p " + + "from Post p " + + "left join fetch p.comments " + + "left join fetch p.tags", Post.class) + .getResultList(); + }); + + assertFalse(posts.isEmpty()); + + assertFalse(posts.get(0).getComments().isEmpty()); + assertFalse(posts.get(0).getTags().isEmpty()); + } + + @Test + public void testWithoutJoinFetch() { + List posts = doInJPA(entityManager -> { + return entityManager.createQuery( + "select p " + + "from Post p ", Post.class) + .getResultList(); + }); + + assertFalse(posts.isEmpty()); + + assertFalse(posts.get(0).getComments().isEmpty()); + assertFalse(posts.get(0).getTags().isEmpty()); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.EAGER + ) + private Set comments = new HashSet<>(); + + @ManyToMany( + cascade = { + CascadeType.PERSIST, + CascadeType.MERGE + }, + fetch = FetchType.EAGER + ) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private Set tags = new HashSet<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Set getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Set getTags() { + return tags; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EagerFetchingManyToOneEntityGraphTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EagerFetchingManyToOneEntityGraphTest.java new file mode 100644 index 000000000..bc1181459 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EagerFetchingManyToOneEntityGraphTest.java @@ -0,0 +1,248 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class EagerFetchingManyToOneEntityGraphTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + PostDetails.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + for (long i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setId(i++) + .setTitle(String.format("High-Performance Java Persistence, part %d", i)) + .setDetails( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn(new Date()) + ) + .addComment( + new PostComment() + .setReview("The first part is about JDBC") + ) + .addComment( + new PostComment() + .setReview("The second part is about JPA") + ) + .addComment( + new PostComment() + .setReview("The third part is about jOOQ") + ) + ); + } + }); + } + + @Test + public void testFindWithNamedEntityFetchGraph() { + PostComment comment = doInJPA(entityManager -> { + return entityManager.find(PostComment.class, 1L, + Collections.singletonMap( + "jakarta.persistence.fetchgraph", + entityManager.getEntityGraph("PostComment.post") + ) + ); + }); + assertNotNull(comment.getPost()); + } + + @Test + public void testFindWithNamedEntityLoadGraph() { + PostComment comment = doInJPA(entityManager -> { + return entityManager.find(PostComment.class, 1L, + Collections.singletonMap( + "jakarta.persistence.loadgraph", + entityManager.getEntityGraph("PostComment.post") + ) + ); + }); + assertNotNull(comment.getPost()); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY + ) + private PostDetails details; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostDetails") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @NamedEntityGraph(name = "PostComment.post", attributeNodes = @NamedAttributeNode("post")) + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EagerFetchingManyToOneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EagerFetchingManyToOneTest.java new file mode 100644 index 000000000..9ae65eff0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EagerFetchingManyToOneTest.java @@ -0,0 +1,236 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class EagerFetchingManyToOneTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + public void afterInit() { + String[] reviews = new String[] { + "amazing", + "awesome", + "excellent" + }; + + doInJPA(entityManager -> { + long pastId = 1; + long commentId = 1; + + for (long i = 1; i <= 3; i++) { + Post post = new Post() + .setId(pastId++) + .setTitle(String.format("High-Performance Java Persistence, part %d", i) + ); + entityManager.persist(post); + + for (int j = 0; j < 3; j++) { + entityManager.persist( + new PostComment() + .setId(commentId++) + .setPost(post) + .setReview(String.format("The part %d was %s", i, reviews[j])) + ); + } + } + + }); + } + + /*private HypersistenceOptimizer hypersistenceOptimizer; + + protected EntityManagerFactory newEntityManagerFactory() { + EntityManagerFactory emf = super.newEntityManagerFactory(); + hypersistenceOptimizer = new HypersistenceOptimizer( + new JpaConfig(emf) + ); + return emf; + }*/ + + @Test + public void testFindOne() { + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + + assertNotNull(comment); + }); + + //assertTrue(hypersistenceOptimizer.getEvents().isEmpty()); + } + + @Test + public void testFindOneWithQuery() { + doInJPA(entityManager -> { + PostComment comment = entityManager.createQuery(""" + select pc + from PostComment pc + where pc.id = :id + """, PostComment.class) + .setParameter("id", 1L) + .getSingleResult(); + + assertNotNull(comment); + }); + } + + @Test + public void testFindWithQuery() { + doInJPA(entityManager -> { + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + join pc.post p + where p.title like :titlePatttern + """, PostComment.class) + .setParameter("titlePatttern", "High-Performance Java Persistence%") + .getResultList(); + + assertEquals(9, comments.size()); + }); + } + + @Test + public void testFindWithQueryAndFetch() { + doInJPA(entityManager -> { + Long commentId = 1L; + PostComment comment = entityManager.createQuery(""" + select pc + from PostComment pc + left join fetch pc.post p + where pc.id = :id + """, PostComment.class) + .setParameter("id", commentId) + .getSingleResult(); + assertNotNull(comment); + }); + } + + @Test + public void testNPlusOneDetection() { + try { + LOGGER.info("Detect N+1"); + SQLStatementCountValidator.reset(); + + List _comments = doInJPA(entityManager -> { + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + where pc.review like :reviewPattern + """, PostComment.class) + .setParameter("reviewPattern", "%excellent") + .getResultList(); + + return comments; + }); + + assertEquals(3, _comments.size()); + + SQLStatementCountValidator.assertSelectCount(1); + } catch (Exception expected) { + LOGGER.error("Exception", expected); + } + } + + @Test + public void testFixingNPlusOne() { + LOGGER.info("Fixing N+1"); + SQLStatementCountValidator.reset(); + + doInJPA(entityManager -> { + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + join fetch pc.post + where pc.review like :reviewPattern + """, PostComment.class) + .setParameter("reviewPattern", "%excellent") + .getResultList(); + + assertEquals(3, comments.size()); + + SQLStatementCountValidator.assertSelectCount(1); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + private String review; + + @ManyToOne + private Post post; + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EntityGraphEagerPostTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EntityGraphEagerPostTest.java new file mode 100644 index 000000000..9f910a6fa --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EntityGraphEagerPostTest.java @@ -0,0 +1,203 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class EntityGraphEagerPostTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + long postId = 1; + long commentId = 1L; + for (long i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setId(postId++) + .setTitle(String.format("High-Performance Java Persistence, part %d", i)) + .addComment( + new PostComment() + .setId(commentId++) + .setReview("The first part is about JDBC") + ) + .addComment( + new PostComment() + .setId(commentId++) + .setReview("The second part is about JPA") + ) + .addComment( + new PostComment() + .setId(commentId++) + .setReview("The third part is about jOOQ") + ) + ); + } + }); + } + + @Test + public void testFind() { + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + + LOGGER.info("The comment post title is '{}'", comment.getPost().getTitle()); + }); + } + + @Test + public void testJPQL() { + doInJPA(entityManager -> { + PostComment comment = entityManager.createQuery(""" + select pc + from PostComment pc + left join fetch pc.post + where pc.id = :id + """, PostComment.class) + .setParameter("id", 1L) + .getSingleResult(); + + LOGGER.info("The comment post title is '{}'", comment.getPost().getTitle()); + }); + } + + @Test + public void testFindWithProgrammaticEntityGraph() { + doInJPA(entityManager -> { + EntityGraph postCommentGraph = entityManager.createEntityGraph(PostComment.class); + postCommentGraph.addAttributeNodes("post"); + + PostComment comment = entityManager.find( + PostComment.class, + 1L, + Collections.singletonMap( + "jakarta.persistence.loadgraph", + postCommentGraph + ) + ); + + LOGGER.info("The comment post title is '{}'", comment.getPost().getTitle()); + }); + } + + @Test + public void testFindWithDeclaredEntityGraph() { + PostComment comment = doInJPA(entityManager -> { + return entityManager.find( + PostComment.class, + 1L, + Collections.singletonMap( + "jakarta.persistence.loadgraph", + entityManager.getEntityGraph("PostComment.post") + ) + ); + }); + assertNotNull(comment.getPost()); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @NamedEntityGraph( + name = "PostComment.post", + attributeNodes = @NamedAttributeNode("post") + ) + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EntityGraphTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EntityGraphTest.java new file mode 100644 index 000000000..81cfc5047 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/EntityGraphTest.java @@ -0,0 +1,382 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner; +import org.junit.Test; +import org.junit.runner.RunWith; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +@RunWith(BytecodeEnhancerRunner.class) +public class EntityGraphTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostDetails.class, + PostComment.class, + PostCommentDetails.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + long postId = 1; + long commentId = 1L; + for (long i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setId(postId++) + .setTitle(String.format("High-Performance Java Persistence, part %d", i)) + .setDetails( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn(new Date()) + ) + .addComment( + new PostComment() + .setId(commentId++) + .setReview("The first part is about JDBC") + .setDetails(new PostCommentDetails().setViewCount(1)) + ) + .addComment( + new PostComment() + .setId(commentId++) + .setReview("The second part is about JPA") + .setDetails(new PostCommentDetails().setViewCount(2)) + ) + .addComment( + new PostComment() + .setId(commentId++) + .setReview("The third part is about jOOQ") + .setDetails(new PostCommentDetails().setViewCount(3)) + ) + ); + } + }); + } + + @Test + public void testFindWithNamedEntityFetchGraph() { + PostComment comment = doInJPA(entityManager -> { + EntityGraph postCommentGraph = entityManager.createEntityGraph(PostComment.class); + postCommentGraph.addAttributeNodes("post"); + + return entityManager.find(PostComment.class, 1L, + Collections.singletonMap( + "jakarta.persistence.fetchgraph", + entityManager.getEntityGraph("PostComment.post") + ) + ); + }); + + assertNotNull(comment.getPost()); + } + + @Test + public void testFindUsingNestedEntityGraph() { + PostCommentDetails commentDetails = doInJPA(entityManager -> { + EntityGraph commentDetailsGraph = entityManager.createEntityGraph(PostCommentDetails.class); + commentDetailsGraph.addAttributeNodes("comment"); + Subgraph commentSubgraph = commentDetailsGraph.addSubgraph("comment"); + commentSubgraph.addAttributeNodes("post"); + + return entityManager.find(PostCommentDetails.class, 1L, + Collections.singletonMap( + "jakarta.persistence.loadgraph", + commentDetailsGraph + ) + ); + }); + + assertNotNull(commentDetails.getComment()); + assertNotNull(commentDetails.getComment().getPost()); + } + + @Test + public void testFindWithNamedEntityLoadGraph() { + PostComment comment = doInJPA(entityManager -> { + return entityManager.find(PostComment.class, 1L, + Collections.singletonMap( + "jakarta.persistence.loadgraph", + entityManager.getEntityGraph("PostComment.post") + ) + ); + }); + assertNotNull(comment.getPost()); + } + + @Test + public void testFindPostWithAllAssociations() { + Post post = doInJPA(entityManager -> { + return entityManager.find(Post.class, 1L, + Collections.singletonMap( + "jakarta.persistence.fetchgraph", + entityManager.getEntityGraph("Post.all") + ) + ); + }); + + assertEquals("High-Performance Java Persistence, part 1", post.getTitle()); + assertEquals("Vlad Mihalcea", post.getDetails().getCreatedBy()); + assertEquals(3, post.getComments().size()); + assertTrue(post.getComments().get(0).getDetails().getViewCount() > 0); + } + + @Entity(name = "Post") + @Table(name = "post") + @NamedEntityGraph( + name = "Post.details", + attributeNodes = @NamedAttributeNode("details") + ) + @NamedEntityGraph( + name = "Post.all", + attributeNodes = { + @NamedAttributeNode("details"), + @NamedAttributeNode(value = "comments", subgraph = "Post.comment.details"), + }, + subgraphs = @NamedSubgraph( + name = "Post.comment.details", + attributeNodes = @NamedAttributeNode("details") + ) + ) + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY + ) + private PostDetails details; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @NamedEntityGraph( + name = "PostComment.post", + attributeNodes = @NamedAttributeNode("post") + ) + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + @OneToOne(mappedBy = "comment", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + private PostCommentDetails details; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public PostCommentDetails getDetails() { + return details; + } + + public PostComment setDetails(PostCommentDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setComment(null); + } + } + else { + details.setComment(this); + } + this.details = details; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } + + @Entity(name = "PostCommentDetails") + @Table(name = "post_comment_details") + public static class PostCommentDetails { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private PostComment comment; + + @Column(name = "view_count") + private int viewCount; + + public Long getId() { + return id; + } + + public PostCommentDetails setId(Long id) { + this.id = id; + return this; + } + + public PostComment getComment() { + return comment; + } + + public PostCommentDetails setComment(PostComment comment) { + this.comment = comment; + return this; + } + + public int getViewCount() { + return viewCount; + } + + public PostCommentDetails setViewCount(int views) { + this.viewCount = views; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/ExtraLazyCollectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/ExtraLazyCollectionTest.java new file mode 100644 index 000000000..517ef453f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/ExtraLazyCollectionTest.java @@ -0,0 +1,182 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ExtraLazyCollectionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("Excellent book to understand Java persistence") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("The best JPA ORM book out there") + ) + .addComment( + new PostComment() + .setId(3L) + .setReview("Must-read for Java developers") + ) + ); + }); + + LOGGER.info("Fetch comments with for-each loop"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + for (PostComment comment: post.getComments()) { + LOGGER.info("{} book review: {}", + post.getTitle(), + comment.getReview() + ); + } + }); + + LOGGER.info("Fetch comments with for loop"); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + int commentCount = post.getComments().size(); + + for(int i = 0; i < commentCount; i++ ) { + PostComment comment = post.getComments().get(i); + LOGGER.info("{} book review: {}", + post.getTitle(), + comment.getReview() + ); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + @LazyCollection(LazyCollectionOption.EXTRA) + @OrderColumn(name = "order_id") + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/FetchAllAssociationsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FetchAllAssociationsTest.java similarity index 91% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/FetchAllAssociationsTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FetchAllAssociationsTest.java index 914575704..997a19704 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/FetchAllAssociationsTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FetchAllAssociationsTest.java @@ -1,29 +1,28 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; +package com.vladmihalcea.hpjp.hibernate.fetching; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.stream.LongStream; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.JoinTable; -import javax.persistence.ManyToMany; -import javax.persistence.ManyToOne; -import javax.persistence.MapsId; -import javax.persistence.OneToMany; -import javax.persistence.OneToOne; -import javax.persistence.Table; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; import org.hibernate.Hibernate; -import org.hibernate.jpa.QueryHints; import org.junit.Test; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -203,7 +202,6 @@ public PostDetails() { } @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") @MapsId private Post post; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FetchTypeEagerManyToOneEntityGraphTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FetchTypeEagerManyToOneEntityGraphTest.java new file mode 100644 index 000000000..c92410a42 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FetchTypeEagerManyToOneEntityGraphTest.java @@ -0,0 +1,216 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.Hibernate; +import org.hibernate.LazyInitializationException; +import org.hibernate.jpa.SpecHints; +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class FetchTypeEagerManyToOneEntityGraphTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + + entityManager.persist( + new PostComment() + .setPost(post) + .setReview("The first part is about JDBC") + ); + + entityManager.persist( + new PostComment() + .setPost(post) + .setReview("The second part is about JPA") + ); + + entityManager.persist( + new PostComment() + .setPost(post) + .setReview("The third part is about jOOQ") + ); + }); + } + + @Test + public void testFindById() { + PostComment comment = doInJPA(entityManager -> { + return entityManager.find(PostComment.class, 1L); + }); + + assertTrue(Hibernate.isInitialized(comment.getPost())); + assertEquals( + "High-Performance Java Persistence", + comment.getPost().getTitle() + ); + } + + @Test + public void testQueryById() { + PostComment comment = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select pc + from PostComment pc + where pc.id = :id + """, PostComment.class) + .setParameter("id", 1L) + .getSingleResult(); + }); + + assertTrue(Hibernate.isInitialized(comment.getPost())); + assertEquals( + "High-Performance Java Persistence", + comment.getPost().getTitle() + ); + } + + @Test + public void testFindByIdFetchGraphOverridesFetchTypeEager() { + PostComment comment = doInJPA(entityManager -> { + return entityManager.find(PostComment.class, 1L, + Map.of( + SpecHints.HINT_SPEC_FETCH_GRAPH, + entityManager.createEntityGraph(PostComment.class) + ) + ); + }); + assertFalse(Hibernate.isInitialized(comment.getPost())); + try { + comment.getPost().getTitle(); + + fail("Should throw LazyInitializationException"); + } catch(LazyInitializationException expected) {} + } + + @Test + public void testQueryByIdFetchGraphOverridesFetchTypeEager() { + PostComment comment = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select pc + from PostComment pc + where pc.id = :id + """, PostComment.class) + .setHint( + SpecHints.HINT_SPEC_FETCH_GRAPH, + entityManager.createEntityGraph(PostComment.class) + ) + .setParameter("id", 1L) + .getSingleResult(); + }); + assertFalse(Hibernate.isInitialized(comment.getPost())); + try { + comment.getPost().getTitle(); + + fail("Should throw LazyInitializationException"); + } catch(LazyInitializationException expected) {} + } + + @Test + public void testLoadGraphDoesNotOverrideFetchTypeEager() { + PostComment comment = doInJPA(entityManager -> { + return entityManager.find(PostComment.class, 1L, + Map.of( + SpecHints.HINT_SPEC_LOAD_GRAPH, + entityManager.createEntityGraph(PostComment.class) + ) + ); + }); + assertTrue(Hibernate.isInitialized(comment.getPost())); + assertEquals( + "High-Performance Java Persistence", + comment.getPost().getTitle() + ); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @Column(length = 100) + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + @Column(length = 250) + private String review; + + @ManyToOne + private Post post; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FindByMultipleIdsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FindByMultipleIdsTest.java new file mode 100644 index 000000000..6f23c8c81 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FindByMultipleIdsTest.java @@ -0,0 +1,180 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.ParameterExpression; +import jakarta.persistence.criteria.Root; + +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class FindByMultipleIdsTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.query.in_clause_parameter_padding", "true"); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea") + ); + + entityManager.persist( + new Book() + .setIsbn("978-1934356555") + .setTitle("SQL Antipatterns") + .setAuthor("Bill Karwin") + ); + + entityManager.persist( + new Book() + .setIsbn("978-3950307825") + .setTitle("SQL Performance Explained") + .setAuthor("Markus Winand") + ); + + entityManager.persist( + new Book() + .setIsbn("978-1449373320") + .setTitle("Designing Data-Intensive Applications") + .setAuthor("Martin Kleppmann") + ); + }); + } + + @Test + public void testJPQL() { + doInJPA(entityManager -> { + List books = entityManager.createQuery(""" + select b + from Book b + where b.isbn in (:isbn) + """, Book.class) + .setParameter("isbn", Arrays.asList( + "978-9730228236", + "978-1934356555", + "978-3950307825" + )) + .getResultList(); + + assertEquals(3, books.size()); + }); + } + + @Test + public void testCriteriaAPI() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery query = builder.createQuery(Book.class); + Root root = query.from(Book.class); + + ParameterExpression isbn = builder.parameter(List.class); + query.where(root.get("isbn").in(isbn)); + + List books = entityManager + .createQuery(query) + .setParameter(isbn, Arrays.asList( + "978-9730228236", + "978-1934356555", + "978-3950307825" + )) + .getResultList(); + + assertEquals(3, books.size()); + }); + } + + @Test + public void testByMultipleIds() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + List books = session.byMultipleIds(Book.class) + .multiLoad( + "978-9730228236", + "978-1934356555", + "978-3950307825" + ); + + assertEquals(3, books.size()); + }); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + List books = session.byMultipleIds(Book.class) + .multiLoad( + List.of( + "978-9730228236", + "978-1934356555", + "978-3950307825" + ) + ); + + assertEquals(3, books.size()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + private String isbn; + + private String title; + + private String author; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/FindEntityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FindEntityTest.java similarity index 94% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/FindEntityTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FindEntityTest.java index cc94eb7bf..53baa6a48 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/FindEntityTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FindEntityTest.java @@ -1,10 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; +package com.vladmihalcea.hpjp.hibernate.fetching; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.hibernate.Session; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -17,15 +17,13 @@ public class FindEntityTest extends AbstractPostgreSQLIntegrationTest { @Override protected Class[] entities() { return new Class[]{ - Post.class, - PostComment.class, + Post.class, + PostComment.class, }; } - @Override - public void init() { - super.init(); + public void afterInit() { doInJPA(entityManager -> { Post post = new Post(); post.setTitle(String.format("Post nr. %d", 1)); diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FindVsGetReferenceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FindVsGetReferenceTest.java new file mode 100644 index 000000000..41a7faa1f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/FindVsGetReferenceTest.java @@ -0,0 +1,165 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.LazyInitializationException; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class FindVsGetReferenceTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + } + + @Test + public void testFind() { + doInJPA(entityManager -> { + PostComment comment = new PostComment(); + comment.setReview("Just awesome!"); + + Post post = entityManager.find(Post.class, 1L); + comment.setPost(post); + + entityManager.persist(comment); + }); + + Post post = doInJPA(entityManager -> { + return entityManager.find(Post.class, 1L); + }); + + assertEquals("High-Performance Java Persistence", post.getTitle()); + } + + @Test + public void testGetReference() { + doInJPA(entityManager -> { + PostComment comment = new PostComment(); + comment.setReview("Just awesome!"); + + Post post = entityManager.getReference(Post.class, 1L); + comment.setPost(post); + + entityManager.persist(comment); + }); + + Post post = doInJPA(entityManager -> { + return entityManager.getReference(Post.class, 1L); + }); + + try { + post.getTitle(); + + fail("Should throw LazyInitializationException"); + } catch (LazyInitializationException e) { + LOGGER.info("Failure expected", e); + } + } + + @Test + public void testDummyPojo() { + Post _post = doInJPA(entityManager -> { + PostComment comment = new PostComment(); + comment.setReview("Just awesome!"); + + Post post = new Post(); + post.setId(1L); + comment.setPost(post); + + entityManager.persist(comment); + + return post; + }); + + assertNull(_post.getTitle()); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/GroupByMapTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/GroupByMapTest.java new file mode 100644 index 000000000..25ae79dbc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/GroupByMapTest.java @@ -0,0 +1,255 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.transform.ResultTransformer; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class GroupByMapTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence eBook has been released!") + .setCreatedOn(LocalDate.of(2016, 8, 30)) + ); + + entityManager.persist( + new Post() + .setId(2L) + .setTitle("High-Performance Java Persistence paperback has been released!") + .setCreatedOn(LocalDate.of(2016, 10, 12)) + ); + + entityManager.persist( + new Post() + .setId(3L) + .setTitle("High-Performance Java Persistence Mach 1 video course has been released!") + .setCreatedOn(LocalDate.of(2018, 1, 30)) + ); + + entityManager.persist( + new Post() + .setId(4L) + .setTitle("High-Performance Java Persistence Mach 2 video course has been released!") + .setCreatedOn(LocalDate.of(2018, 5, 8)) + ); + + entityManager.persist( + new Post() + .setId(5L) + .setTitle("Hypersistence Optimizer has been released!") + .setCreatedOn(LocalDate.of(2019, 3, 19)) + ); + }); + } + + @Test + public void testGroupByStreamCollector() { + doInJPA(entityManager -> { + Map postCountByYearMap = entityManager + .createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from Post p " + + "group by " + + " YEAR(p.createdOn)", Tuple.class) + .getResultStream() + .collect( + Collectors.toMap( + tuple -> ((Number) tuple.get("year")).intValue(), + tuple -> ((Number) tuple.get("postCount")).intValue() + ) + ); + + assertEquals(2, postCountByYearMap.get(2016).intValue()); + assertEquals(2, postCountByYearMap.get(2018).intValue()); + assertEquals(1, postCountByYearMap.get(2019).intValue()); + }); + } + + @Test + public void testGroupByResultTransformer() { + doInJPA(entityManager -> { + Map postCountByYearMap = (Map) entityManager + .createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from Post p " + + "group by " + + " YEAR(p.createdOn)") + .unwrap(org.hibernate.query.Query.class) + .setResultTransformer( + new MapResultTransformer() + ) + .getSingleResult(); + + assertEquals(2, postCountByYearMap.get(2016).intValue()); + assertEquals(2, postCountByYearMap.get(2018).intValue()); + assertEquals(1, postCountByYearMap.get(2019).intValue()); + }); + } + + @Test + public void testGroupByIdStreamCollector() { + doInJPA(entityManager -> { + Map postByIdMap = entityManager + .createQuery( + "select p " + + "from Post p ", Post.class) + .getResultStream() + .collect( + Collectors.toMap( + Post::getId, + Function.identity() + ) + ); + + assertEquals( + "High-Performance Java Persistence eBook has been released!", + postByIdMap.get(1L).getTitle() + ); + + assertEquals( + "Hypersistence Optimizer has been released!", + postByIdMap.get(5L).getTitle() + ); + }); + } + + @Test + public void testGroupByIdResultTransformer() { + doInJPA(entityManager -> { + Map postByIdMap = (Map) entityManager + .createQuery( + "select p " + + "from Post p ") + .unwrap(org.hibernate.query.Query.class) + .setResultTransformer( + new ResultTransformer() { + + Map result = new HashMap<>(); + + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + Post post = (Post) tuple[0]; + result.put( + post.getId(), + post + ); + return tuple; + } + + @Override + public List transformList(List collection) { + return Collections.singletonList(result); + } + } + ) + .getSingleResult(); + + assertEquals( + "High-Performance Java Persistence eBook has been released!", + postByIdMap.get(1L).getTitle() + ); + + assertEquals( + "Hypersistence Optimizer has been released!", + postByIdMap.get(5L).getTitle() + ); + }); + } + + @FunctionalInterface + public interface ListResultTransformer extends ResultTransformer { + + @Override + default List transformList(List collection) { + return collection; + } + } + + public class MapResultTransformer implements ListResultTransformer { + + Map result = new HashMap<>(); + + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + K key = (K) tuple[0]; + V value = (V) tuple[1]; + result.put( + key, + value + ); + return tuple; + } + + @Override + public List transformList(List collection) { + return Collections.singletonList(result); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDate createdOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDate getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDate createdOn) { + this.createdOn = createdOn; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/HibernateInitializeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/HibernateInitializeTest.java new file mode 100644 index 000000000..f266976f4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/HibernateInitializeTest.java @@ -0,0 +1,342 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Hibernate; +import org.hibernate.Session; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.junit.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.*; + +public class HibernateInitializeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.cache.region.factory_class", "jcache"); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + post.addComment( + new PostComment() + .setId(1L) + .setReview("A must-read!") + ); + + post.addComment( + new PostComment() + .setId(2L) + .setReview("Awesome!") + ); + + post.addComment( + new PostComment() + .setId(3L) + .setReview("5 stars") + ); + + entityManager.persist(post); + }); + } + + @Test + public void testEntityProxyWithoutSecondLevelCache() { + + doInJPA(entityManager -> { + LOGGER.info("Clear the second-level cache"); + + entityManager.getEntityManagerFactory().getCache().evictAll(); + + LOGGER.info("Loading a PostComment"); + + PostComment comment = entityManager.find( + PostComment.class, + 1L + ); + + assertEquals( + "A must-read!", + comment.getReview() + ); + + Post post = comment.getPost(); + + LOGGER.info("Post entity class: {}", post.getClass().getName()); + + Hibernate.initialize(post); + + assertEquals( + "High-Performance Java Persistence", + post.getTitle() + ); + + Hibernate.initialize(post); + }); + } + + @Test + public void testEntityProxyJoinFetchWithoutSecondLevelCache() { + + doInJPA(entityManager -> { + LOGGER.info("Clear the second-level cache"); + + entityManager.getEntityManagerFactory().getCache().evictAll(); + + LOGGER.info("Loading a PostComment"); + + PostComment comment = entityManager.createQuery( + "select pc " + + "from PostComment pc " + + "join fetch pc.post " + + "where pc.id = :id", PostComment.class) + .setParameter("id", 1L) + .getSingleResult(); + + assertEquals( + "A must-read!", + comment.getReview() + ); + + Post post = comment.getPost(); + + LOGGER.info("Post entity class: {}", post.getClass().getName()); + + assertEquals( + "High-Performance Java Persistence", + post.getTitle() + ); + }); + } + + @Test + public void testEntityProxy() { + + doInJPA(entityManager -> { + LOGGER.info("Loading a PostComment"); + + PostComment comment = entityManager.find( + PostComment.class, + 1L + ); + + assertEquals( + "A must-read!", + comment.getReview() + ); + + Post post = comment.getPost(); + + LOGGER.info("Proxy class: {}", post.getClass().getName()); + + Hibernate.initialize(post); + + assertEquals( + "High-Performance Java Persistence", + post.getTitle() + ); + + Hibernate.initialize(post); + }); + } + + @Test + public void testCollectionProxy() { + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertEquals(3, post.getComments().size()); + }); + + doInJPA(entityManager -> { + LOGGER.info("Loading a Post"); + + Post post = entityManager.find( + Post.class, + 1L + ); + + List comments = post.getComments(); + + LOGGER.info("Collection class: {}", comments.getClass().getName()); + + Hibernate.initialize(comments); + + LOGGER.info("Post comments: {}", comments); + }); + } + + @Test + public void testDetachedProxy() { + Post post = doInJPA(entityManager -> { + LOGGER.info("Loading a Post"); + + return entityManager.find( + Post.class, + 1L + ); + }); + + doInJPA(entityManager -> { + + entityManager.unwrap(Session.class).update(post); + + List comments = post.getComments(); + + LOGGER.info("Collection class: {}", comments.getClass().getName()); + + Hibernate.initialize(comments); + + LOGGER.info("Post comments: {}", comments); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Post)) return false; + return id != null && id.equals(((Post) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class PostComment { + + @Id + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostComment)) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public String toString() { + return "PostComment{" + + "id=" + id + + ", review='" + review + '\'' + + '}'; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/HibernateProxyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/HibernateProxyTest.java new file mode 100644 index 000000000..9b567c8c0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/HibernateProxyTest.java @@ -0,0 +1,145 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Hibernate; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.*; + +public class HibernateProxyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Test + public void test() { + Post _post = doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + return post; + }); + + doInJPA(entityManager -> { + LOGGER.info("Saving a PostComment"); + + Post post = entityManager.getReference(Post.class, 1L); + + PostComment comment = new PostComment(); + comment.setId(1L); + comment.setPost(post); + comment.setReview("A must-read!"); + entityManager.persist(comment); + }); + + doInJPA(entityManager -> { + LOGGER.info("Loading a PostComment"); + + PostComment comment = entityManager.find( + PostComment.class, + 1L + ); + + LOGGER.info("Loading the Post Proxy"); + + assertEquals( + "High-Performance Java Persistence", + comment.getPost().getTitle() + ); + }); + + doInJPA(entityManager -> { + LOGGER.info("Equality check"); + Post post = entityManager.getReference(Post.class, 1L); + LOGGER.info("Post entity class: {}", post.getClass().getName()); + + assertNotEquals(_post.getClass(), post.getClass()); + + assertEquals(_post.getClass(), Hibernate.unproxy(post).getClass()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Post)) return false; + //Intentionally uses field to prove how Proxy works + return id != null && id.equals(((Post) o).id); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyAttributeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyAttributeTest.java new file mode 100644 index 000000000..63b3e18bf --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyAttributeTest.java @@ -0,0 +1,87 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.hibernate.forum.Attachment; +import com.vladmihalcea.hpjp.hibernate.forum.MediaType; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Properties; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class LazyAttributeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Attachment.class, + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected void additionalProperties(Properties properties) { + //properties.setProperty(AvailableSettings.USE_STREAMS_FOR_BINARY, Boolean.FALSE.toString()); + } + + @Override + protected void afterInit() { + executeStatement("ALTER TABLE attachment MODIFY content LONGTEXT"); + } + + @Test + public void test() throws URISyntaxException { + final Path bookFilePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource("ehcache.xml").toURI()); + final Path videoFilePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource("spy.properties").toURI()); + + doInJPA(entityManager -> { + try { + entityManager.persist( + new Attachment() + .setId(1L) + .setName("High-Performance Java Persistence") + .setMediaType(MediaType.PDF) + .setContent(Files.readAllBytes(bookFilePath)) + ); + + entityManager.persist( + new Attachment() + .setId(2L) + .setName("High-Performance Java Persistence - Mach 2") + .setMediaType(MediaType.MPEG_VIDEO) + .setContent(Files.readAllBytes(videoFilePath)) + ); + } catch (IOException e) { + fail(e.getMessage()); + } + }); + + doInJPA(entityManager -> { + try { + Attachment book = entityManager.find(Attachment.class, 1L); + LOGGER.debug("Fetched book: {}", book.getName()); + assertArrayEquals(Files.readAllBytes(bookFilePath), book.getContent()); + + Attachment video = entityManager.find(Attachment.class, 2L); + LOGGER.debug("Fetched video: {}", video.getName()); + assertArrayEquals(Files.readAllBytes(videoFilePath), video.getContent()); + } catch (IOException e) { + fail(e.getMessage()); + } + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyAttributeWithMultipleEntitiesTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyAttributeWithMultipleEntitiesTest.java similarity index 88% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyAttributeWithMultipleEntitiesTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyAttributeWithMultipleEntitiesTest.java index 24aa92fc4..3871c0f57 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyAttributeWithMultipleEntitiesTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyAttributeWithMultipleEntitiesTest.java @@ -1,10 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; +package com.vladmihalcea.hpjp.hibernate.fetching; -import com.vladmihalcea.book.hpjp.hibernate.forum.MediaType; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.hibernate.forum.MediaType; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; @@ -18,7 +19,7 @@ /** * @author Vlad Mihalcea */ -public class LazyAttributeWithMultipleEntitiesTest extends AbstractMySQLIntegrationTest { +public class LazyAttributeWithMultipleEntitiesTest extends AbstractTest { @Override protected Class[] entities() { @@ -28,6 +29,16 @@ protected Class[] entities() { }; } + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected void afterInit() { + executeStatement("ALTER TABLE attachment MODIFY content LONGTEXT"); + } + @Test public void test() throws URISyntaxException { final Path bookFilePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource("ehcache.xml").toURI()); diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyFetchingManyToOneFindEntityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyFetchingManyToOneFindEntityTest.java new file mode 100644 index 000000000..7e0f25d04 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyFetchingManyToOneFindEntityTest.java @@ -0,0 +1,252 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.hibernate.forum.PostComment_; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.LazyInitializationException; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class LazyFetchingManyToOneFindEntityTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Test + public void testFind() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle(String.format("Post nr. %d", 1)); + PostComment comment = new PostComment(); + comment.setId(1L); + comment.setPost(post); + comment.setReview("Excellent!"); + entityManager.persist(post); + entityManager.persist(comment); + }); + + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + + LOGGER.info("Loaded comment entity"); + LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); + assertNotNull(comment); + }); + + doInJPA(entityManager -> { + LOGGER.info("Using custom entity graph"); + + EntityGraph postEntityGraph = entityManager.createEntityGraph( + PostComment.class); + postEntityGraph.addAttributeNodes("post"); + + PostComment comment = entityManager.find(PostComment.class, 1L, + Collections.singletonMap("jakarta.persistence.fetchgraph", postEntityGraph) + ); + LOGGER.info("Fetch entity graph"); + assertNotNull(comment); + }); + + doInJPA(entityManager -> { + LOGGER.info("Using JPQL"); + + PostComment comment = entityManager.createQuery(""" + select pc + from PostComment pc + join fetch pc.post p + where pc.id = :id + """, PostComment.class) + .setParameter("id", 1L) + .getSingleResult(); + assertNotNull(comment); + }); + } + + @Test + public void testNPlusOne() { + + String review = "Excellent!"; + + doInJPA(entityManager -> { + + for (long i = 1; i <= 3; i++) { + Post post = new Post(); + post.setId(i); + post.setTitle(String.format("Post nr. %d", i)); + entityManager.persist(post); + + PostComment comment = new PostComment(); + comment.setId(i); + comment.setPost(post); + comment.setReview(review); + entityManager.persist(comment); + } + }); + + doInJPA(entityManager -> { + LOGGER.info("N+1 query problem"); + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + where pc.review = :review + """, PostComment.class) + .setParameter("review", review) + .getResultList(); + + LOGGER.info("Loaded {} comments", comments.size()); + + for(PostComment comment : comments) { + LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); + } + }); + + doInJPA(entityManager -> { + LOGGER.info("N+1 query problem fixed"); + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + join fetch pc.post p + where pc.review = :review + """, PostComment.class) + .setParameter("review", review) + .getResultList(); + LOGGER.info("Loaded {} comments", comments.size()); + for(PostComment comment : comments) { + LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); + } + }); + } + + @Test(expected = LazyInitializationException.class) + public void testSessionIsClosed() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle(String.format("Post nr. %d", 1)); + PostComment comment = new PostComment(); + comment.setId(1L); + comment.setPost(post); + comment.setReview("Excellent!"); + entityManager.persist(post); + entityManager.persist(comment); + }); + + PostComment comment = null; + + EntityManager entityManager = null; + EntityTransaction transaction = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + transaction = entityManager.getTransaction(); + transaction.begin(); + + comment = entityManager.find(PostComment.class, 1L); + + transaction.commit(); + } catch (Throwable e) { + if ( transaction != null && transaction.isActive()) + transaction.rollback(); + throw e; + } finally { + if (entityManager != null) { + entityManager.close(); + } + } + + LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyInitializationExceptionFixTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyInitializationExceptionFixTest.java similarity index 93% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyInitializationExceptionFixTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyInitializationExceptionFixTest.java index 0b9247f2f..ea947a2b9 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyInitializationExceptionFixTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyInitializationExceptionFixTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; +package com.vladmihalcea.hpjp.hibernate.fetching; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.List; /** @@ -111,7 +111,6 @@ public void setTitle(String title) { @Entity(name = "PostComment") @Table(name = "post_comment") - @NamedEntityGraph(name = "PostComment.post", attributeNodes = {}) public static class PostComment { @Id diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyInitializationExceptionFixWithDTOTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyInitializationExceptionFixWithDTOTest.java new file mode 100644 index 000000000..8e1317b2a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyInitializationExceptionFixWithDTOTest.java @@ -0,0 +1,157 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import jakarta.persistence.*; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class LazyInitializationExceptionFixWithDTOTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator(Collections.singletonList(PostCommentDTO.class)) + ) + ); + } + + @Test + public void testNPlusOne() { + + String review = "Excellent!"; + + doInJPA(entityManager -> { + + for (long i = 1; i < 4; i++) { + Post post = new Post(); + post.setId(i); + post.setTitle(String.format("Post nr. %d", i)); + entityManager.persist(post); + + PostComment comment = new PostComment(); + comment.setId(i); + comment.setPost(post); + comment.setReview(review); + entityManager.persist(comment); + } + }); + + List comments = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select new + PostCommentDTO( + pc.id, pc.review, p.title + ) + from PostComment pc + join pc.post p + where pc.review = :review + """, PostCommentDTO.class) + .setParameter("review", review) + .getResultList(); + }); + + for(PostCommentDTO comment : comments) { + LOGGER.info("The post title is '{}'", comment.getTitle()); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyInitializationExceptionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyInitializationExceptionTest.java new file mode 100644 index 000000000..874e6d1e9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyInitializationExceptionTest.java @@ -0,0 +1,191 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.LazyInitializationException; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class LazyInitializationExceptionTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + private String review = "Excellent!"; + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + for (long i = 1; i < 4; i++) { + Post post = new Post(); + post.setId(i); + post.setTitle(String.format("Post nr. %d", i)); + entityManager.persist(post); + + PostComment comment = new PostComment(); + comment.setId(i); + comment.setPost(post); + comment.setReview(review); + entityManager.persist(comment); + } + }); + } + + @Test + public void testNPlusOne() { + + List comments = null; + + EntityManager entityManager = null; + EntityTransaction transaction = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + transaction = entityManager.getTransaction(); + transaction.begin(); + + comments = entityManager.createQuery( + "select pc " + + "from PostComment pc " + + "where pc.review = :review", PostComment.class) + .setParameter("review", review) + .getResultList(); + + transaction.commit(); + } catch (Throwable e) { + if ( transaction != null && transaction.isActive()) + transaction.rollback(); + throw e; + } finally { + if (entityManager != null) { + entityManager.close(); + } + } + try { + for(PostComment comment : comments) { + LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); + } + } catch (LazyInitializationException expected) { + assertTrue(expected.getMessage().toLowerCase(Locale.ROOT).contains("could not initialize proxy")); + } + } + + @Test + public void testNPlusOneSimplified() { + + List comments = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select pc + from PostComment pc + where pc.review = :review + """, PostComment.class) + .setParameter("review", review) + .getResultList(); + }); + + try { + for(PostComment comment : comments) { + LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); + } + } catch (LazyInitializationException expected) { + assertTrue( + expected.getMessage().toLowerCase(Locale.ROOT).contains( + "could not initialize proxy" + ) + ); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyInitializationOutsideTransactionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyInitializationOutsideTransactionTest.java similarity index 94% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyInitializationOutsideTransactionTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyInitializationOutsideTransactionTest.java index b90ae9b94..db87fd9e4 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/LazyInitializationOutsideTransactionTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/LazyInitializationOutsideTransactionTest.java @@ -1,10 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; +package com.vladmihalcea.hpjp.hibernate.fetching; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.hibernate.cfg.AvailableSettings; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.List; import java.util.Properties; @@ -119,7 +119,6 @@ public void setTitle(String title) { @Entity(name = "PostComment") @Table(name = "post_comment") - @NamedEntityGraph(name = "PostComment.post", attributeNodes = {}) public static class PostComment { @Id diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/MySQLScrollableResultsNoStreamingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/MySQLScrollableResultsNoStreamingTest.java similarity index 93% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/MySQLScrollableResultsNoStreamingTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/MySQLScrollableResultsNoStreamingTest.java index e6e7edaeb..b37e87f54 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/MySQLScrollableResultsNoStreamingTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/MySQLScrollableResultsNoStreamingTest.java @@ -1,18 +1,19 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; +package com.vladmihalcea.hpjp.hibernate.fetching; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.Timer; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; import org.hibernate.query.Query; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import javax.persistence.Entity; -import javax.persistence.EntityManager; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -84,6 +85,7 @@ public void init() { entityManager.persist(post); if(i % 50 == 0 && i > 0) { entityManager.flush(); + entityManager.clear(); } }); }); @@ -99,6 +101,7 @@ protected Properties properties() { } @Test + @Ignore public void testStream() { //warming up LOGGER.info("Warming up"); diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/MySQLScrollableResultsStreamingCustomFetchSizeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/MySQLScrollableResultsStreamingCustomFetchSizeTest.java new file mode 100644 index 000000000..2fcf1f97f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/MySQLScrollableResultsStreamingCustomFetchSizeTest.java @@ -0,0 +1,183 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; +import org.hibernate.jpa.AvailableHints; +import org.hibernate.query.Query; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.LongStream; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public class MySQLScrollableResultsStreamingCustomFetchSizeTest extends AbstractMySQLIntegrationTest { + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Timer timer = metricRegistry.timer(getClass().getSimpleName()); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .build(); + + private final int resultSetSize; + + public MySQLScrollableResultsStreamingCustomFetchSizeTest(Integer resultSetSize) { + this.resultSetSize = resultSetSize; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + MySQLDataSourceProvider dataSourceProvider = new MySQLDataSourceProvider(); + dataSourceProvider.setUseCursorFetch(true); + return dataSourceProvider; + } + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Parameterized.Parameters + public static Collection parameters() { + List providers = new ArrayList<>(); + providers.add(new Integer[]{1}); + providers.add(new Integer[]{2}); + providers.add(new Integer[]{5}); + providers.add(new Integer[]{10}); + providers.add(new Integer[]{25}); + providers.add(new Integer[]{50}); + providers.add(new Integer[]{75}); + providers.add(new Integer[]{100}); + providers.add(new Integer[]{250}); + providers.add(new Integer[]{500}); + providers.add(new Integer[]{750}); + providers.add(new Integer[]{1000}); + providers.add(new Integer[]{1500}); + providers.add(new Integer[]{2000}); + providers.add(new Integer[]{2500}); + providers.add(new Integer[]{5000}); + return providers; + } + + @Override + public void init() { + super.init(); + doInJPA(entityManager -> { + LongStream.range(0, 5000).forEach(i -> { + Post post = new Post(i); + post.setTitle(String.format("Post nr. %d", i)); + entityManager.persist(post); + if(i % 50 == 0 && i > 0) { + entityManager.flush(); + entityManager.clear(); + } + }); + }); + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + return properties; + } + + @Test + @Ignore + public void testStream() { + //warming up + LOGGER.info("Warming up"); + doInJPA(entityManager -> { + for (int i = 0; i < 25_000; i++) { + stream(entityManager); + } + }); + int iterations = 10_000; + doInJPA(entityManager -> { + for (int i = 0; i < iterations; i++) { + long startNanos = System.nanoTime(); + stream(entityManager); + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + } + }); + logReporter.report(); + } + + private void stream(EntityManager entityManager) { + final AtomicLong sum = new AtomicLong(); + try(Stream postStream = entityManager + .createQuery("select p from Post p", Post.class) + .setMaxResults(resultSetSize) + .setHint(AvailableHints.HINT_FETCH_SIZE, resultSetSize) + .unwrap(Query.class) + .stream()) { + postStream.forEach(post -> sum.incrementAndGet()); + } + assertEquals(resultSetSize, sum.get()); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/MySQLScrollableResultsStreamingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/MySQLScrollableResultsStreamingTest.java similarity index 91% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/MySQLScrollableResultsStreamingTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/MySQLScrollableResultsStreamingTest.java index 0eb34907d..599439359 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/MySQLScrollableResultsStreamingTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/MySQLScrollableResultsStreamingTest.java @@ -1,19 +1,20 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; +package com.vladmihalcea.hpjp.hibernate.fetching; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.Timer; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import org.hibernate.jpa.QueryHints; +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.jpa.AvailableHints; import org.hibernate.query.Query; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import javax.persistence.Entity; -import javax.persistence.EntityManager; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -85,6 +86,7 @@ public void init() { entityManager.persist(post); if(i % 50 == 0 && i > 0) { entityManager.flush(); + entityManager.clear(); } }); }); @@ -100,6 +102,7 @@ protected Properties properties() { } @Test + @Ignore public void testStream() { //warming up LOGGER.info("Warming up"); @@ -124,7 +127,7 @@ private void stream(EntityManager entityManager) { try(Stream postStream = entityManager .createQuery("select p from Post p", Post.class) .setMaxResults(resultSetSize) - .setHint(QueryHints.HINT_FETCH_SIZE, Integer.MIN_VALUE) + .setHint(AvailableHints.HINT_FETCH_SIZE, Integer.MIN_VALUE) .unwrap(Query.class) .stream()) { postStream.forEach(post -> sum.incrementAndGet()); diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneEagerFetchingManyToOneFindEntityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneEagerFetchingManyToOneFindEntityTest.java new file mode 100644 index 000000000..9b3a0ce27 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneEagerFetchingManyToOneFindEntityTest.java @@ -0,0 +1,147 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class NPlusOneEagerFetchingManyToOneFindEntityTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Test + public void testNPlusOne() { + + doInJPA(entityManager -> { + + String[] reviews = new String[] { + "Excellent book to understand Java Persistence", + "Must-read for Java developers", + "Five Stars", + "A great reference book" + }; + + for (int i = 0; i < 4; i++) { + long id = i + 1; + + Post post = new Post() + .setId(id) + .setTitle(String.format("High-Performance Java Persistence - Part %d", id)); + + entityManager.persist(post); + + entityManager.persist( + new PostComment() + .setId(id) + .setPost(post) + .setReview(reviews[i]) + ); + } + }); + + doInJPA(entityManager -> { + LOGGER.info("N+1 query problem"); + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + """, PostComment.class) + .getResultList(); + + LOGGER.info("Loaded {} comments", comments.size()); + }); + + doInJPA(entityManager -> { + LOGGER.info("N+1 query problem fixed"); + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + join fetch pc.post p + """, PostComment.class) + .getResultList(); + + LOGGER.info("Loaded {} comments", comments.size()); + + for(PostComment comment : comments) { + LOGGER.info("The Post '{}' got this review '{}'", comment.getPost().getTitle(), comment.getReview()); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneLazyFetchingManyToOneFindEntityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneLazyFetchingManyToOneFindEntityTest.java new file mode 100644 index 000000000..1ff0b4e8f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneLazyFetchingManyToOneFindEntityTest.java @@ -0,0 +1,152 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class NPlusOneLazyFetchingManyToOneFindEntityTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Test + public void testNPlusOne() { + + doInJPA(entityManager -> { + + String[] reviews = new String[] { + "Excellent book to understand Java Persistence", + "Must-read for Java developers", + "Five Stars", + "A great reference book" + }; + + for (int i = 0; i < 4; i++) { + long id = i + 1; + + Post post = new Post() + .setId(id) + .setTitle(String.format("High-Performance Java Persistence - Part %d", id)); + + entityManager.persist(post); + + entityManager.persist( + new PostComment() + .setId(id) + .setPost(post) + .setReview(reviews[i]) + ); + } + }); + + doInJPA(entityManager -> { + LOGGER.info("N+1 query problem"); + List comments = entityManager + .createQuery(""" + select pc + from PostComment pc + """, PostComment.class) + .getResultList(); + + LOGGER.info("Loaded {} comments", comments.size()); + + for(PostComment comment : comments) { + LOGGER.info("The Post '{}' got this review '{}'", comment.getPost().getTitle(), comment.getReview()); + } + }); + + doInJPA(entityManager -> { + LOGGER.info("N+1 query problem fixed"); + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + join fetch pc.post p + """, PostComment.class) + .getResultList(); + + LOGGER.info("Loaded {} comments", comments.size()); + + for(PostComment comment : comments) { + LOGGER.info("The Post '{}' got this review '{}'", comment.getPost().getTitle(), comment.getReview()); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NPlusOneLazyFetchingWithSubselectManyToOneFindEntityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneLazyFetchingWithSubselectManyToOneFindEntityTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NPlusOneLazyFetchingWithSubselectManyToOneFindEntityTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneLazyFetchingWithSubselectManyToOneFindEntityTest.java index 0af1f4fd9..980b8e598 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NPlusOneLazyFetchingWithSubselectManyToOneFindEntityTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneLazyFetchingWithSubselectManyToOneFindEntityTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; +package com.vladmihalcea.hpjp.hibernate.fetching; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.annotations.Fetch; import org.hibernate.annotations.FetchMode; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; @@ -144,7 +144,6 @@ public List getTags() { @Entity(name = "PostComment") @Table(name = "post_comment") - @NamedEntityGraph(name = "PostComment.post", attributeNodes = {}) public static class PostComment { @Id diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneManyToOneEagerTechingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneManyToOneEagerTechingTest.java new file mode 100644 index 000000000..03665634c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneManyToOneEagerTechingTest.java @@ -0,0 +1,162 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.junit.Test; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class NPlusOneManyToOneEagerTechingTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Test + public void testNPlusOne() { + + String review = "Excellent!"; + + doInJPA(entityManager -> { + + for (long i = 1; i <= 3; i++) { + Post post = new Post(); + post.setId(i); + post.setTitle(String.format("Post nr. %d", i)); + entityManager.persist(post); + + PostComment comment = new PostComment(); + comment.setId(i); + comment.setPost(post); + comment.setReview(review); + entityManager.persist(comment); + } + }); + + doInJPA(entityManager -> { + LOGGER.info("N+1 query problem"); + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + where pc.review = :review + """, PostComment.class) + .setParameter("review", review) + .getResultList(); + + LOGGER.info("Loaded {} comments", comments.size()); + + for (PostComment comment : comments) { + LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); + } + }); + + doInJPA(entityManager -> { + LOGGER.info("N+1 query problem fixed"); + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + join fetch pc.post p + where pc.review = :review + """, PostComment.class) + .setParameter("review", review) + .getResultList(); + + LOGGER.info("Loaded {} comments", comments.size()); + + for (PostComment comment : comments) { + LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneSQLFetchingFKTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneSQLFetchingFKTest.java new file mode 100644 index 000000000..4fc6bf02c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NPlusOneSQLFetchingFKTest.java @@ -0,0 +1,175 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class NPlusOneSQLFetchingFKTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testNPlusOne() { + + doInJPA(entityManager -> { + + String[] reviews = new String[] { + "Excellent book to understand Java Persistence", + "Must-read for Java developers", + "Five Stars", + "A great reference book" + }; + + for (int i = 0; i < 4; i++) { + long id = i + 1; + + Post post = new Post() + .setId(id) + .setTitle(String.format("High-Performance Java Persistence - Part %d", id)); + + entityManager.persist(post); + + entityManager.persist( + new PostComment() + .setId(id) + .setPost(post) + .setReview(reviews[i]) + ); + } + }); + + doInJPA(entityManager -> { + LOGGER.info("N+1 query problem"); + List comments = entityManager.createNativeQuery(""" + SELECT + pc.id AS id, + pc.review AS review, + pc.post_id AS postId + FROM post_comment pc + """, Tuple.class) + .getResultList(); + + for (Tuple comment : comments) { + String review = (String) comment.get("review"); + Long postId = ((Number) comment.get("postId")).longValue(); + + String postTitle = (String) entityManager.createNativeQuery(""" + SELECT + p.title + FROM post p + WHERE p.id = :postId + """) + .setParameter("postId", postId) + .getSingleResult(); + + LOGGER.info("The Post '{}' got this review '{}'", postTitle, review); + } + }); + + doInJPA(entityManager -> { + LOGGER.info("N+1 query problem fixed"); + + List comments = entityManager.createNativeQuery(""" + SELECT + pc.id AS id, + pc.review AS review, + p.title AS postTitle + FROM post_comment pc + JOIN post p ON pc.post_id = p.id + """, Tuple.class) + .getResultList(); + + for (Tuple comment : comments) { + String review = (String) comment.get("review"); + String postTitle = (String) comment.get("postTitle"); + + LOGGER.info("The Post '{}' got this review '{}'", postTitle, review); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NamedNativeQueryParameterTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NamedNativeQueryParameterTest.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NamedNativeQueryParameterTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NamedNativeQueryParameterTest.java index 158811d08..e09591f49 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NamedNativeQueryParameterTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NamedNativeQueryParameterTest.java @@ -1,10 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; +package com.vladmihalcea.hpjp.hibernate.fetching; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.Session; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; import java.util.stream.LongStream; @@ -51,11 +51,13 @@ public void testWithParameters() { @Entity(name = "Post") @Table(name = "post") @NamedNativeQuery( - name = "findPostCommentsByPostTitle", - query = "select c.* " + - "from post_comment c " + - "where c.id > :id ", - resultClass = PostComment.class + name = "findPostCommentsByPostTitle", + query = """ + select c.* + from post_comment c + where c.id > :id + """, + resultClass = PostComment.class ) public static class Post { diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NamedQueryPerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NamedQueryPerformanceTest.java new file mode 100644 index 000000000..f9d4c276b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NamedQueryPerformanceTest.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import org.hibernate.Session; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; + +/** + * @author Vlad Mihalcea + */ +public class NamedQueryPerformanceTest extends PlanCacheSizePerformanceTest { + + public static final String QUERY_NAME_1 = "findPostCommentSummary"; + public static final String QUERY_NAME_2 = "findPostComments"; + + public NamedQueryPerformanceTest(int planCacheMaxSize) { + super(planCacheMaxSize); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManagerFactory().addNamedQuery(QUERY_NAME_1, getEntityQuery1(entityManager)); + entityManagerFactory().addNamedQuery(QUERY_NAME_2, getEntityQuery2(entityManager)); + }); + } + + @Override + protected Query getEntityQuery1(EntityManager entityManager) { + Session session = entityManager.unwrap(Session.class); + return session.getNamedQuery(QUERY_NAME_1); + } + + @Override + protected Query getEntityQuery2(EntityManager entityManager) { + Session session = entityManager.unwrap(Session.class); + return session.getNamedQuery(QUERY_NAME_2); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NativeQueryTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NativeQueryTest.java similarity index 79% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NativeQueryTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NativeQueryTest.java index 838155893..db68a465c 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/NativeQueryTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NativeQueryTest.java @@ -1,11 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; +package com.vladmihalcea.hpjp.hibernate.fetching; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.Session; -import org.hibernate.transform.AliasToBeanResultTransformer; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; import java.util.stream.LongStream; @@ -47,31 +46,26 @@ public void testPagination() { int pageStart = 20; int pageSize = 10; - + doInJPA(entityManager -> { - List summaries = entityManager.createNamedQuery( - "PostCommentSummary") + List summaries = entityManager.createNativeQuery(""" + SELECT p.title + FROM post p + ORDER BY p.id + """) .setFirstResult(pageStart) .setMaxResults(pageSize) .getResultList(); - assertEquals(10, summaries.size()); - }); - doInJPA(entityManager -> { - Session session = entityManager.unwrap(Session.class); - List summaries = session.createSQLQuery( - "SELECT p.id as id, p.title as title, c.review as review " + - "FROM post_comment c " + - "JOIN post p ON c.post_id = p.id " + - "ORDER BY p.id") - .setFirstResult(pageStart) - .setMaxResults(pageSize) - .setResultTransformer(new AliasToBeanResultTransformer(PostCommentSummary.class)) - .list(); assertEquals(pageSize, summaries.size()); }); } + @Override + protected Database database() { + return Database.ORACLE; + } + @NamedNativeQuery( name = "PostCommentSummary", query = diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NaturalIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NaturalIdTest.java new file mode 100644 index 000000000..b3717aab8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/NaturalIdTest.java @@ -0,0 +1,240 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NaturalIdCache; +import org.junit.Test; + +import jakarta.persistence.*; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import java.util.List; +import java.util.Objects; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class NaturalIdTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.cache.use_second_level_cache", Boolean.TRUE.toString()); + properties.put("hibernate.cache.region.factory_class", "jcache"); + return properties; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java persistence"); + post.setSlug("high-performance-java-persistence"); + entityManager.persist(post); + }); + } + + @Test + public void testFindBySimpleNaturalId() { + doInJPA(entityManager -> { + String slug = "high-performance-java-persistence"; + + Post post = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Post.class) + .load(slug); + + printNaturalIdCacheRegionStatistics(Post.class); + + assertNotNull(post); + }); + } + + @Test + public void testFindByNaturalId() { + doInJPA(entityManager -> { + String slug = "high-performance-java-persistence"; + + Post post = entityManager.unwrap(Session.class) + .byNaturalId(Post.class) + .using("slug", slug) + .load(); + + assertNotNull(post); + }); + } + + @Test + public void testFindPostWithJPQL() { + doInJPA(entityManager -> { + String slug = "high-performance-java-persistence"; + + Post post = entityManager.createQuery(""" + select p + from Post p + where p.slug = :slug + """, Post.class) + .setParameter("slug", slug) + .getSingleResult(); + + assertNotNull(post); + }); + } + + @Test + public void testFindAllPostsWithJPQL() { + doInJPA(entityManager -> { + List slugs = List.of( + "high-performance-java-persistence" + ); + + List posts = entityManager.createQuery(""" + select p + from Post p + where p.slug in (:slugs) + """, Post.class) + .setParameter("slugs", slugs) + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + @Test + public void testFindPostWithSQL() { + doInJPA(entityManager -> { + String slug = "high-performance-java-persistence"; + + Post post = (Post) entityManager.createNativeQuery(""" + SELECT * + FROM post + WHERE slug = :slug + """, Post.class) + .setParameter("slug", slug) + .getSingleResult(); + + assertNotNull(post); + }); + } + + @Test + public void testFindAllPostsWithSQL() { + doInJPA(entityManager -> { + List slugs = List.of( + "high-performance-java-persistence" + ); + + List posts = entityManager.createNativeQuery(""" + SELECT * + FROM post + WHERE slug IN (:slugs) + """, Post.class) + .setParameter("slugs", slugs) + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + @Test + public void testFindWithCriteriaAPI() { + doInJPA(entityManager -> { + String slug = "high-performance-java-persistence"; + + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteria = builder.createQuery(Post.class); + Root p = criteria.from(Post.class); + criteria.where(builder.equal(p.get("slug"), slug)); + Post post = entityManager.createQuery(criteria).getSingleResult(); + + assertNotNull(post); + }); + } + + @Test + public void testGetReferenceByNaturalId() { + doInJPA(entityManager -> { + String slug = "high-performance-java-persistence"; + Session session = entityManager.unwrap(Session.class); + LOGGER.info("Loading a post by natural identifier"); + Post post = session.bySimpleNaturalId(Post.class).getReference(slug); + LOGGER.info("Proxy is loaded"); + LOGGER.info("Post title is {}", post.getTitle()); + assertNotNull(post); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @NaturalIdCache + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @NaturalId + @Column(nullable = false, unique = true) + private String slug; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Post post = (Post) o; + return Objects.equals(slug, post.getSlug()); + } + + @Override + public int hashCode() { + return Objects.hash(slug); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/PlanCacheSizePerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/PlanCacheSizePerformanceTest.java new file mode 100644 index 000000000..aeaa9aa08 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/PlanCacheSizePerformanceTest.java @@ -0,0 +1,270 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.codahale.metrics.UniformReservoir; +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.jpa.AvailableHints; +import org.hibernate.query.*; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import jakarta.persistence.*; +import jakarta.persistence.Query; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.LongStream; + +/** + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public class PlanCacheSizePerformanceTest extends AbstractTest { + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Timer timer = new Timer(new UniformReservoir(10000)); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .convertDurationsTo(TimeUnit.MICROSECONDS) + .build(); + + private final int planCacheMaxSize; + + public PlanCacheSizePerformanceTest(int planCacheMaxSize) { + this.planCacheMaxSize = planCacheMaxSize; + } + + @Parameterized.Parameters + public static Collection rdbmsDataSourceProvider() { + List planCacheMaxSizes = new ArrayList<>(); + planCacheMaxSizes.add(new Integer[] {1}); + planCacheMaxSizes.add(new Integer[] {100}); + return planCacheMaxSizes; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.query.plan_cache_max_size", + planCacheMaxSize + ); + + properties.put( + "hibernate.query.plan_parameter_metadata_max_size", + planCacheMaxSize + ); + } + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + + @Override + public void init() { + metricRegistry.register(getClass().getSimpleName(), timer); + super.init(); + int commentsSize = 5; + doInJPA(entityManager -> { + LongStream.range(0, 50).forEach(i -> { + Post post = new Post(); + post.setId(i); + post.setTitle(String.format("Post nr. %d", i)); + + LongStream.range(0, commentsSize).forEach(j -> { + PostComment comment = new PostComment(); + comment.setId((i * commentsSize) + j); + comment.setReview(String.format("Good review nr. %d", comment.getId())); + post.addComment(comment); + + }); + entityManager.persist(post); + }); + }); + } + + @Test + @Ignore + public void testEntityQueries() { + compileQueries(this::getEntityQuery1, this::getEntityQuery2); + } + + @Test + @Ignore + public void testNativeQueries() { + compileQueries(this::getNativeQuery1, this::getNativeQuery2); + } + + protected void compileQueries( + Function query1, + Function query2) { + + LOGGER.info("Warming up"); + + doInJPA(entityManager -> { + for (int i = 0; i < 10000; i++) { + query1.apply(entityManager); + query2.apply(entityManager); + } + }); + + LOGGER.info("Compile queries for plan cache size {}", planCacheMaxSize); + + doInJPA(entityManager -> { + for (int i = 0; i < 2500; i++) { + long startNanos = System.nanoTime(); + query1.apply(entityManager); + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + + startNanos = System.nanoTime(); + query2.apply(entityManager); + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + } + }); + + logReporter.report(); + } + + protected Query getEntityQuery1(EntityManager entityManager) { + return entityManager.createQuery( + "select new " + + " com.vladmihalcea.book.hpjp.hibernate.fetching.PostCommentSummary( " + + " p.id, p.title, c.review ) " + + "from PostComment c " + + "join c.post p") + .setFirstResult(10) + .setMaxResults(20) + .setHint(AvailableHints.HINT_FETCH_SIZE, 20); + } + + protected Query getEntityQuery2(EntityManager entityManager) { + return entityManager.createQuery( + "select c " + + "from PostComment c " + + "join fetch c.post p " + + "where p.title like :title" + ); + } + + protected Query getNativeQuery1(EntityManager entityManager) { + return entityManager.createNativeQuery( + "select p.id, p.title, c.review * " + + "from post_comment c " + + "join post p on p.id = c.post_id ") + .setFirstResult(10) + .setMaxResults(20) + .setHint(AvailableHints.HINT_FETCH_SIZE, 20); + } + + protected Query getNativeQuery2(EntityManager entityManager) { + return entityManager.createNativeQuery( + "select c.*, p.* " + + "from post_comment c " + + "join post p on p.id = c.post_id " + + "where p.title like :title") + .unwrap(NativeQuery.class) + .addEntity(PostComment.class) + .addEntity(Post.class); + } + + @Entity(name = "Post") + @Table(name = "post") + + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @NamedNativeQuery( + name = "findPostComments", + query = "select c.review " + + "from post_comment c " + + "where c.id > :id " + ) + @NamedNativeQuery( + name = "findPostCommentSummary", + query = "select c.review " + + "from post_comment c " + + "left join post p on p.id = c.post_id " + + "where p.title > :title " + ) + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/PostCommentDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/PostCommentDTO.java new file mode 100644 index 000000000..b8792c8cf --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/PostCommentDTO.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentDTO { + + private final Long id; + + private final String review; + + private final String title; + + public PostCommentDTO(Long id, String review, String title) { + this.id = id; + this.review = review; + this.title = title; + } + + public Long getId() { + return id; + } + + public String getReview() { + return review; + } + + public String getTitle() { + return title; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/PostCommentSummary.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/PostCommentSummary.java new file mode 100644 index 000000000..d20d49c21 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/PostCommentSummary.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentSummary { + + private Number id; + private String title; + private String review; + + public PostCommentSummary(Number id, String title, String review) { + this.id = id; + this.title = title; + this.review = review; + } + + public PostCommentSummary() {} + + public Number getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getReview() { + return review; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/PostgreSQLScrollableResultsStreamingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/PostgreSQLScrollableResultsStreamingTest.java new file mode 100644 index 000000000..7b62ed463 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/PostgreSQLScrollableResultsStreamingTest.java @@ -0,0 +1,169 @@ +package com.vladmihalcea.hpjp.hibernate.fetching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.Session; +import org.hibernate.jpa.AvailableHints; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLScrollableResultsStreamingTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try (Statement statement = connection.createStatement()) { + statement.execute("CREATE INDEX idx_post_created_on ON post (created_on DESC)"); + } + }); + + LocalDateTime startTimestamp = LocalDateTime.now(); + LongStream.rangeClosed(1, 50 * 100).forEach(i -> { + entityManager.persist( + new Post() + .setId(i) + .setTitle(String.format("Post nr. %d", i)) + .setCreatedOn(Timestamp.valueOf(startTimestamp.plusHours(i))) + ); + if (i % 50 == 0 && i > 0) { + entityManager.flush(); + entityManager.clear(); + } + }); + }); + + executeStatement("VACUUM FULL ANALYZE"); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Test + public void testStreamExecutionPlan() { + doInJPA(entityManager -> { + executeStatement(entityManager, """ + SET auto_explain.log_min_duration TO 0; + """); + + List posts = (List) entityManager.createNativeQuery(""" + SELECT id, title, created_on + FROM post + ORDER BY created_on DESC + """, Tuple.class) + .setHint(AvailableHints.HINT_FETCH_SIZE, 50) + .getResultStream() + .limit(50) + .collect(Collectors.toList()); + + assertEquals(50, posts.size()); + + //Read the execution plan from $PG_DATA/log/postgresql-yyyy-mm-dd_HHmmss.log + }); + } + + @Test + public void testPaginationExecutionPlan() { + doInJPA(entityManager -> { + executeStatement(entityManager, """ + SET auto_explain.log_min_duration TO 0; + """); + + List posts = (List) entityManager.createNativeQuery(""" + SELECT id, title, created_on + FROM post + ORDER BY created_on DESC + """) + .setMaxResults(50) + .getResultList(); + + assertEquals(50, posts.size()); + }); + + //Read the execution plan from $PG_DATA/log/postgresql-yyyy-mm-dd_HHmmss.log + } + + @Test + public void testStreamWithoutMaxResult() { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + order by p.createdOn desc + """, Post.class) + .getResultStream() + .limit(50) + .collect(Collectors.toList()); + + assertEquals(50, posts.size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private Date createdOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/ProjectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/ProjectionTest.java similarity index 80% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/ProjectionTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/ProjectionTest.java index 4b02df49c..525354ea7 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/fetching/ProjectionTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/ProjectionTest.java @@ -1,13 +1,13 @@ -package com.vladmihalcea.book.hpjp.hibernate.fetching; +package com.vladmihalcea.hpjp.hibernate.fetching; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import org.hibernate.jpa.QueryHints; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import org.hibernate.jpa.AvailableHints; +import org.hibernate.jpa.boot.spi.IntegratorProvider; import org.junit.Test; -import javax.persistence.*; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; +import jakarta.persistence.*; +import java.util.*; import java.util.stream.LongStream; import static org.junit.Assert.assertEquals; @@ -28,10 +28,18 @@ protected Class[] entities() { }; } + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator(Collections.singletonList(PostCommentSummary.class)) + ) + ); + } @Override - public void init() { - super.init(); + public void afterInit() { int commentsSize = 5; doInJPA(entityManager -> { LongStream.range(0, 50).forEach(i -> { @@ -53,14 +61,14 @@ public void init() { @Test public void test() { doInJPA(entityManager -> { - List summaries = entityManager.createQuery( - "select new " + - " com.vladmihalcea.book.hpjp.hibernate.fetching.PostCommentSummary( " + - " p.id, p.title, c.review ) " + - "from PostComment c " + - "join c.post p " + - "order by p.id") + List summaries = entityManager.createQuery(""" + select new PostCommentSummary(p.id, p.title, c.review) + from PostComment c + join c.post p + order by p.id + """, PostCommentSummary.class) .getResultList(); + assertFalse(summaries.isEmpty()); }); } @@ -71,16 +79,16 @@ public void testPagination() { int pageSize = 10; doInJPA(entityManager -> { - List summaries = entityManager.createQuery( - "select new " + - " com.vladmihalcea.book.hpjp.hibernate.fetching.PostCommentSummary( " + - " p.id, p.title, c.review ) " + - "from PostComment c " + - "join c.post p " + - "order by p.id") + List summaries = entityManager.createQuery(""" + select new PostCommentSummary(p.id, p.title, c.review) + from PostComment c + join c.post p + order by p.id + """, PostCommentSummary.class) .setFirstResult(pageStart) .setMaxResults(pageSize) .getResultList(); + assertEquals(pageSize, summaries.size()); }); } @@ -91,13 +99,15 @@ public void testPaginationEntityQuery() { int pageSize = 10; doInJPA(entityManager -> { - List posts = entityManager.createQuery( - "select p " + - "from Post p " + - "join fetch p.comments") + List posts = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + """) .setFirstResult(pageStart) .setMaxResults(pageSize) .getResultList(); + assertEquals(pageSize, posts.size()); }); } @@ -108,16 +118,16 @@ public void testFetchSize() { int pageSize = 50; doInJPA(entityManager -> { - List summaries = entityManager.createQuery( - "select new " + - " com.vladmihalcea.book.hpjp.hibernate.fetching.PostCommentSummary( " + - " p.id, p.title, c.review ) " + - "from PostComment c " + - "join c.post p") + List summaries = entityManager.createQuery(""" + select new PostCommentSummary(p.id, p.title, c.review) + from PostComment c + join c.post p + """) .setFirstResult(pageStart) .setMaxResults(pageSize) - .setHint(QueryHints.HINT_FETCH_SIZE, pageSize) + .setHint(AvailableHints.HINT_FETCH_SIZE, pageSize) .getResultList(); + assertEquals(pageSize, summaries.size()); }); } @@ -219,7 +229,6 @@ public PostDetails() { } @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") @MapsId private Post post; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetch.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetch.java new file mode 100644 index 000000000..dd104b2e2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetch.java @@ -0,0 +1,169 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.detector; + +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import jakarta.persistence.EntityManager; +import org.hibernate.Session; +import org.hibernate.event.spi.LoadEvent; +import org.hibernate.event.spi.PostLoadEvent; + +import java.io.Serializable; +import java.util.*; + +import static java.util.stream.Collectors.groupingBy; + +/** + * @author Vlad Mihalcea + */ +public class AssociationFetch { + + private final Object entity; + + public AssociationFetch(Object entity) { + this.entity = entity; + } + + public Object getEntity() { + return entity; + } + + public static class Context implements Serializable { + public static final String SESSION_PROPERTY_KEY = "ASSOCIATION_FETCH_LIST"; + + private Map entityFetchCountByClassNameMap = new LinkedHashMap<>(); + + private Set joinedFetchedEntities = new LinkedHashSet<>(); + + private Set secondaryFetchedEntities = new LinkedHashSet<>(); + + private Map loadedEntities = new LinkedHashMap<>(); + + public List getAssociationFetches() { + List associationFetches = new ArrayList<>(); + + for(Map.Entry loadedEntityMapEntry : loadedEntities.entrySet()) { + EntityIdentifier entityIdentifier = loadedEntityMapEntry.getKey(); + Object entity = loadedEntityMapEntry.getValue(); + if(joinedFetchedEntities.contains(entityIdentifier) || + secondaryFetchedEntities.contains(entityIdentifier)) { + associationFetches.add(new AssociationFetch(entity)); + } + } + + return associationFetches; + } + + public List getJoinedAssociationFetches() { + List associationFetches = new ArrayList<>(); + + for(Map.Entry loadedEntityMapEntry : loadedEntities.entrySet()) { + EntityIdentifier entityIdentifier = loadedEntityMapEntry.getKey(); + Object entity = loadedEntityMapEntry.getValue(); + if(joinedFetchedEntities.contains(entityIdentifier)) { + associationFetches.add(new AssociationFetch(entity)); + } + } + + return associationFetches; + } + + public List getSecondaryAssociationFetches() { + List associationFetches = new ArrayList<>(); + + for(Map.Entry loadedEntityMapEntry : loadedEntities.entrySet()) { + EntityIdentifier entityIdentifier = loadedEntityMapEntry.getKey(); + Object entity = loadedEntityMapEntry.getValue(); + if(secondaryFetchedEntities.contains(entityIdentifier)) { + associationFetches.add(new AssociationFetch(entity)); + } + } + + return associationFetches; + } + + public Map> getAssociationFetchEntityMap() { + return getAssociationFetches() + .stream() + .map(AssociationFetch::getEntity) + .collect(groupingBy(Object::getClass)); + } + + public void preLoad(LoadEvent loadEvent) { + String entityClassName = loadEvent.getEntityClassName(); + entityFetchCountByClassNameMap.put(entityClassName, SessionStatistics.getEntityFetchCount(entityClassName)); + } + + public void load(LoadEvent loadEvent) { + String entityClassName = loadEvent.getEntityClassName(); + int previousFetchCount = entityFetchCountByClassNameMap.get(entityClassName); + int currentFetchCount = SessionStatistics.getEntityFetchCount(entityClassName); + + EntityIdentifier entityIdentifier = new EntityIdentifier( + ReflectionUtils.getClass(loadEvent.getEntityClassName()), + loadEvent.getEntityId() + ); + + if (loadEvent.isAssociationFetch()) { + if (currentFetchCount == previousFetchCount) { + joinedFetchedEntities.add(entityIdentifier); + } else if (currentFetchCount > previousFetchCount){ + secondaryFetchedEntities.add(entityIdentifier); + } + } + } + + public void postLoad(PostLoadEvent postLoadEvent) { + loadedEntities.put( + new EntityIdentifier( + postLoadEvent.getEntity().getClass(), + postLoadEvent.getId() + ), + postLoadEvent.getEntity() + ); + } + + public static Context get(Session session) { + Context context = (Context) session.getProperties().get(SESSION_PROPERTY_KEY); + if (context == null) { + context = new Context(); + session.setProperty(SESSION_PROPERTY_KEY, context); + } + return context; + } + + public static Context get(EntityManager entityManager) { + return get(entityManager.unwrap(Session.class)); + } + } + + private static class EntityIdentifier { + private final Class entityClass; + + private final Object entityId; + + public EntityIdentifier(Class entityClass, Object entityId) { + this.entityClass = entityClass; + this.entityId = entityId; + } + + public Class getEntityClass() { + return entityClass; + } + + public Object getEntityId() { + return entityId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof EntityIdentifier)) return false; + EntityIdentifier that = (EntityIdentifier) o; + return Objects.equals(getEntityClass(), that.getEntityClass()) && Objects.equals(getEntityId(), that.getEntityId()); + } + + @Override + public int hashCode() { + return Objects.hash(getEntityClass(), getEntityId()); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetchLoadEventListener.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetchLoadEventListener.java new file mode 100644 index 000000000..7766cb0ee --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetchLoadEventListener.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.detector; + +import org.hibernate.event.spi.LoadEvent; +import org.hibernate.event.spi.LoadEventListener; + +/** + * @author Vlad Mihalcea + */ +public class AssociationFetchLoadEventListener implements LoadEventListener { + + public static final AssociationFetchLoadEventListener INSTANCE = new AssociationFetchLoadEventListener(); + + @Override + public void onLoad(LoadEvent event, LoadType loadType) { + AssociationFetch.Context + .get(event.getSession()) + .load(event); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetchPostLoadEventListener.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetchPostLoadEventListener.java new file mode 100644 index 000000000..427bdf104 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetchPostLoadEventListener.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.detector; + +import org.hibernate.event.spi.PostLoadEvent; +import org.hibernate.event.spi.PostLoadEventListener; + +/** + * @author Vlad Mihalcea + */ +public class AssociationFetchPostLoadEventListener implements PostLoadEventListener { + + public static final AssociationFetchPostLoadEventListener INSTANCE = new AssociationFetchPostLoadEventListener(); + + @Override + public void onPostLoad(PostLoadEvent event) { + AssociationFetch.Context + .get(event.getSession()) + .postLoad(event); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetchPreLoadEventListener.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetchPreLoadEventListener.java new file mode 100644 index 000000000..7d6d2e6bd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetchPreLoadEventListener.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.detector; + +import org.hibernate.event.spi.LoadEvent; +import org.hibernate.event.spi.LoadEventListener; + +/** + * @author Vlad Mihalcea + */ +public class AssociationFetchPreLoadEventListener implements LoadEventListener { + + public static final AssociationFetchPreLoadEventListener INSTANCE = new AssociationFetchPreLoadEventListener(); + + @Override + public void onLoad(LoadEvent event, LoadType loadType) { + AssociationFetch.Context + .get(event.getSession()) + .preLoad(event); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetchingEventListenerIntegrator.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetchingEventListenerIntegrator.java new file mode 100644 index 000000000..ce82d6dfa --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/AssociationFetchingEventListenerIntegrator.java @@ -0,0 +1,48 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.detector; + +import org.hibernate.boot.Metadata; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; + +/** + * @author Vlad Mihalcea + */ +public class AssociationFetchingEventListenerIntegrator implements Integrator { + + public static final AssociationFetchingEventListenerIntegrator INSTANCE = new AssociationFetchingEventListenerIntegrator(); + + @Override + public void integrate( + Metadata metadata, + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + final EventListenerRegistry eventListenerRegistry = + serviceRegistry.getService(EventListenerRegistry.class); + + eventListenerRegistry.prependListeners( + EventType.LOAD, + AssociationFetchPreLoadEventListener.INSTANCE + ); + + eventListenerRegistry.appendListeners( + EventType.LOAD, + AssociationFetchLoadEventListener.INSTANCE + ); + + eventListenerRegistry.appendListeners( + EventType.POST_LOAD, + AssociationFetchPostLoadEventListener.INSTANCE + ); + } + + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/EagerFetchingDetectorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/EagerFetchingDetectorTest.java new file mode 100644 index 000000000..b841eb0a0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/EagerFetchingDetectorTest.java @@ -0,0 +1,328 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.detector; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.StatisticsSettings; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.stat.internal.StatisticsInitiator; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class EagerFetchingDetectorTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + PostCommentDetails.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + AvailableSettings.GENERATE_STATISTICS, + Boolean.TRUE.toString() + ); + properties.put( + StatisticsSettings.STATS_BUILDER, + SessionStatistics.Factory.INSTANCE + ); + } + + @Override + protected Integrator integrator() { + return AssociationFetchingEventListenerIntegrator.INSTANCE; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + PostComment comment1 = new PostComment(); + comment1.setId(1L); + comment1.setReview("Good"); + comment1.setPost(post); + + PostCommentDetails details1 = new PostCommentDetails(); + details1.setComment(comment1); + details1.setVotes(10); + + PostComment comment2 = new PostComment(); + comment2.setId(2L); + comment2.setReview("Excellent"); + comment2.setPost(post); + + PostCommentDetails details2 = new PostCommentDetails(); + details2.setComment(comment2); + details2.setVotes(10); + + entityManager.persist(post); + entityManager.persist(comment1); + entityManager.persist(comment2); + entityManager.persist(details1); + entityManager.persist(details2); + }); + } + + @Test + @Ignore + public void testFindPostComment() { + doInJPA(entityManager -> { + AssociationFetch.Context context = AssociationFetch.Context.get(entityManager); + assertTrue(context.getAssociationFetches().isEmpty()); + + PostComment comment = entityManager.find(PostComment.class, 1L); + + List associationFetches = context.getAssociationFetches(); + assertEquals(1, associationFetches.size()); + assertEquals(1, context.getJoinedAssociationFetches().size()); + assertEquals(0, context.getSecondaryAssociationFetches().size()); + + AssociationFetch associationFetch = associationFetches.get(0); + assertSame(comment.getPost(), associationFetch.getEntity()); + }); + } + + @Test + @Ignore + public void testFindPostCommentDetails() { + doInJPA(entityManager -> { + AssociationFetch.Context context = AssociationFetch.Context.get(entityManager); + assertTrue(context.getAssociationFetches().isEmpty()); + + PostCommentDetails commentDetails = entityManager.find(PostCommentDetails.class, 1L); + + assertEquals(1, context.getJoinedAssociationFetches().size()); + assertEquals(1, context.getSecondaryAssociationFetches().size()); + Map> associationFetchMap = context.getAssociationFetchEntityMap(); + assertEquals(2, associationFetchMap.size()); + + List postCommentAssociationFetches = associationFetchMap.get(PostComment.class); + assertEquals(1, postCommentAssociationFetches.size()); + assertSame(commentDetails.getComment(), postCommentAssociationFetches.get(0)); + + List postAssociationFetches = associationFetchMap.get(Post.class); + assertEquals(1, postAssociationFetches.size()); + assertSame(commentDetails.getComment().getPost(), postAssociationFetches.get(0)); + }); + } + + @Test + @Ignore + public void testJPQLPostCommentDetails() { + doInJPA(entityManager -> { + AssociationFetch.Context context = AssociationFetch.Context.get(entityManager); + assertTrue(context.getAssociationFetches().isEmpty()); + + List commentDetailsList = entityManager.createQuery(""" + select pcd + from PostCommentDetails pcd + order by pcd.id + """, + PostCommentDetails.class) + .getResultList(); + + assertEquals(3, context.getAssociationFetches().size()); + assertEquals(2, context.getSecondaryAssociationFetches().size()); + assertEquals(1, context.getJoinedAssociationFetches().size()); + + Map> associationFetchMap = context.getAssociationFetchEntityMap(); + assertEquals(2, associationFetchMap.size()); + + for (PostCommentDetails commentDetails : commentDetailsList) { + assertTrue(associationFetchMap.get(PostComment.class).contains(commentDetails.getComment())); + assertTrue(associationFetchMap.get(Post.class).contains(commentDetails.getComment().getPost())); + } + }); + } + + @Test + @Ignore + public void testJPQLPostCommentDetailsJoinFetchEagerAssociations() { + doInJPA(entityManager -> { + AssociationFetch.Context context = AssociationFetch.Context.get(entityManager); + assertTrue(context.getAssociationFetches().isEmpty()); + + List commentDetailsList = entityManager.createQuery(""" + select pcd + from PostCommentDetails pcd + join fetch pcd.comment pc + join fetch pc.post + order by pcd.id + """, + PostCommentDetails.class) + .getResultList(); + + assertEquals(3, context.getJoinedAssociationFetches().size()); + assertTrue(context.getSecondaryAssociationFetches().isEmpty()); + }); + } + + @Test + public void testStatisticsSecondaryQueries() { + doInJPA(entityManager -> { + assertEquals(0, SessionStatistics.getEntityFetchCount(PostCommentDetails.class)); + assertEquals(0, SessionStatistics.getEntityFetchCount(PostComment.class)); + assertEquals(0, SessionStatistics.getEntityFetchCount(Post.class)); + + List commentDetailsList = entityManager.createQuery(""" + select pcd + from PostCommentDetails pcd + order by pcd.id + """, + PostCommentDetails.class) + .getResultList(); + + assertEquals(2, commentDetailsList.size()); + + assertEquals(0, SessionStatistics.getEntityFetchCount(PostCommentDetails.class)); + assertEquals(2, SessionStatistics.getEntityFetchCount(PostComment.class)); + assertEquals(0, SessionStatistics.getEntityFetchCount(Post.class)); + }); + } + + @Test + public void testStatisticsJoinFetch() { + doInJPA(entityManager -> { + assertEquals(0, SessionStatistics.getEntityFetchCount(PostCommentDetails.class)); + assertEquals(0, SessionStatistics.getEntityFetchCount(PostComment.class)); + assertEquals(0, SessionStatistics.getEntityFetchCount(Post.class)); + + List commentDetailsList = entityManager.createQuery(""" + select pcd + from PostCommentDetails pcd + join fetch pcd.comment pc + join fetch pc.post + order by pcd.id + """, + PostCommentDetails.class) + .getResultList(); + + assertEquals(2, commentDetailsList.size()); + + assertEquals(0, SessionStatistics.getEntityFetchCount(PostCommentDetails.class)); + assertEquals(0, SessionStatistics.getEntityFetchCount(PostComment.class)); + assertEquals(0, SessionStatistics.getEntityFetchCount(Post.class)); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } + + @Entity(name = "PostCommentDetails") + @Table(name = "post_comment_details") + public static class PostCommentDetails { + + @Id + private Long id; + + @OneToOne + @MapsId + @OnDelete(action = OnDeleteAction.CASCADE) + private PostComment comment; + + private int votes; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public PostComment getComment() { + return comment; + } + + public void setComment(PostComment comment) { + this.comment = comment; + } + + public int getVotes() { + return votes; + } + + public void setVotes(int votes) { + this.votes = votes; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/SessionStatistics.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/SessionStatistics.java new file mode 100644 index 000000000..fd6b4fe61 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/detector/SessionStatistics.java @@ -0,0 +1,64 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.detector; + +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.stat.internal.StatisticsImpl; +import org.hibernate.stat.spi.StatisticsFactory; +import org.hibernate.stat.spi.StatisticsImplementor; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import static java.util.stream.Collectors.groupingBy; + +/** + * @author Vlad Mihalcea + */ +public class SessionStatistics extends StatisticsImpl { + + private static final ThreadLocal> entityFetchCountContext = new ThreadLocal<>(); + + public SessionStatistics(SessionFactoryImplementor sessionFactory) { + super(sessionFactory); + } + + @Override + public void openSession() { + entityFetchCountContext.set(new LinkedHashMap<>()); + super.openSession(); + } + + @Override + public void fetchEntity(String entityName) { + Map entityFetchCountMap = entityFetchCountContext.get(); + entityFetchCountMap.computeIfAbsent(ReflectionUtils.getClass(entityName), clazz -> new AtomicInteger()).incrementAndGet(); + super.fetchEntity(entityName); + } + + @Override + public void closeSession() { + entityFetchCountContext.remove(); + super.closeSession(); + } + + public static int getEntityFetchCount(String entityClassName) { + return getEntityFetchCount( + ReflectionUtils.getClass(entityClassName) + ); + } + + public static int getEntityFetchCount(Class entityClass) { + AtomicInteger entityFetchCount = entityFetchCountContext.get().get(entityClass); + return entityFetchCount != null ? entityFetchCount.get() : 0; + } + + public static class Factory implements StatisticsFactory { + + public static final Factory INSTANCE = new Factory(); + + @Override + public StatisticsImplementor buildStatistics(SessionFactoryImplementor sessionFactory) { + return new SessionStatistics(sessionFactory); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/file/PostgreSQLCopyQueryResultSetToFileTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/file/PostgreSQLCopyQueryResultSetToFileTest.java new file mode 100644 index 000000000..dfd774dab --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/file/PostgreSQLCopyQueryResultSetToFileTest.java @@ -0,0 +1,302 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.file; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.PostgreSQLDataSourceProvider; +import jakarta.persistence.*; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.jpa.AvailableHints; +import org.junit.Test; +import org.testcontainers.shaded.org.apache.commons.io.FileUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLCopyQueryResultSetToFileTest extends AbstractTest { + + public static final int BATCH_SIZE = 5000; + + private final String queryResultSetOutputFolder = System.getenv("PG_QUERY_OUTPUT"); + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider() + .setReWriteBatchedInserts(true); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.STATEMENT_BATCH_SIZE, String.valueOf(BATCH_SIZE)); + properties.setProperty(AvailableSettings.ORDER_INSERTS, Boolean.TRUE.toString()); + properties.setProperty(AvailableSettings.ORDER_UPDATES, Boolean.TRUE.toString()); + properties.setProperty(AvailableSettings.LOG_SLOW_QUERY, "1"); + } + + @Override + public void afterInit() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + long startNanos = System.nanoTime(); + doInJPA(entityManager -> { + String[] reviews = new String[] { + "Excellent book to understand Java Persistence", + "Must-read for Java developers", + "Five Stars", + "A great reference book", + "The ultimate guide to a critical topic" + }; + + long postCount = 1_000_000; + //long postCount = 200; + long commentId = 1; + + for (long id = 1; id <= postCount; id++) { + Post post = new Post() + .setId(id) + .setTitle( + String.format( + "High-Performance Java Persistence - page %d", + id + ) + ); + + for(String review : reviews) { + post.addComment( + new PostComment() + .setId(commentId++) + .setReview(review) + ); + } + + entityManager.persist(post); + + if (id % BATCH_SIZE == 0) { + entityManager.flush(); + entityManager.clear(); + } + } + }); + LOGGER.info( + "Data inserted in {} ms", + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos) + ); + } + + @Test + public void test() { + if (!ENABLE_LONG_RUNNING_TESTS) { + return; + } + testFetchAll(); + testCopy(); + } + + public void testFetchAll() { + doInJPA(entityManager -> { + try { + String fileName = "post_and_comments.csv"; + Path filePath = Paths.get(queryResultSetOutputFolder, fileName).toAbsolutePath(); + if (Files.exists(filePath)) { + Files.delete(filePath); + } + assertFalse(Files.exists(filePath)); + + long startNanos = System.nanoTime(); + try(Stream tuples = entityManager + .createNativeQuery(""" + SELECT 'post_id,post_title,comment_id,comment_review' + UNION ALL + SELECT concat_ws(',', + p.id, + p.title, + pc.id, + pc.review + ) + FROM post p + INNER JOIN post_comment pc ON pc.post_id = p.id + """, String.class) + .setHint(AvailableHints.HINT_FETCH_SIZE, BATCH_SIZE) + .getResultStream()) { + Files.write(filePath, (Iterable) tuples::iterator); + } + + assertTrue(Files.exists(filePath)); + + long endNanos = System.nanoTime(); + long lineCount; + try (Stream stream = Files.lines(filePath)) { + lineCount = stream.count(); + } + LOGGER.info( + "Fetched and saved [{}] records in [{}] ms", + lineCount - 1, + TimeUnit.NANOSECONDS.toMillis( + endNanos - startNanos + ) + ); + LOGGER.info( + "File [{}] size is [{}]", + fileName, + FileUtils.byteCountToDisplaySize(Files.size(filePath)) + ); + } catch (IOException e) { + fail(e.getMessage()); + } + }); + } + + public void testCopy() { + try { + String fileName = "post_and_comments.csv"; + Path filePath = Paths.get(queryResultSetOutputFolder, fileName).toAbsolutePath(); + if (Files.exists(filePath)) { + Files.delete(filePath); + } + assertFalse(Files.exists(filePath)); + + long startNanos = System.nanoTime(); + int copyCount = exportQueryResultSet(""" + SELECT + p.id AS post_id, + p.title AS post_title, + pc.id AS comment_id, + pc.review AS comment_review + FROM post p + INNER JOIN post_comment pc ON pc.post_id = p.id + """, + filePath + ); + + LOGGER.info( + "Copy has exported [{}] records in [{}] ms", + copyCount, + TimeUnit.NANOSECONDS.toMillis( + System.nanoTime() - startNanos + ) + ); + + assertTrue(Files.exists(filePath)); + LOGGER.info( + "File [{}] size is [{}]", + fileName, + FileUtils.byteCountToDisplaySize(Files.size(filePath)) + ); + } catch (IOException e) { + fail(e.getMessage()); + } + } + + private int exportQueryResultSet(String query, Path filePath) { + return doInJPA(entityManager -> { + return entityManager.createNativeQuery( + String.format(""" + COPY (%s) + TO '%s' + WITH CSV HEADER + """, + query, + filePath + ) + ) + .executeUpdate(); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/maxrows/MySQLSetMaxRowsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/maxrows/MySQLSetMaxRowsTest.java new file mode 100644 index 000000000..9a575c7f9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/maxrows/MySQLSetMaxRowsTest.java @@ -0,0 +1,154 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.maxrows; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.CreationTimestamp; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.Date; +import java.util.Properties; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLSetMaxRowsTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try (Statement statement = connection.createStatement()) { + statement.execute("CREATE INDEX idx_post_created_on ON post (created_on DESC)"); + } + }); + LongStream.range(0, 50 * 100).forEach(i -> { + Post post = new Post(i); + post.setTitle(String.format("Post nr. %d", i)); + entityManager.persist(post); + if (i % 50 == 0 && i > 0) { + entityManager.flush(); + entityManager.clear(); + } + }); + }); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Test + public void testSetMaxSize() { + doInJPA(entityManager -> { + + entityManager.unwrap(Session.class).doWork(connection -> { + try(PreparedStatement statement = connection.prepareStatement(""" + EXPLAIN ANALYZE + SELECT p.title + FROM post p + ORDER BY p.created_on DESC + """ + )) { + statement.setMaxRows(50); + ResultSet resultSet = statement.executeQuery(); + + while (resultSet.next()) { + String executionPlanLines = resultSet.getString(1); + LOGGER.info("Execution plan: {}{}", + System.lineSeparator(), + executionPlanLines + ); + } + } + }); + }); + } + + @Test + public void testLimit() { + doInJPA(entityManager -> { + + entityManager.unwrap(Session.class).doWork(connection -> { + try(PreparedStatement statement = connection.prepareStatement(""" + EXPLAIN ANALYZE + SELECT p.title + FROM post p + ORDER BY p.created_on DESC + LIMIT 50 + """ + )) { + ResultSet resultSet = statement.executeQuery(); + + while (resultSet.next()) { + String executionPlanLines = resultSet.getString(1); + LOGGER.info("Execution plan: {}{}", + System.lineSeparator(), + executionPlanLines + ); + } + } + }); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + @CreationTimestamp + private Date createdOn; + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/maxrows/OracleSetMaxRowsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/maxrows/OracleSetMaxRowsTest.java new file mode 100644 index 000000000..7cb4819a8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/maxrows/OracleSetMaxRowsTest.java @@ -0,0 +1,165 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.maxrows; + +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.CreationTimestamp; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class OracleSetMaxRowsTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try (Statement statement = connection.createStatement()) { + statement.execute("CREATE INDEX idx_post_created_on ON post (created_on DESC)"); + } + }); + LongStream.range(0, 50 * 100).forEach(i -> { + Post post = new Post(i); + post.setTitle(String.format("Post nr. %d", i)); + entityManager.persist(post); + if (i % 50 == 0 && i > 0) { + entityManager.flush(); + entityManager.clear(); + } + }); + }); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Test + public void testSetMaxSize() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(PreparedStatement statement = connection.prepareStatement(""" + EXPLAIN PLAN FOR + SELECT p.title + FROM post p + ORDER BY p.created_on DESC + """ + )) { + statement.setMaxRows(50); + statement.executeQuery(); + } + }); + + List planLines = entityManager.createNativeQuery(""" + SELECT * + FROM TABLE(DBMS_XPLAN.DISPLAY (FORMAT=>'ALL +OUTLINE')) + """) + .getResultList(); + + assertTrue(planLines.size() > 1); + + LOGGER.info("Execution plan: {}{}", + System.lineSeparator(), + planLines.stream().collect(Collectors.joining(System.lineSeparator())) + ); + }); + } + + @Test + public void testLimit() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(PreparedStatement statement = connection.prepareStatement(""" + EXPLAIN PLAN FOR + SELECT * + FROM ( + SELECT title + FROM post + ORDER BY id DESC + ) + WHERE ROWNUM <= 50 + """ + )) { + statement.executeQuery(); + } + }); + + List planLines = entityManager.createNativeQuery(""" + SELECT * + FROM TABLE(DBMS_XPLAN.DISPLAY (FORMAT=>'ALL +OUTLINE')) + """) + .getResultList(); + + assertTrue(planLines.size() > 1); + + LOGGER.info("Execution plan: {}{}", + System.lineSeparator(), + planLines.stream().collect(Collectors.joining(System.lineSeparator())) + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + @CreationTimestamp + private Date createdOn; + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/maxrows/PostgreSQLSetMaxRowsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/maxrows/PostgreSQLSetMaxRowsTest.java new file mode 100644 index 000000000..a61a54202 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/maxrows/PostgreSQLSetMaxRowsTest.java @@ -0,0 +1,164 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.maxrows; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.CreationTimestamp; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.Date; +import java.util.Properties; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLSetMaxRowsTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try (Statement statement = connection.createStatement()) { + statement.execute("CREATE INDEX idx_post_created_on ON post (created_on DESC)"); + } + }); + LongStream.range(0, 50 * 100).forEach(i -> { + Post post = new Post(i); + post.setTitle(String.format("Post nr. %d", i)); + entityManager.persist(post); + if (i % 50 == 0 && i > 0) { + entityManager.flush(); + entityManager.clear(); + } + }); + }); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Test + public void testSetMaxSize() { + doInJPA(entityManager -> { + executeStatement(entityManager, """ + SET session_preload_libraries = 'auto_explain'; + SET auto_explain.log_analyze TO ON; + SET auto_explain.log_min_duration TO 0; + """); + + entityManager.unwrap(Session.class).doWork(connection -> { + try(PreparedStatement statement = connection.prepareStatement(""" + SELECT p.title + FROM post p + ORDER BY p.created_on DESC + """ + )) { + statement.setMaxRows(50); + ResultSet resultSet = statement.executeQuery(); + + int count = 0; + + while (resultSet.next()) { + count++; + } + + assertEquals(50, count); + } + }); + //Read the execution plan from $PG_DATA/log/postgresql-yyyy-mm-dd_HHmmss.log + }); + } + + @Test + public void testLimit() { + doInJPA(entityManager -> { + executeStatement(entityManager, """ + SET session_preload_libraries = 'auto_explain'; + SET auto_explain.log_analyze TO ON; + SET auto_explain.log_min_duration TO 0; + """); + + entityManager.unwrap(Session.class).doWork(connection -> { + try(PreparedStatement statement = connection.prepareStatement(""" + SELECT p.title + FROM post p + ORDER BY p.created_on DESC + LIMIT 50 + """ + )) { + ResultSet resultSet = statement.executeQuery(); + + int count = 0; + + while (resultSet.next()) { + count++; + } + + assertEquals(50, count); + } + }); + //Read the execution plan from $PG_DATA/log/postgresql-yyyy-mm-dd_HHmmss.log + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + @CreationTimestamp + private Date createdOn; + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/maxrows/SQLServerSetMaxRowsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/maxrows/SQLServerSetMaxRowsTest.java new file mode 100644 index 000000000..753020614 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/maxrows/SQLServerSetMaxRowsTest.java @@ -0,0 +1,174 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.maxrows; + +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.CreationTimestamp; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.sql.Statement; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerSetMaxRowsTest extends AbstractSQLServerIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try (Statement statement = connection.createStatement()) { + statement.execute("CREATE INDEX idx_post_created_on ON post (created_on DESC)"); + } + }); + LongStream.range(0, 50 * 100).forEach(i -> { + Post post = new Post(i); + post.setTitle(String.format("Post nr. %d", i)); + entityManager.persist(post); + if (i % 50 == 0 && i > 0) { + entityManager.flush(); + entityManager.clear(); + } + }); + }); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Test + public void testSetMaxSize() { + doInJPA(entityManager -> { + List> planLines = new ArrayList<>(); + + entityManager.unwrap(Session.class) + .doWork(connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate( + "SET STATISTICS IO, TIME, PROFILE ON" + ); + + statement.setMaxRows(50); + boolean moreResultSets = statement.execute(""" + SELECT p.title + FROM post p + ORDER BY p.created_on DESC + """); + + while (moreResultSets) { + planLines.addAll(parseResultSet(statement.getResultSet())); + + moreResultSets = statement.getMoreResults(); + } + + statement.executeUpdate( + "SET STATISTICS IO, TIME, PROFILE OFF" + ); + } + }); + + LOGGER.info("Execution plan: {}{}", + System.lineSeparator(), + planLines.stream().map(Map::toString).collect(Collectors.joining(System.lineSeparator())) + ); + }); + } + + @Test + public void testLimit() { + doInJPA(entityManager -> { + List> planLines = new ArrayList<>(); + + entityManager.unwrap(Session.class) + .doWork(connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate( + "SET STATISTICS IO, TIME, PROFILE ON" + ); + + boolean moreResultSets = statement.execute(""" + SELECT TOP 50 p.title + FROM post p + ORDER BY p.created_on DESC + """); + + while (moreResultSets) { + planLines.addAll(parseResultSet(statement.getResultSet())); + + moreResultSets = statement.getMoreResults(); + } + + statement.executeUpdate( + "SET STATISTICS IO, TIME, PROFILE OFF" + ); + } + }); + + LOGGER.info("Execution plan: {}{}", + System.lineSeparator(), + planLines.stream().map(Map::toString).collect(Collectors.joining(System.lineSeparator())) + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + @CreationTimestamp + private Date createdOn; + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/CriteriaAPIFetchingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/CriteriaAPIFetchingTest.java new file mode 100644 index 000000000..6a089cca2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/CriteriaAPIFetchingTest.java @@ -0,0 +1,407 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.multiple; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import jakarta.persistence.criteria.*; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class CriteriaAPIFetchingTest extends AbstractPostgreSQLIntegrationTest { + + public static final int POST_COUNT = 50; + public static final int POST_COMMENT_COUNT = 20; + public static final int TAG_COUNT = 10; + public static final int VOTE_COUNT = 5; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + Tag.class, + User.class, + Company.class, + UserVote.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + User alice = new User() + .setId(1L) + .setName("Alice"); + + User bob = new User() + .setId(2L) + .setName("Bob"); + + entityManager.persist(alice); + entityManager.persist(bob); + + List tags = new ArrayList<>(); + + for (long i = 1; i <= TAG_COUNT; i++) { + Tag tag = new Tag() + .setId(i) + .setName(String.format("Tag nr. %d", i)); + + entityManager.persist(tag); + tags.add(tag); + } + + long commentId = 0; + long voteId = 0; + + for (long postId = 1; postId <= POST_COUNT; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post nr. %d", postId)); + + + for (long i = 0; i < POST_COMMENT_COUNT; i++) { + PostComment comment = new PostComment() + .setId(++commentId) + .setReview("Excellent!"); + + for (int j = 0; j < VOTE_COUNT; j++) { + comment.addVote( + new UserVote() + .setId(++voteId) + .setScore(Math.random() > 0.5 ? 1 : -1) + ); + } + + post.addComment(comment); + + } + + for (int i = 0; i < TAG_COUNT; i++) { + post.getTags().add(tags.get(i)); + } + + entityManager.persist(post); + } + }); + } + + @Test + public void testInnerJoin() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = builder.createQuery(UserVote.class); + Root userVoteRoot = query.from(UserVote.class); + + Fetch postCommentJoin = userVoteRoot.fetch("http://github.com/comment"); + Fetch postJoin = postCommentJoin.fetch("http://github.com/post"); + Fetch userJoin = userVoteRoot.fetch("http://github.com/user"); + Fetch companyJoin = userJoin.fetch("http://github.com/company"); + + query.where( + builder.like( + ((Join) postJoin).get("title"), + "Post nr%" + ) + ); + + List userVotes = entityManager + .createQuery(query) + .getResultList(); + + assertEquals(0, userVotes.size()); + }); + } + + @Test + public void testLeftJoin() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = builder.createQuery(UserVote.class); + Root userVoteRoot = query.from(UserVote.class); + + Fetch postCommentJoin = userVoteRoot.fetch("http://github.com/comment", JoinType.LEFT); + Fetch postJoin = postCommentJoin.fetch("http://github.com/post", JoinType.LEFT); + Fetch userJoin = userVoteRoot.fetch("http://github.com/user", JoinType.LEFT); + Fetch companyJoin = userJoin.fetch("http://github.com/company", JoinType.LEFT); + + query.where( + builder.like( + ((Join) postJoin).get("title"), + "Post nr%" + ) + ); + + List userVotes = entityManager + .createQuery(query) + .getResultList(); + + assertEquals(POST_COUNT * POST_COMMENT_COUNT * VOTE_COUNT, userVotes.size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + @OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true) + private List votes = new ArrayList<>(); + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public List getVotes() { + return votes; + } + + public PostComment addVote(UserVote vote) { + votes.add(vote); + vote.setComment(this); + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } + + @Entity(name = "User") + @Table(name = "blog_user") + public static class User { + + @Id + private Long id; + + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + private Company company; + + public Long getId() { + return id; + } + + public User setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public User setName(String name) { + this.name = name; + return this; + } + } + + @Entity(name = "Company") + @Table(name = "company") + public static class Company { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Company setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Company setName(String name) { + this.name = name; + return this; + } + } + + @Entity(name = "UserVote") + @Table(name = "user_vote") + public static class UserVote { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + private PostComment comment; + + private int score; + + public Long getId() { + return id; + } + + public UserVote setId(Long id) { + this.id = id; + return this; + } + + public User getUser() { + return user; + } + + public UserVote setUser(User user) { + this.user = user; + return this; + } + + public PostComment getComment() { + return comment; + } + + public UserVote setComment(PostComment comment) { + this.comment = comment; + return this; + } + + public int getScore() { + return score; + } + + public UserVote setScore(int score) { + this.score = score; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/EagerFetchingMultipleBagLazyCollectionOptionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/EagerFetchingMultipleBagLazyCollectionOptionTest.java new file mode 100644 index 000000000..dab0546f2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/EagerFetchingMultipleBagLazyCollectionOptionTest.java @@ -0,0 +1,216 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.multiple; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class EagerFetchingMultipleBagLazyCollectionOptionTest extends AbstractPostgreSQLIntegrationTest { + + public static final int POST_COUNT = 50; + public static final int POST_COMMENT_COUNT = 20; + public static final int TAG_COUNT = 10; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + Tag.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + List tags = new ArrayList<>(); + + for (long i = 1; i <= TAG_COUNT; i++) { + Tag tag = new Tag() + .setId(i) + .setName(String.format("Tag nr. %d", i)); + + entityManager.persist(tag); + tags.add(tag); + } + + long commentId = 0; + + for (long postId = 1; postId <= POST_COUNT; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post nr. %d", postId)); + + + for (long i = 0; i < POST_COMMENT_COUNT; i++) { + post.addComment( + new PostComment() + .setId(++commentId) + .setReview("Excellent!") + ); + } + + for (int i = 0; i < TAG_COUNT; i++) { + post.getTags().add(tags.get(i)); + } + + entityManager.persist(post); + } + }); + } + + @Test + public void testFind() { + doInJPA(entityManager -> { + Post post = entityManager.find( + Post.class, + 1L + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + @LazyCollection(LazyCollectionOption.FALSE) + private List comments = new ArrayList<>(); + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + @LazyCollection(LazyCollectionOption.FALSE) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/EagerFetchingMultipleBagTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/EagerFetchingMultipleBagTest.java new file mode 100644 index 000000000..bd1aa3f3d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/EagerFetchingMultipleBagTest.java @@ -0,0 +1,371 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.multiple; + +import com.blazebit.persistence.Criteria; +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.spi.CriteriaBuilderConfiguration; +import com.blazebit.persistence.view.*; +import com.blazebit.persistence.view.spi.EntityViewConfiguration; +import com.vladmihalcea.hpjp.hibernate.forum.Post_; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import jakarta.persistence.CascadeType; +import jakarta.persistence.*; +import org.hibernate.loader.MultipleBagFetchException; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static com.blazebit.persistence.view.FetchStrategy.MULTISET; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class EagerFetchingMultipleBagTest extends AbstractPostgreSQLIntegrationTest { + + public static final long POST_COUNT = 50; + public static final long POST_COMMENT_COUNT = 20; + public static final long TAG_COUNT = 10; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + Tag.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + List tags = new ArrayList<>(); + + for (long i = 1; i <= TAG_COUNT; i++) { + Tag tag = new Tag() + .setId(i) + .setName(String.format("Tag nr. %d", i)); + + entityManager.persist(tag); + tags.add(tag); + } + + long commentId = 0; + + for (long postId = 1; postId <= POST_COUNT; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post nr. %d", postId)); + + + for (long i = 0; i < POST_COMMENT_COUNT; i++) { + post.addComment( + new PostComment() + .setId(++commentId) + .setReview("Excellent!") + ); + } + + for (int i = 0; i < TAG_COUNT; i++) { + post.getTags().add(tags.get(i)); + } + + entityManager.persist(post); + } + }); + } + + @Test + public void testOneQueryTwoJoinFetch() { + try { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + left join fetch p.tags + where p.id between :minId and :maxId + """, Post.class) + .setParameter("minId", 1L) + .setParameter("maxId", 50L) + .getResultList(); + }); + } catch (Exception e) { + assertTrue( + MultipleBagFetchException.class.isAssignableFrom( + ExceptionUtil.rootCause(e).getClass() + ) + ); + LOGGER.error("Failure", e); + } + } + + @Test + public void testTwoJoinFetchQueries() { + List _posts = doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + where p.id between :minId and :maxId + """, Post.class) + .setParameter("minId", 1L) + .setParameter("maxId", 50L) + .getResultList(); + + posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.tags t + where p in :posts + """, Post.class) + .setParameter("posts", posts) + .getResultList(); + + assertEquals(POST_COUNT, posts.size()); + + return posts; + }); + + for(Post post : _posts) { + assertEquals(POST_COMMENT_COUNT, post.getComments().size()); + assertEquals(TAG_COUNT, post.getTags().size()); + } + } + + @Test + public void testTwoJoinFetchQueriesWithoutInClause() { + List _posts = doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + where p.id between :minId and :maxId + """, Post.class) + .setParameter("minId", 1L) + .setParameter("maxId", 50L) + .getResultList(); + + posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.tags t + where p.id between :minId and :maxId + """, Post.class) + .setParameter("minId", 1L) + .setParameter("maxId", 50L) + .getResultList(); + + assertEquals(POST_COUNT, posts.size()); + + return posts; + }); + + for(Post post : _posts) { + assertEquals(POST_COMMENT_COUNT, post.getComments().size()); + assertEquals(TAG_COUNT, post.getTags().size()); + } + } + + @Test + public void testBlaze() { + CriteriaBuilderConfiguration config = Criteria.getDefault(); + CriteriaBuilderFactory criteriaBuilderFactory = config.createCriteriaBuilderFactory(entityManagerFactory()); + + EntityViewConfiguration entityViewConfiguration = EntityViews.createDefaultConfiguration() + .addEntityView(PostView.class) + .addEntityView(PostCommentView.class) + .addEntityView(TagView.class) + .addEntityView(PostWithCommentsAndTagsView.class); + + EntityViewManager entityViewManager = entityViewConfiguration + .createEntityViewManager(criteriaBuilderFactory); + + List posts = doInJPA(entityManager -> { + return entityViewManager.applySetting( + EntityViewSetting.create(PostWithCommentsAndTagsView.class), + criteriaBuilderFactory.create(entityManager, Post.class) + ) + .where(Post_.ID) + .betweenExpression(":minId") + .andExpression(":maxId") + .setParameter("minId", 1L) + .setParameter("maxId", 50L) + .getResultList(); + }); + + for(PostWithCommentsAndTagsView post : posts) { + assertEquals(POST_COMMENT_COUNT, post.getComments().size()); + assertEquals(TAG_COUNT, post.getTags().size()); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } + + @EntityView(Post.class) + public interface PostView { + @IdMapping + Long getId(); + + String getTitle(); + } + + @EntityView(PostComment.class) + public interface PostCommentView { + @IdMapping + Long getId(); + + String getReview(); + } + + @EntityView(Tag.class) + public interface TagView { + @IdMapping + Long getId(); + + String getName(); + } + + @EntityView(Post.class) + public interface PostWithCommentsAndTagsView extends PostView { + + @Mapping(fetch = MULTISET) + List getComments(); + + @Mapping(fetch = MULTISET) + List getTags(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/EagerFetchingMultipleSetTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/EagerFetchingMultipleSetTest.java new file mode 100644 index 000000000..bac64cc42 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/EagerFetchingMultipleSetTest.java @@ -0,0 +1,221 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.multiple; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.query.SQLExtractor; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class EagerFetchingMultipleSetTest extends AbstractPostgreSQLIntegrationTest { + + public static final int POST_COUNT = 50; + public static final int POST_COMMENT_COUNT = 20; + public static final int TAG_COUNT = 10; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + Tag.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + List tags = new ArrayList<>(); + + for (long i = 1; i <= TAG_COUNT; i++) { + Tag tag = new Tag() + .setId(i) + .setName(String.format("Tag nr. %d", i + 1)); + + entityManager.persist(tag); + tags.add(tag); + } + + long commentId = 0; + + for (long postId = 1; postId <= POST_COUNT; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post nr. %d", postId)); + + + for (long i = 0; i < POST_COMMENT_COUNT; i++) { + post.addComment( + new PostComment() + .setId(++commentId) + .setReview("Excellent!") + ); + } + + for (int i = 0; i < TAG_COUNT; i++) { + post.getTags().add(tags.get(i)); + } + + entityManager.persist(post); + } + }); + } + + @Test + public void testFindWithJoinFetchQuery() { + doInJPA(entityManager -> { + Query jpqlQuery = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + left join fetch p.tags + where p.id between :minId and :maxId + """, Post.class) + .setParameter("minId", 1L) + .setParameter("maxId", 50L); + + List posts = entityManager.createNativeQuery( + SQLExtractor.from(jpqlQuery) + , Tuple.class) + .setParameter(1, 1L) + .setParameter(2, 50L) + .getResultList(); + + assertEquals(POST_COUNT * POST_COMMENT_COUNT * TAG_COUNT, posts.size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private Set comments = new HashSet<>(); + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private Set tags = new HashSet<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Set getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Set getTags() { + return tags; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/EagerFetchingMultipleToOneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/EagerFetchingMultipleToOneTest.java new file mode 100644 index 000000000..b5920b0cb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/EagerFetchingMultipleToOneTest.java @@ -0,0 +1,227 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.multiple; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class EagerFetchingMultipleToOneTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + PostDetails.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + for (long id = 1; id <= 50; id++) { + Post post = new Post() + .setId(id) + .setTitle(String.format("Post nr. %d", id)); + + post.addComment( + new PostComment() + .setId(id) + .setReview(String.format("Post comment nr. %d", id)) + ); + + post.setDetails( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn(new Date()) + ); + + entityManager.persist(post); + } + }); + } + + @Test + public void testOneQueryTwoJoinFetch() { + doInJPA(entityManager -> { + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + join fetch pc.post p + join fetch p.details d + where pc.id between :minId and :maxId + """, PostComment.class) + .setParameter("minId", 1L) + .setParameter("maxId", 50L) + .getResultList(); + + assertEquals(50, comments.size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @OneToOne(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private PostDetails details; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + this.details = details; + details.setPost(this); + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Post post; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/MultiLevelCollectionFetchingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/MultiLevelCollectionFetchingTest.java new file mode 100644 index 000000000..50c137713 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/multiple/MultiLevelCollectionFetchingTest.java @@ -0,0 +1,365 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.multiple; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MultiLevelCollectionFetchingTest extends AbstractPostgreSQLIntegrationTest { + + public static final int POST_COUNT = 50; + public static final int POST_COMMENT_COUNT = 20; + public static final int TAG_COUNT = 10; + public static final int VOTE_COUNT = 5; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + Tag.class, + User.class, + UserVote.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + User alice = new User() + .setId(1L) + .setName("Alice"); + + User bob = new User() + .setId(2L) + .setName("Bob"); + + entityManager.persist(alice); + entityManager.persist(bob); + + List tags = new ArrayList<>(); + + for (long i = 1; i <= TAG_COUNT; i++) { + Tag tag = new Tag() + .setId(i) + .setName(String.format("Tag nr. %d", i)); + + entityManager.persist(tag); + tags.add(tag); + } + + long commentId = 0; + long voteId = 0; + + for (long postId = 1; postId <= POST_COUNT; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post nr. %d", postId)); + + + for (long i = 0; i < POST_COMMENT_COUNT; i++) { + PostComment comment = new PostComment() + .setId(++commentId) + .setReview("Excellent!"); + + for (int j = 0; j < VOTE_COUNT; j++) { + comment.addVote( + new UserVote() + .setId(++voteId) + .setScore(Math.random() > 0.5 ? 1 : -1) + .setUser(Math.random() > 0.5 ? alice : bob) + ); + } + + post.addComment(comment); + + } + + for (int i = 0; i < TAG_COUNT; i++) { + post.getTags().add(tags.get(i)); + } + + entityManager.persist(post); + } + }); + } + + @Test + public void testTwoJoinFetchQueries() { + List posts = doInJPA(entityManager -> { + List _posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + where p.id between :minId and :maxId + """, Post.class) + .setParameter("minId", 1L) + .setParameter("maxId", 50L) + .getResultList(); + + entityManager.createQuery(""" + select p + from Post p + left join fetch p.tags t + where p in :posts + """, Post.class) + .setParameter("posts", _posts) + .getResultList(); + + entityManager.createQuery(""" + select pc + from PostComment pc + left join fetch pc.votes v + join pc.post p + where p in :posts + """, PostComment.class) + .setParameter("posts", _posts) + .getResultList(); + + return _posts; + }); + + assertEquals(POST_COUNT, posts.size()); + + for (Post post : posts) { + assertEquals(POST_COMMENT_COUNT, post.getComments().size()); + for(PostComment comment : post.getComments()) { + assertEquals(VOTE_COUNT, comment.getVotes().size()); + } + assertEquals(TAG_COUNT, post.getTags().size()); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + @OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true) + private List votes = new ArrayList<>(); + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public List getVotes() { + return votes; + } + + public PostComment addVote(UserVote vote) { + votes.add(vote); + vote.setComment(this); + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } + + @Entity(name = "User") + @Table(name = "blog_user") + public static class User { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public User setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public User setName(String name) { + this.name = name; + return this; + } + } + + @Entity(name = "UserVote") + @Table(name = "user_vote") + public static class UserVote { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + private PostComment comment; + + private int score; + + public Long getId() { + return id; + } + + public UserVote setId(Long id) { + this.id = id; + return this; + } + + public User getUser() { + return user; + } + + public UserVote setUser(User user) { + this.user = user; + return this; + } + + public PostComment getComment() { + return comment; + } + + public UserVote setComment(PostComment comment) { + this.comment = comment; + return this; + } + + public int getScore() { + return score; + } + + public UserVote setScore(int score) { + this.score = score; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/DistinctPostResultTransformer.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/DistinctPostResultTransformer.java new file mode 100644 index 000000000..fca1a10ba --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/DistinctPostResultTransformer.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.pagination; + +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; +import jakarta.persistence.EntityManager; +import org.hibernate.query.ResultListTransformer; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class DistinctPostResultTransformer implements ResultListTransformer { + + private final EntityManager entityManager; + + public DistinctPostResultTransformer(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + public List transformList(List list) { + Map identifiableMap = new LinkedHashMap<>(list.size()); + for (Object entityArray : list) { + if (Object[].class.isAssignableFrom(entityArray.getClass())) { + Post post = null; + PostComment comment = null; + + Object[] tuples = (Object[]) entityArray; + + for (Object tuple : tuples) { + if(tuple instanceof Identifiable) { + entityManager.detach(tuple); + + if (tuple instanceof Post) { + post = (Post) tuple; + } else if (tuple instanceof PostComment) { + comment = (PostComment) tuple; + } else { + throw new UnsupportedOperationException( + "Tuple " + tuple.getClass() + " is not supported!" + ); + } + } + } + + if (post != null) { + if (!identifiableMap.containsKey(post.getId())) { + identifiableMap.put(post.getId(), post); + post.setComments(new ArrayList<>()); + } + if (comment != null) { + post.addComment(comment); + } + } + } + } + return new ArrayList<>(identifiableMap.values()); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/FailOnPaginationWithCollectionFetchTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/FailOnPaginationWithCollectionFetchTest.java new file mode 100644 index 000000000..7bfddb521 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/FailOnPaginationWithCollectionFetchTest.java @@ -0,0 +1,145 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.pagination; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.ParameterExpression; +import jakarta.persistence.criteria.Root; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Properties; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class FailOnPaginationWithCollectionFetchTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + public static final int COMMENT_COUNT = 5; + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty("hibernate.query.fail_on_pagination_over_collection_fetch", "true"); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + LocalDateTime timestamp = LocalDateTime.of( + 2018, 10, 9, 12, 0, 0, 0 + ); + + LongStream.rangeClosed(1, 50) + .forEach(postId -> { + Post post = new Post() + .setId(postId) + .setTitle( + String.format("High-Performance Java Persistence - Chapter %d", + postId) + ) + .setCreatedOn( + Timestamp.valueOf(timestamp.plusMinutes(postId)) + ); + + LongStream.rangeClosed(1, COMMENT_COUNT) + .forEach(commentOffset -> { + long commentId = ((postId - 1) * COMMENT_COUNT) + commentOffset; + + post.addComment( + new PostComment() + .setId(commentId) + .setReview( + String.format("Comment nr. %d - A must-read!", commentId) + ) + .setCreatedOn( + Timestamp.valueOf(timestamp.plusMinutes(commentId)) + ) + ); + + }); + + entityManager.persist(post); + }); + }); + } + + @Test + public void testFetchAndPaginate() { + doInJPA(entityManager -> { + try { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + where p.title like :titlePattern + order by p.createdOn + """, Post.class) + .setParameter("titlePattern", "High-Performance Java Persistence %") + .setMaxResults(5) + .getResultList(); + + fail("Should have thrown Exception"); + } catch (Exception e) { + LOGGER.debug("Expected", e); + assertTrue(e.getMessage().contains("in-memory pagination")); + } + }); + } + + @Test + public void testFetchAndPaginateWithCriteriaApi() { + doInJPA(entityManager -> { + try { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteria = builder.createQuery(Post.class); + + Root post = criteria.from(Post.class); + post.fetch("http://github.com/comments"); + + ParameterExpression parameterExpression = builder.parameter(String.class); + + criteria.where( + builder.like( + post.get("title"), + parameterExpression + ) + ) + .orderBy( + builder.asc( + post.get("createdOn") + ) + ); + + entityManager + .createQuery(criteria) + .setParameter(parameterExpression, "High-Performance Java Persistence %") + .setMaxResults(5) + .getResultList(); + + fail("Should have thrown Exception"); + } catch (Exception e) { + assertTrue(e.getMessage().contains("in-memory pagination")); + } + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/PaginationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/PaginationTest.java new file mode 100644 index 000000000..09761e070 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/PaginationTest.java @@ -0,0 +1,467 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.pagination; + +import com.vladmihalcea.hpjp.hibernate.fetching.PostCommentSummary; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Tuple; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.jpa.AvailableHints; +import org.hibernate.query.NativeQuery; +import org.junit.Ignore; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Properties; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PaginationTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + public static final int COMMENT_COUNT = 5; + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty( + AvailableSettings.FAIL_ON_PAGINATION_OVER_COLLECTION_FETCH, + Boolean.FALSE.toString() + ); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + LocalDateTime timestamp = LocalDateTime.of( + 2018, 10, 9, 12, 0, 0, 0 + ); + + LongStream.rangeClosed(1, 50) + .forEach(postId -> { + Post post = new Post() + .setId(postId) + .setTitle( + String.format("High-Performance Java Persistence - Chapter %d", + postId) + ) + .setCreatedOn( + Timestamp.valueOf(timestamp.plusMinutes(postId)) + ); + + LongStream.rangeClosed(1, COMMENT_COUNT) + .forEach(commentOffset -> { + long commentId = ((postId - 1) * COMMENT_COUNT) + commentOffset; + + post.addComment( + new PostComment() + .setId(commentId) + .setReview( + String.format("Comment nr. %d - A must-read!", commentId) + ) + .setCreatedOn( + Timestamp.valueOf(timestamp.plusMinutes(commentId)) + ) + ); + + }); + + entityManager.persist(post); + }); + }); + } + + @Test + public void testLimit() { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + order by p.createdOn + """, Post.class) + .setMaxResults(10) + .getResultList(); + + assertEquals(10, posts.size()); + assertEquals("High-Performance Java Persistence - Chapter 1", posts.get(0).getTitle()); + assertEquals("High-Performance Java Persistence - Chapter 10", posts.get(9).getTitle()); + }); + } + + @Test + public void testLimitNativeSql() { + doInJPA(entityManager -> { + List posts = entityManager.createNativeQuery(""" + select p.title + from post p + order by p.created_on + """) + .setMaxResults(10) + .getResultList(); + + assertEquals(10, posts.size()); + assertEquals("High-Performance Java Persistence - Chapter 1", posts.get(0)); + assertEquals("High-Performance Java Persistence - Chapter 10", posts.get(9)); + }); + } + + @Test + public void testOffset() { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + order by p.createdOn + """, Post.class) + .setFirstResult(20) + .setMaxResults(10) + .getResultList(); + + assertEquals(10, posts.size()); + assertEquals("High-Performance Java Persistence - Chapter 21", posts.get(0).getTitle()); + assertEquals("High-Performance Java Persistence - Chapter 30", posts.get(9).getTitle()); + }); + } + + @Test + public void testOffsetNative() { + doInJPA(entityManager -> { + List posts = entityManager.createNativeQuery(""" + SELECT p.id AS id, p.created_on AS created_on, p.title AS title + FROM post p + ORDER BY p.created_on + """, Tuple.class) + .setFirstResult(10) + .setMaxResults(10) + .getResultList(); + + assertEquals(10, posts.size()); + assertEquals("High-Performance Java Persistence - Chapter 11", posts.get(0).get("title")); + assertEquals("High-Performance Java Persistence - Chapter 20", posts.get(9).get("title")); + }); + } + + @Test + public void testDTO() { + doInJPA(entityManager -> { + List summaries = entityManager.createQuery(""" + select new + com.vladmihalcea.hpjp.hibernate.fetching.PostCommentSummary( + p.id, p.title, c.review + ) + from PostComment c + join c.post p + order by c.createdOn + """) + .setMaxResults(10) + .getResultList(); + + assertEquals(10, summaries.size()); + assertEquals("High-Performance Java Persistence - Chapter 1", summaries.get(0).getTitle()); + assertEquals("Comment nr. 1 - A must-read!", summaries.get(0).getReview()); + + assertEquals("High-Performance Java Persistence - Chapter 2", summaries.get(9).getTitle()); + assertEquals("Comment nr. 10 - A must-read!", summaries.get(9).getReview()); + }); + } + + @Test + public void testFetchAndPaginate() { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + where p.title like :titlePattern + order by p.createdOn, p.id + """, Post.class) + .setParameter("titlePattern", "High-Performance Java Persistence %") + .setMaxResults(5) + .getResultList(); + + assertEquals(5, posts.size()); + assertArrayEquals( + LongStream.rangeClosed(1, 5).toArray(), + posts.stream().mapToLong(Post::getId).toArray() + ); + }); + } + + @Test + public void testFetchAndPaginateWithTwoQueries() { + doInJPA(entityManager -> { + List postIds = entityManager.createQuery(""" + select p.id + from Post p + where p.title like :titlePattern + order by p.createdOn, p.id + """, Long.class) + .setParameter("titlePattern", "High-Performance Java Persistence %") + .setMaxResults(5) + .getResultList(); + + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + where p.id in (:postIds) + order by p.createdOn, p.id + """, Post.class) + .setParameter("postIds", postIds) + .getResultList(); + + assertEquals(5, posts.size()); + + Post post1 = posts.get(0); + + List comments = post1.getComments(); + + for (int i = 0; i < COMMENT_COUNT - 1; i++) { + PostComment postComment1 = comments.get(i); + + assertEquals( + String.format( + "Comment nr. %d - A must-read!", + i + 1 + ), + postComment1.getReview() + ); + } + }); + } + + @Test + public void testFetchAndPaginateParentWithNoChild() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(100L) + .setTitle("High-Performance Java Persistence - Second Edition") + .setCreatedOn( + Timestamp.valueOf( + LocalDateTime.of( + 2018, 10, 8, 12, 0, 0, 0 + ) + ) + ) + ); + }); + + List posts = doInJPA(entityManager -> { + DistinctPostResultTransformer resultTransformer = new DistinctPostResultTransformer(entityManager); + + return entityManager + .createNamedQuery("PostWithCommentByRank") + .setParameter("titlePattern", "High-Performance Java Persistence %") + .setParameter("rank", 2) + .setHint(AvailableHints.HINT_READ_ONLY, true) + .unwrap(NativeQuery.class) + .setResultListTransformer(resultTransformer) + .getResultList(); + }); + + assertEquals(2, posts.size()); + + Post post1 = posts.get(0); + long commentId = ((post1.getId() - 1) * COMMENT_COUNT); + + post1.addComment( + new PostComment() + .setId(commentId) + .setReview( + String.format("Comment nr. %d", commentId) + ) + .setCreatedOn( + Timestamp.valueOf(LocalDateTime.now()) + ) + ); + + Post post2 = posts.get(1); + post2.removeComment(post2.getComments().get(0)); + + doInJPA(entityManager -> { + entityManager.merge(post1); + entityManager.merge(post2); + }); + } + + @Test + public void testFetchAndPaginateUsingDenseRank() { + doInJPA(entityManager -> { + List posts = entityManager.createNamedQuery("PostWithCommentByRank") + .setParameter("titlePattern", "High-Performance Java Persistence %") + .setParameter("rank", 5) + .unwrap(NativeQuery.class) + .setResultListTransformer(new DistinctPostResultTransformer(entityManager)) + .getResultList(); + + assertEquals(5, posts.size()); + + Post post1 = posts.get(0); + + List comments = post1.getComments(); + + for (int i = 0; i < COMMENT_COUNT - 1; i++) { + PostComment postComment1 = comments.get(i); + + assertEquals( + String.format( + "Comment nr. %d - A must-read!", + i + 1 + ), + postComment1.getReview() + ); + } + }); + } + + @Test + public void testFetchAndPaginateUsingDenseRankAndMerge() { + List posts = doInJPA(entityManager -> { + return entityManager + .createNamedQuery("PostWithCommentByRank") + .setParameter( + "titlePattern", + "High-Performance Java Persistence %" + ) + .setParameter( + "rank", + 2 + ) + .unwrap(NativeQuery.class) + .setResultListTransformer(new DistinctPostResultTransformer(entityManager)) + .getResultList(); + }); + + assertEquals(2, posts.size()); + + Post post1 = posts.get(0); + + post1.addComment( + new PostComment() + .setId((post1.getId() - 1) * COMMENT_COUNT) + .setReview("Awesome!") + .setCreatedOn( + Timestamp.valueOf(LocalDateTime.now()) + ) + ); + + Post post2 = posts.get(1); + post2.removeComment(post2.getComments().get(0)); + + doInJPA(entityManager -> { + entityManager.merge(post1); + entityManager.merge(post2); + }); + } + + @Test + @SuppressWarnings("unchecked") + public void testFetchAndPaginateUsingDenseRankNativeSQL() { + doInJPA(entityManager -> { + List posts = entityManager.createNativeQuery(""" + SELECT * + FROM ( + SELECT *, + dense_rank() OVER ( + ORDER BY "p.created_on", "p.id" + ) rank + FROM ( + SELECT p.id AS "p.id", + p.created_on AS "p.created_on", + p.title AS "p.title", + pc.id as "pc.id", + pc.created_on AS "pc.created_on", + pc.review AS "pc.review", + pc.post_id AS "pc.post_id" + FROM post p + LEFT JOIN post_comment pc ON p.id = pc.post_id + WHERE p.title LIKE :titlePattern + ORDER BY p.created_on + ) p_pc + ) p_pc_r + WHERE p_pc_r.rank <= :rank + """, + Tuple.class) + .setParameter("titlePattern", "High-Performance Java Persistence %") + .setParameter("rank", 5) + .getResultList(); + + assertEquals(5 * COMMENT_COUNT, posts.size()); + }); + } + + @Test + public void testFetchAndPaginateUsingDenseRankJPQL() { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments pc + where p.id in ( + select id + from ( + select + id as id, + dense_rank() over (order by createdOn ASC) as ranking + from Post + where title like :titlePattern + ) pr + where ranking <= :rank + ) + """, + Post.class) + .setParameter("titlePattern", "High-Performance Java Persistence %") + .setParameter("rank", 5) + .getResultList(); + + assertEquals(5, posts.size()); + }); + } + + @Test + @Ignore("Still not working on Hibernate 6.3") + public void testFetchAndPaginateUsingDenseRankJPQLWithCTE() { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + with p_pc as ( + select + p, + dense_rank() over ( + order by p.createdOn, p.id + ) rank + from Post p + left join fetch p.comments pc + where p.title like :titlePattern + order by p.createdOn + ) + select p + from p_pc + where p_pc.rank <= :rank + """, + Post.class) + .setParameter("titlePattern", "High-Performance Java Persistence %") + .setParameter("rank", 5) + .getResultList(); + + assertEquals(5 * COMMENT_COUNT, posts.size()); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/Post.java new file mode 100644 index 000000000..875df0f61 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/Post.java @@ -0,0 +1,122 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.pagination; + +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +@NamedNativeQuery( + name = "PostWithCommentByRank", + query = """ + SELECT * + FROM ( + SELECT + *, + DENSE_RANK() OVER ( + ORDER BY "p.created_on", "p.id" + ) rank + FROM ( + SELECT + p.id AS "p.id", p.created_on AS "p.created_on", + p.title AS "p.title", pc.post_id AS "pc.post_id", + pc.id as "pc.id", pc.created_on AS "pc.created_on", + pc.review AS "pc.review" + FROM post p + LEFT JOIN post_comment pc ON p.id = pc.post_id + WHERE p.title LIKE :titlePattern + ORDER BY p.created_on + ) p_pc + ) p_pc_r + WHERE p_pc_r.rank <= :rank + """, + resultSetMapping = "PostWithCommentByRankMapping" +) +@SqlResultSetMapping( + name = "PostWithCommentByRankMapping", + entities = { + @EntityResult( + entityClass = Post.class, + fields = { + @FieldResult(name = "id", column = "p.id"), + @FieldResult(name = "createdOn", column = "p.created_on"), + @FieldResult(name = "title", column = "p.title"), + } + ), + @EntityResult( + entityClass = PostComment.class, + fields = { + @FieldResult(name = "id", column = "pc.id"), + @FieldResult(name = "createdOn", column = "pc.created_on"), + @FieldResult(name = "review", column = "pc.review"), + @FieldResult(name = "post", column = "pc.post_id"), + } + ) + } +) +public class Post implements Identifiable { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on", nullable = false) + private Timestamp createdOn; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(Timestamp createdOn) { + this.createdOn = createdOn; + return this; + } + + public List getComments() { + return comments; + } + + public Post setComments(List comments) { + this.comments = comments; + return this; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/PostComment.java new file mode 100644 index 000000000..63c4cd084 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/PostComment.java @@ -0,0 +1,62 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.pagination; + +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.util.Date; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment implements Identifiable { + + @Id + private Long id; + + @ManyToOne + private Post post; + + private String review; + + @Column(name = "created_on") + private Timestamp createdOn; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(Timestamp createdOn) { + this.createdOn = createdOn; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/blaze/BlazeKeysetPaginationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/blaze/BlazeKeysetPaginationTest.java new file mode 100644 index 000000000..d2617b032 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/fetching/pagination/blaze/BlazeKeysetPaginationTest.java @@ -0,0 +1,160 @@ +package com.vladmihalcea.hpjp.hibernate.fetching.pagination.blaze; + +import com.blazebit.persistence.Criteria; +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.PagedList; +import com.blazebit.persistence.spi.CriteriaBuilderConfiguration; +import com.vladmihalcea.hpjp.hibernate.fetching.pagination.Post; +import com.vladmihalcea.hpjp.hibernate.fetching.pagination.PostComment; +import com.vladmihalcea.hpjp.hibernate.fetching.pagination.Post_; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import jakarta.persistence.EntityManagerFactory; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Properties; +import java.util.stream.LongStream; + +/** + * @author Vlad Mihalcea + */ +public class BlazeKeysetPaginationTest extends AbstractTest { + + private CriteriaBuilderFactory cbf; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected EntityManagerFactory newEntityManagerFactory() { + EntityManagerFactory entityManagerFactory = super.newEntityManagerFactory(); + CriteriaBuilderConfiguration config = Criteria.getDefault(); + cbf = config.createCriteriaBuilderFactory(entityManagerFactory); + return entityManagerFactory; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + public static final int COMMENT_COUNT = 5; + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty( + AvailableSettings.FAIL_ON_PAGINATION_OVER_COLLECTION_FETCH, + Boolean.FALSE.toString() + ); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + LocalDateTime timestamp = LocalDateTime.of( + 2021, 10, 9, 12, 0, 0, 0 + ); + + LongStream.rangeClosed(1, 50) + .forEach(postId -> { + Post post = new Post() + .setId(postId) + .setTitle( + String.format("High-Performance Java Persistence - Chapter %d", + postId) + ) + .setCreatedOn( + Timestamp.valueOf(timestamp.plusMinutes(postId)) + ); + + LongStream.rangeClosed(1, COMMENT_COUNT) + .forEach(commentOffset -> { + long commentId = ((postId - 1) * COMMENT_COUNT) + commentOffset; + + post.addComment( + new PostComment() + .setId(commentId) + .setReview( + String.format("Comment nr. %d - A must-read!", commentId) + ) + .setCreatedOn( + Timestamp.valueOf(timestamp.plusMinutes(commentId)) + ) + ); + + }); + + entityManager.persist(post); + }); + }); + } + + @Test + public void testKeysetPagination() { + doInJPA(entityManager -> { + int pageSize = 10; + + PagedList postPage = cbf + .create(entityManager, Post.class) + .orderByAsc(Post_.CREATED_ON) + .orderByAsc(Post_.ID) + .page(0, pageSize) + .withKeysetExtraction(true) + .getResultList(); + + LOGGER.info("Matching entity count: {}", postPage.getTotalSize()); + LOGGER.info("Page count: {}", postPage.getTotalPages()); + LOGGER.info("Current page number: {}", postPage.getPage()); + LOGGER.info("Post ids: {}", + postPage.stream() + .map(Post::getId) + .toList() + ); + + postPage = cbf + .create(entityManager, Post.class) + .orderByAsc(Post_.CREATED_ON) + .orderByAsc(Post_.ID) + .page( + postPage.getKeysetPage(), + postPage.getPage() * postPage.getMaxResults(), + postPage.getMaxResults() + ) + .getResultList(); + + LOGGER.info("Current page number: {}", postPage.getPage()); + LOGGER.info("Post ids: {}", + postPage.stream() + .map(Post::getId) + .toList() + ); + + postPage = cbf + .create(entityManager, Post.class) + .orderByAsc(Post_.CREATED_ON) + .orderByAsc(Post_.ID) + .page( + postPage.getKeysetPage(), + postPage.getPage() * postPage.getMaxResults(), + postPage.getMaxResults() + ) + .withKeysetExtraction(true) + .getResultList(); + + LOGGER.info("Current page number: {}", postPage.getPage()); + LOGGER.info("Post ids: {}", + postPage.stream() + .map(Post::getId) + .toList() + ); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/AlwaysFlushTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/AlwaysFlushTest.java new file mode 100644 index 000000000..5b7363b13 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/AlwaysFlushTest.java @@ -0,0 +1,235 @@ +package com.vladmihalcea.hpjp.hibernate.flushing; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.FlushMode; +import org.hibernate.query.NativeQuery; +import org.hibernate.transform.Transformers; +import org.jboss.logging.Logger; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class AlwaysFlushTest extends AbstractPostgreSQLIntegrationTest { + + private static final Logger log = Logger.getLogger(AlwaysFlushTest.class); + + @Override + protected Class[] entities() { + return new Class[] { + Board.class, + Post.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testFlushSQL() { + doInJPA(entityManager -> { + entityManager.createNativeQuery("delete from Post").executeUpdate(); + entityManager.createNativeQuery("delete from Board").executeUpdate(); + }); + doInJPA(entityManager -> { + log.info("testFlushSQL"); + + Board board1 = new Board(); + board1.setName("JPA"); + Board board2 = new Board(); + board2.setName("Hibernate"); + + entityManager.persist(board1); + entityManager.persist(board2); + + Post post1 = new Post("JPA 1"); + post1.setVersion(1); + post1.setBoard(board1); + entityManager.persist(post1); + + Post post2 = new Post("Hibernate 1"); + post2.setVersion(2); + post2.setBoard(board2); + entityManager.persist(post2); + + Post post3 = new Post("Hibernate 3"); + post3.setVersion(1); + post3.setBoard(board2); + entityManager.persist(post3); + + List result = entityManager.createNativeQuery(""" + SELECT b.name as "forumName", COUNT (p) as "postCount" + FROM post p + JOIN board b on b.id = p.board_id + GROUP BY b.name + """) + .unwrap(NativeQuery.class) + .setHibernateFlushMode(FlushMode.ALWAYS) + .setTupleTransformer(Transformers.aliasToBean(ForumCount.class)) + .getResultList(); + + assertEquals(result.size(), 2); + }); + } + + @Test + public void testSynchronizeSQL() { + doInJPA(entityManager -> { + entityManager.createNativeQuery("delete from Post").executeUpdate(); + entityManager.createNativeQuery("delete from Board").executeUpdate(); + }); + doInJPA(entityManager -> { + log.info("testFlushSQL"); + + Board board1 = new Board(); + board1.setName("JPA"); + Board board2 = new Board(); + board2.setName("Hibernate"); + + entityManager.persist(board1); + entityManager.persist(board2); + + Post post1 = new Post("JPA 1"); + post1.setVersion(1); + post1.setBoard(board1); + entityManager.persist(post1); + + Post post2 = new Post("Hibernate 1"); + post2.setVersion(2); + post2.setBoard(board2); + entityManager.persist(post2); + + Post post3 = new Post("Hibernate 3"); + post3.setVersion(1); + post3.setBoard(board2); + entityManager.persist(post3); + + List result = entityManager.createNativeQuery(""" + SELECT b.name as "forumName", COUNT (p) as "postCount" + FROM post p + JOIN board b on b.id = p.board_id + GROUP BY b.name + """) + .unwrap(NativeQuery.class) + .addSynchronizedEntityClass(Board.class) + .addSynchronizedEntityClass(Post.class) + .setTupleTransformer(Transformers.aliasToBean(ForumCount.class)) + .getResultList(); + + assertEquals(result.size(), 2); + }); + } + + public static class ForumCount { + + private String forumName; + + private long postCount; + + public String getForumName() { + return forumName; + } + + public void setForumName(String forumName) { + this.forumName = forumName; + } + + public long getPostCount() { + return postCount; + } + + public void setPostCount(Number postCount) { + this.postCount = postCount.longValue(); + } + } + + @Entity(name = "Board") + @Table(name = "board") + public static class Board { + + @Id + @GeneratedValue + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @ManyToOne + private Board board; + + @Version + private short version; + + public Post() {} + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Board getBoard() { + return board; + } + + public void setBoard(Board board) { + this.board = board; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = (short) version; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/BatchProcessingArticleTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/BatchProcessingArticleTest.java new file mode 100644 index 000000000..430ad0446 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/BatchProcessingArticleTest.java @@ -0,0 +1,104 @@ +package com.vladmihalcea.hpjp.hibernate.flushing; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class BatchProcessingArticleTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.jdbc.batch_size", "25"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_versioned_data", "true"); + return properties; + } + + @Test + public void testFlushClearCommit() { + int entityCount = 50; + int batchSize = 25; + + EntityManager entityManager = entityManagerFactory().createEntityManager(); + EntityTransaction entityTransaction = entityManager.getTransaction(); + + try { + entityTransaction.begin(); + + for (int i = 0; i < entityCount; i++) { + if (i > 0 && i % batchSize == 0) { + entityTransaction.commit(); + entityTransaction.begin(); + + entityManager.clear(); + } + + Post post = new Post(String.format("Post %d", i + 1)); + entityManager.persist(post); + } + + entityTransaction.commit(); + } catch (RuntimeException e) { + if (entityTransaction.isActive()) { + entityTransaction.rollback(); + } + throw e; + } finally { + entityManager.close(); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(generator = "seq_post") + @SequenceGenerator( + name = "seq_post", + sequenceName = "seq_post", + allocationSize = 25 + ) + private Long id; + + private String title; + + + public Post() { + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/BatchProcessingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/BatchProcessingTest.java new file mode 100644 index 000000000..0b027a9f7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/BatchProcessingTest.java @@ -0,0 +1,102 @@ +package com.vladmihalcea.hpjp.hibernate.flushing; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class BatchProcessingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.jdbc.batch_size", "25"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_versioned_data", "true"); + return properties; + } + + @Test + public void testFlushClearCommit() { + int entityCount = 50; + int batchSize = 25; + + EntityManager entityManager = entityManagerFactory().createEntityManager(); + + try { + entityManager.getTransaction().begin(); + + for (int i = 0; i < entityCount; ++i) { + if (i > 0 && i % batchSize == 0) { + flush(entityManager); + } + + Post post = new Post().setTitle(String.format("Post %d", i + 1)); + entityManager.persist(post); + } + + entityManager.getTransaction().commit(); + } catch (RuntimeException e) { + if (entityManager.getTransaction().isActive()) { + entityManager.getTransaction().rollback(); + } + throw e; + } finally { + entityManager.close(); + } + } + + private void flush(EntityManager entityManager) { + //Commit triggers a flush when using FlushType.AUTO + entityManager.getTransaction().commit(); + entityManager.getTransaction().begin(); + + entityManager.clear(); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(generator = "seq_post") + @SequenceGenerator( + name = "seq_post", + sequenceName = "seq_post", + allocationSize = 25 + ) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/BytecodeEnhancementDirtyCheckingPerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/BytecodeEnhancementDirtyCheckingPerformanceTest.java similarity index 77% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/BytecodeEnhancementDirtyCheckingPerformanceTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/BytecodeEnhancementDirtyCheckingPerformanceTest.java index c2cc29370..caf4d5b28 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/BytecodeEnhancementDirtyCheckingPerformanceTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/BytecodeEnhancementDirtyCheckingPerformanceTest.java @@ -1,21 +1,22 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; +package com.vladmihalcea.hpjp.hibernate.flushing; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.Timer; -import com.vladmihalcea.book.hpjp.hibernate.forum.Post; -import com.vladmihalcea.book.hpjp.hibernate.forum.PostComment; -import com.vladmihalcea.book.hpjp.hibernate.forum.PostDetails; -import com.vladmihalcea.book.hpjp.hibernate.forum.Tag; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.hibernate.forum.Post; +import com.vladmihalcea.hpjp.hibernate.forum.PostComment; +import com.vladmihalcea.hpjp.hibernate.forum.PostDetails; +import com.vladmihalcea.hpjp.hibernate.forum.Tag; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.EmptyInterceptor; import org.hibernate.Interceptor; import org.hibernate.type.Type; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import javax.persistence.EntityManager; +import jakarta.persistence.EntityManager; import java.io.Serializable; import java.util.*; import java.util.concurrent.TimeUnit; @@ -50,9 +51,9 @@ public static Collection rdbmsDataSourceProvider() { List counts = new ArrayList<>(); counts.add(new Integer[] {5}); counts.add(new Integer[] {10}); -/* counts.add(new Integer[] {20}); + counts.add(new Integer[] {20}); counts.add(new Integer[] {50}); - counts.add(new Integer[] {100});*/ + counts.add(new Integer[] {100}); return counts; } @@ -100,20 +101,32 @@ public void init() { super.init(); doInJPA(entityManager -> { for (int i = 0; i < entityCount; i++) { - Post post = new Post("JPA with Hibernate"); - post.setId(i * 10L); - - PostDetails details = new PostDetails(); - details.setCreatedOn(new Date()); - details.setCreatedBy("Vlad"); - post.addDetails(details); - - Tag tag1 = new Tag(); - tag1.setId(i * 10L); - tag1.setName("Java"); - Tag tag2 = new Tag(); - tag2.setId(i * 10L + 1); - tag2.setName("Hibernate"); + Post post = new Post() + .setId(i * 10L) + .setTitle("JPA with Hibernate") + .setDetails( + new PostDetails() + .setCreatedOn(new Date()) + .setCreatedBy("Vlad MIhalcea") + ) + .addComment( + new PostComment() + .setId(i * 10L) + .setReview("Good") + ) + .addComment( + new PostComment() + .setId(i * 10L + 1) + .setReview("Excellent") + ); + + Tag tag1 = new Tag() + .setId(i * 10L) + .setName("Java"); + + Tag tag2 = new Tag() + .setId(i * 10L + 1) + .setName("Hibernate"); entityManager.persist(post); @@ -123,17 +136,6 @@ public void init() { post.getTags().add(tag1); post.getTags().add(tag2); - PostComment comment1 = new PostComment(); - comment1.setId(i * 10L); - comment1.setReview("Good"); - - PostComment comment2 = new PostComment(); - comment2.setId(i * 10L + 1); - comment2.setReview("Excellent"); - - post.addComment(comment1); - post.addComment(comment2); - entityManager.flush(); postIds.add(post.getId()); } @@ -141,6 +143,7 @@ public void init() { } @Test + @Ignore public void testDirtyChecking() { doInJPA(entityManager -> { List posts = posts(entityManager); @@ -168,7 +171,7 @@ public void testDirtyChecking() { private List posts(EntityManager entityManager) { return entityManager.createQuery( - "select distinct pc " + + "select pc " + "from PostComment pc " + "join fetch pc.post p " + "join fetch p.tags " + diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/CommitFlushTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/CommitFlushTest.java similarity index 83% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/CommitFlushTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/CommitFlushTest.java index 48c28cb1e..7bc0a63a3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/CommitFlushTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/CommitFlushTest.java @@ -1,13 +1,13 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; +package com.vladmihalcea.hpjp.hibernate.flushing; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; import org.jboss.logging.Logger; import org.junit.Test; -import javax.persistence.FlushModeType; +import jakarta.persistence.FlushModeType; -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.Post; +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.Post; import static org.junit.Assert.assertTrue; /** diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/DefaultDirtyCheckingPerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/DefaultDirtyCheckingPerformanceTest.java similarity index 97% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/DefaultDirtyCheckingPerformanceTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/DefaultDirtyCheckingPerformanceTest.java index ecfa7e181..5849dd183 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/DefaultDirtyCheckingPerformanceTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/DefaultDirtyCheckingPerformanceTest.java @@ -1,17 +1,18 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; +package com.vladmihalcea.hpjp.hibernate.flushing; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.Timer; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.EmptyInterceptor; import org.hibernate.Interceptor; import org.hibernate.type.Type; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serializable; import java.util.*; import java.util.concurrent.TimeUnit; @@ -46,9 +47,9 @@ public static Collection rdbmsDataSourceProvider() { List counts = new ArrayList<>(); counts.add(new Integer[] {5}); counts.add(new Integer[] {10}); -/* counts.add(new Integer[] {20}); + counts.add(new Integer[] {20}); counts.add(new Integer[] {50}); - counts.add(new Integer[] {100});*/ + counts.add(new Integer[] {100}); return counts; } @@ -137,6 +138,7 @@ public void init() { } @Test + @Ignore public void testDirtyChecking() { doInJPA(entityManager -> { List posts = posts(entityManager); @@ -164,7 +166,7 @@ public void testDirtyChecking() { private List posts(EntityManager entityManager) { return entityManager.createQuery( - "select distinct pc " + + "select pc " + "from PostComment pc " + "join fetch pc.post p " + "join fetch p.tags " + @@ -329,7 +331,6 @@ public PostDetails() { } @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") @MapsId private Post post; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/HibernateAlwaysFlushConfigurationPropertyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/HibernateAlwaysFlushConfigurationPropertyTest.java new file mode 100644 index 000000000..146a18cb6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/HibernateAlwaysFlushConfigurationPropertyTest.java @@ -0,0 +1,56 @@ +package com.vladmihalcea.hpjp.hibernate.flushing; + +import org.hibernate.FlushMode; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class HibernateAlwaysFlushConfigurationPropertyTest extends JPAAutoFlushTest { + + @Override + protected boolean nativeHibernateSessionFactoryBootstrap() { + return true; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty( + AvailableSettings.FLUSH_MODE, FlushMode.ALWAYS.name() + ); + } + + @Test + public void testFlushAutoNativeSQL() { + doInJPA(entityManager -> { + assertEquals( + 0, + ((Number) + entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM post + """) + .getSingleResult() + ).intValue() + ); + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + + int postCount = ((Number) entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM post + """) + .getSingleResult()).intValue(); + + assertEquals(1, postCount); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/HibernateAutoFlushTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/HibernateAutoFlushTest.java new file mode 100644 index 000000000..dff3ed10b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/HibernateAutoFlushTest.java @@ -0,0 +1,180 @@ +package com.vladmihalcea.hpjp.hibernate.flushing; + +import org.hibernate.FlushMode; +import org.hibernate.Session; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class HibernateAutoFlushTest extends JPAAutoFlushTest { + + @Override + protected boolean nativeHibernateSessionFactoryBootstrap() { + return true; + } + + @Test + public void testFlushAutoNativeSQL() { + doInJPA(entityManager -> { + assertEquals( + 0, + ((Number) + entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM post + """) + .getSingleResult() + ).intValue() + ); + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + + assertEquals( + 0, + ((Number) + entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM post + """) + .getSingleResult() + ).intValue() + ); + }); + } + + @Test + public void testFlushAutoNativeSQLFlushModeAlways() { + doInJPA(entityManager -> { + assertEquals( + 0, + ((Number) + entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM post + """) + .getSingleResult() + ).intValue() + ); + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + + assertEquals( + 1, + ((Number) + entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM post + """) + .unwrap(org.hibernate.query.Query.class) + .setHibernateFlushMode(FlushMode.ALWAYS) + .getSingleResult() + ).intValue() + ); + }); + } + + @Test + public void testSessionModeAlways() { + doInJPA(entityManager -> { + assertEquals( + 0, + ((Number) + entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM post + """) + .getSingleResult() + ).intValue() + ); + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + + entityManager + .unwrap(Session.class) + .setHibernateFlushMode(FlushMode.ALWAYS); + + assertEquals( + 1, + ((Number) + entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM post + """) + .getSingleResult() + ).intValue() + ); + }); + } + + @Test + public void testFlushAutoNativeSQLSynchronizedEntityClass() { + doInJPA(entityManager -> { + assertEquals( + 0, + ((Number) + entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM post + """) + .getSingleResult() + ).intValue() + ); + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + + int postCount = ((Number) entityManager.unwrap(Session.class).createNativeQuery(""" + SELECT COUNT(*) + FROM post + """) + .addSynchronizedEntityClass(Post.class) + .getSingleResult()).intValue(); + + assertEquals(1, postCount); + }); + } + + @Test + public void testFlushAutoNativeSQLSynchronizedQuerySpace() { + doInJPA(entityManager -> { + assertEquals( + 0, + ((Number) + entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM post + """) + .getSingleResult() + ).intValue() + ); + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + + int postCount = ((Number) entityManager.unwrap(Session.class).createNativeQuery(""" + SELECT COUNT(*) + FROM post + """) + .addSynchronizedQuerySpace("post") + .getSingleResult()).intValue(); + + assertEquals(1, postCount); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/HibernateDeleteEntityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/HibernateDeleteEntityTest.java new file mode 100644 index 000000000..f42e04a4c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/HibernateDeleteEntityTest.java @@ -0,0 +1,70 @@ +package com.vladmihalcea.hpjp.hibernate.flushing; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +public class HibernateDeleteEntityTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected boolean nativeHibernateSessionFactoryBootstrap() { + return true; + } + + @Test + public void test() { + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + + entityManager.unwrap(Session.class).delete(post); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/HibernateSaveSequenceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/HibernateSaveSequenceTest.java similarity index 87% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/HibernateSaveSequenceTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/HibernateSaveSequenceTest.java index 020e9954d..f14f6eeb7 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/HibernateSaveSequenceTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/HibernateSaveSequenceTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; +package com.vladmihalcea.hpjp.hibernate.flushing; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea @@ -43,7 +43,7 @@ public static class Post { private String title; @Version - private Long version; + private Short version; public Long getId() { return id; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/JPAAutoFlushTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/JPAAutoFlushTest.java new file mode 100644 index 000000000..9be873fcb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/JPAAutoFlushTest.java @@ -0,0 +1,252 @@ +package com.vladmihalcea.hpjp.hibernate.flushing; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JPAAutoFlushTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + Tag.class + }; + } + + @Override + protected boolean nativeHibernateSessionFactoryBootstrap() { + return false; + } + + @Test + public void testFlushAutoJPQL() { + doInJPA(entityManager -> { + assertEquals( + 0, + ((Number) entityManager.createQuery(""" + select count(p) + from Post p + """) + .getSingleResult() + ).intValue() + ); + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + + int tagCount = ((Number) entityManager.createQuery(""" + select count(t) + from Tag t + """) + .getSingleResult()).intValue(); + + int postCount = ((Number) entityManager.createQuery(""" + select count(p) + from Post p + """) + .getSingleResult()).intValue(); + + assertEquals(1, postCount); + }); + } + + @Test + public void testFlushAutoJPQLTableSpaceOverlap() { + doInJPA(entityManager -> { + assertEquals( + 0, + ((Number) + entityManager.createQuery(""" + select count(p) + from Post p + """) + .getSingleResult() + ).intValue() + ); + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + + List details = entityManager.createQuery(""" + select pd + from PostDetails pd + join fetch pd.post + """) + .getResultList(); + + int postCount = ((Number) entityManager.createQuery(""" + select count(p) + from Post p + """) + .getSingleResult()).intValue(); + + assertEquals(1, postCount); + }); + } + + @Test + public void testFlushAutoNativeSQL() { + doInJPA(entityManager -> { + assertEquals( + 0, + ((Number) + entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM Post + """) + .getSingleResult() + ).intValue() + ); + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + + int tagCount = ((Number) entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM tag + """) + .getSingleResult()).intValue(); + + int postCount = ((Number) entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM post + """) + .getSingleResult()).intValue(); + + assertEquals(1, postCount); + }); + } + + @Test + public void testFlushAutoDoWorkNativeSQL() { + doInJPA(entityManager -> { + assertEquals( + 0, + ((Number) + entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM Post + """) + .getSingleResult() + ).intValue() + ); + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + + int postCount = (entityManager.unwrap(Session.class).doReturningWork(connection -> + selectColumn( + connection, + """ + SELECT COUNT(*) + FROM post + """, + Number.class + ) + )).intValue(); + + //doWork does not trigger a flush. + assertEquals(0, postCount); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY + ) + private PostDetails details; + + @ManyToMany + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private Set tags = new HashSet<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + public PostDetails() { + createdOn = new Date(); + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/JPARemoveEntityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/JPARemoveEntityTest.java new file mode 100644 index 000000000..a2c8177b8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/JPARemoveEntityTest.java @@ -0,0 +1,65 @@ +package com.vladmihalcea.hpjp.hibernate.flushing; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +public class JPARemoveEntityTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Test + public void test() { + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.getReference(Post.class, 1L); + + entityManager.remove(post); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/ManualFlushTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/ManualFlushTest.java similarity index 75% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/ManualFlushTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/ManualFlushTest.java index f7932fcde..2470517df 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/ManualFlushTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/ManualFlushTest.java @@ -1,13 +1,13 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; +package com.vladmihalcea.hpjp.hibernate.flushing; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; import org.hibernate.FlushMode; import org.hibernate.Session; import org.jboss.logging.Logger; import org.junit.Test; -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.Post; +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.Post; import static org.junit.Assert.assertTrue; /** @@ -36,14 +36,14 @@ public void testFlushSQL() { entityManager.persist(post); Session session = entityManager.unwrap(Session.class); - session.setFlushMode(FlushMode.MANUAL); + session.setHibernateFlushMode(FlushMode.MANUAL); assertTrue(((Number) entityManager .createQuery("select count(id) from Post") .getSingleResult()).intValue() == 0); assertTrue(((Number) session - .createSQLQuery("select count(*) from Post") + .createNativeQuery("select count(*) from Post") .uniqueResult()).intValue() == 0); }); } diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/PersistIdentityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/PersistIdentityTest.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/PersistIdentityTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/PersistIdentityTest.java index 66946d15c..bf10b14e4 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/PersistIdentityTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/PersistIdentityTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; +package com.vladmihalcea.hpjp.hibernate.flushing; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/PersistSequenceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/PersistSequenceTest.java similarity index 77% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/PersistSequenceTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/PersistSequenceTest.java index aaa0e3c09..4cae4b5dc 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/PersistSequenceTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/PersistSequenceTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; +package com.vladmihalcea.hpjp.hibernate.flushing; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea @@ -17,7 +17,6 @@ protected Class[] entities() { }; } - @Test public void testId() { @@ -52,25 +51,11 @@ public void testMerge() { }); } - @Test - public void testMergeInsteadOfPersist() { - - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - - entityManager.merge(post); - LOGGER.info("The post entity identifier is {}", post.getId()); - }); - } - @Test public void testRedundantMerge() { doInJPA(entityManager -> { Post post = new Post(); - post.setId(1L); post.setTitle("High-Performance Java Persistence"); entityManager.persist(post); @@ -87,9 +72,6 @@ public static class Post { private String title; - @Version - private Long version; - public Long getId() { return id; } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/ReadOnlyQueryTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/ReadOnlyQueryTest.java new file mode 100644 index 000000000..c55b99027 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/ReadOnlyQueryTest.java @@ -0,0 +1,91 @@ +package com.vladmihalcea.hpjp.hibernate.flushing; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Session; +import org.hibernate.annotations.QueryHints; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.List; + +import static org.junit.Assert.assertFalse; + +/** + * @author Vlad Mihalcea + */ +public class ReadOnlyQueryTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + public void init() { + super.init(); + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + }); + } + + @Test + public void testReadOnly() { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .setHint(QueryHints.READ_ONLY, true) + .getResultList(); + }); + } + + @Test + public void testDefaultReadOnly() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + boolean isDefaultReadOnly = session.isDefaultReadOnly(); + assertFalse(isDefaultReadOnly); + session.setDefaultReadOnly(true); + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/RefreshTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/RefreshTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/RefreshTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/RefreshTest.java index cd97f817c..5b1197011 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/flushing/RefreshTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/RefreshTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.flushing; +package com.vladmihalcea.hpjp.hibernate.flushing; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.annotations.Generated; import org.hibernate.annotations.GenerationTime; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; @@ -97,7 +97,7 @@ public static class Post { private String createdOn; @Version - private int version; + private short version; public Post() { } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/SessionAlwaysFlushTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/SessionAlwaysFlushTest.java new file mode 100644 index 000000000..eb7d7fb6c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/SessionAlwaysFlushTest.java @@ -0,0 +1,229 @@ +package com.vladmihalcea.hpjp.hibernate.flushing; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.FlushMode; +import org.hibernate.Session; +import org.hibernate.transform.Transformers; +import org.jboss.logging.Logger; +import org.junit.Test; + +import java.math.BigInteger; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SessionAlwaysFlushTest extends AbstractPostgreSQLIntegrationTest { + + private static final Logger log = Logger.getLogger(AlwaysFlushTest.class); + + @Override + protected Class[] entities() { + return new Class[]{ + Board.class, + Post.class, + }; + } + + @Override + protected boolean nativeHibernateSessionFactoryBootstrap() { + return true; + } + + @Test + public void testFlushSQL() { + doInJPA(entityManager -> { + entityManager.createNativeQuery("delete from Post").executeUpdate(); + entityManager.createNativeQuery("delete from Board").executeUpdate(); + }); + doInJPA(entityManager -> { + log.info("testFlushSQL"); + + Board board1 = new Board(); + board1.setName("JPA"); + Board board2 = new Board(); + board2.setName("Hibernate"); + + entityManager.persist(board1); + entityManager.persist(board2); + + Post post1 = new Post("JPA 1"); + post1.setBoard(board1); + entityManager.persist(post1); + + Post post2 = new Post("Hibernate 1"); + post2.setBoard(board2); + entityManager.persist(post2); + + Post post3 = new Post("Hibernate 3"); + post3.setBoard(board2); + entityManager.persist(post3); + + Session session = entityManager.unwrap(Session.class); + List result = session.createNativeQuery(""" + SELECT + b.name as forum, + COUNT (p) as count + FROM post p + JOIN board b on b.id = p.board_id + GROUP BY forum + """) + .setHibernateFlushMode(FlushMode.ALWAYS) + .setResultTransformer(Transformers.aliasToBean(ForumCount.class)) + .list(); + + assertEquals(result.size(), 2); + }); + } + + @Test + public void testSynchronizeSQL() { + doInHibernate(session -> { + session.createNativeQuery("delete from Post").executeUpdate(); + session.createNativeQuery("delete from Board").executeUpdate(); + }); + doInHibernate(session -> { + log.info("testFlushSQL"); + + Board board1 = new Board(); + board1.setName("JPA"); + Board board2 = new Board(); + board2.setName("Hibernate"); + + session.persist(board1); + session.persist(board2); + + Post post1 = new Post("JPA 1"); + post1.setBoard(board1); + session.persist(post1); + + Post post2 = new Post("Hibernate 1"); + post2.setBoard(board2); + session.persist(post2); + + Post post3 = new Post("Hibernate 3"); + post3.setBoard(board2); + session.persist(post3); + + List result = session.createNativeQuery(""" + SELECT + b.name as forum, + COUNT (p) as count + FROM post p + JOIN board b on b.id = p.board_id + GROUP BY forum + """) + .addSynchronizedEntityClass(Board.class) + .addSynchronizedEntityClass(Post.class) + .setResultTransformer(Transformers.aliasToBean(ForumCount.class)) + .list(); + + assertEquals(result.size(), 2); + }); + } + + public static class ForumCount { + + private String forum; + + private Long count; + + public String getForum() { + return forum; + } + + public void setForum(String forum) { + this.forum = forum; + } + + public Long getCount() { + return count; + } + + public void setCount(Number count) { + this.count = count.longValue(); + } + } + + @Entity(name = "Board") + @Table(name = "board") + public static class Board { + + @Id + @GeneratedValue + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @ManyToOne + private Board board; + + @Version + private short version; + + public Post() { + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Board getBoard() { + return board; + } + + public void setBoard(Board board) { + this.board = board; + } + + public int getVersion() { + return version; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/order/FlushOrderBidirectionalOneToManyMergeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/order/FlushOrderBidirectionalOneToManyMergeTest.java new file mode 100644 index 000000000..b4f81dcd9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/order/FlushOrderBidirectionalOneToManyMergeTest.java @@ -0,0 +1,258 @@ +package com.vladmihalcea.hpjp.hibernate.flushing.order; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.NaturalId; +import org.hibernate.jpa.AvailableHints; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class FlushOrderBidirectionalOneToManyMergeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + + doInJPA(entityManager -> { + entityManager + .find(Post.class, 1L) + .addComment(new PostComment().setReview("JDBC section is a must-read!").setSlug("/1")) + .addComment(new PostComment().setReview("The book size is larger than usual.").setSlug("/2")) + .addComment(new PostComment().setReview("Just half-way through.").setSlug("/3")) + .addComment(new PostComment().setReview("The book has over 450 pages.").setSlug("/4")); + }); + } + + @Test + @Ignore + public void testPostMergeFlushOrderFail() { + Post post = fetchPostWithComments(1L); + + modifyPostComments(post); + + doInJPA(entityManager -> { + entityManager.merge(post); + }); + + verifyResults(); + } + + public Post fetchPostWithComments(Long postId) { + return doInJPA(entityManager -> { + return entityManager.createQuery( + "select p " + + "from Post p " + + "join fetch p.comments " + + "where p.id = :postId ", Post.class) + .setHint(AvailableHints.HINT_READ_ONLY, true) + .setParameter("postId", postId) + .getSingleResult(); + }); + } + + private void modifyPostComments(Post post) { + post.getComments().get(0).setReview("The JDBC part is a must-have!"); + + PostComment removedComment = post.getComments().get(2); + post.removeComment(removedComment); + + post.addComment( + new PostComment() + .setReview( + "The last part is about jOOQ and " + + "how to get the most of your relational database." + ) + .setSlug(removedComment.getSlug()) + ); + } + + private void verifyResults() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments c + where p.id = :idvorder by c.id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + assertEquals(4, post.getComments().size()); + + assertEquals( + "The JDBC part is a must-have!", + post.getComments().get(0).getReview() + ); + + assertEquals( + "The book size is larger than usual.", + post.getComments().get(1).getReview() + ); + + assertEquals( + "The book has over 450 pages.", + post.getComments().get(2).getReview() + ); + + assertEquals( + "The last part is about jOOQ and how to get the most of your relational database.", + post.getComments().get(3).getReview() + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + private Post setComments(List comments) { + this.comments = comments; + return this; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + + return this; + } + } + + @Entity(name = "PostComment") + @Table( + name = "post_comment", + uniqueConstraints = @UniqueConstraint( + name = "slug_uq", + columnNames = "slug" + ) + ) + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @NaturalId + private String slug; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getSlug() { + return slug; + } + + public PostComment setSlug(String slug) { + this.slug = slug; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostComment)) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/order/FlushOrderTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/order/FlushOrderTest.java new file mode 100644 index 000000000..a750d5fff --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/flushing/order/FlushOrderTest.java @@ -0,0 +1,141 @@ +package com.vladmihalcea.hpjp.hibernate.flushing.order; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class FlushOrderTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + post.setSlug("high-performance-java-persistence"); + + entityManager.persist(post); + }); + } + + @Test + @Ignore + public void testOperationOrder() { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.remove(post); + + Post newPost = new Post(); + newPost.setId(2L); + newPost.setTitle("High-Performance Java Persistence Book"); + newPost.setSlug("high-performance-java-persistence"); + entityManager.persist(newPost); + }); + } + + @Test + public void testOperationOrderWithManualFlush() { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + entityManager.remove(post); + + entityManager.flush(); + + Post newPost = new Post(); + newPost.setId(2L); + newPost.setTitle("High-Performance Java Persistence Book"); + newPost.setSlug("high-performance-java-persistence"); + entityManager.persist(newPost); + }); + } + + @Test + public void testUpdate() { + doInJPA(entityManager -> { + Post post = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Post.class) + .load("high-performance-java-persistence"); + + post.setTitle("High-Performance Java Persistence Book"); + + org.hibernate.engine.spi.PersistenceContext persistenceContext = getHibernatePersistenceContext( + entityManager + ); + + EntityEntry entityEntry = persistenceContext.getEntry(post); + Object[] loadedState = entityEntry.getLoadedState(); + assertNotNull(loadedState); + }); + } + + private org.hibernate.engine.spi.PersistenceContext getHibernatePersistenceContext( + EntityManager entityManager + ) { + SharedSessionContractImplementor session = entityManager.unwrap( + SharedSessionContractImplementor.class + ); + return session.getPersistenceContext(); + } + + @Entity(name = "Post") + @Table( + name = "post", + uniqueConstraints = @UniqueConstraint( + name = "slug_uq", + columnNames = "slug" + ) + ) + public static class Post { + + @Id + private Long id; + + private String title; + + @NaturalId + private String slug; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/AbstractPooledSequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/AbstractPooledSequenceIdentifierTest.java new file mode 100644 index 000000000..4cefed68a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/AbstractPooledSequenceIdentifierTest.java @@ -0,0 +1,79 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; + +import java.sql.Statement; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +public abstract class AbstractPooledSequenceIdentifierTest extends AbstractTest { + + protected abstract Object newEntityInstance(); + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.id.new_generator_mappings", "true"); + return properties; + } + + protected void insertSequences() { + LOGGER.debug("testSequenceIdentifierGenerator"); + doInJPA(entityManager -> { + for (int i = 0; i < 5; i++) { + entityManager.persist(newEntityInstance()); + entityManager.flush(); + } + entityManager.flush(); + assertEquals( + 5, + ((Number) entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM post + """ + ).getSingleResult()).intValue() + ); + + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + statement.executeUpdate(""" + INSERT INTO post ( + id, + title + ) + VALUES ( + nextval('post_sequence'), + 'High-Performance Hibernate' + ) + """); + } + }); + + assertEquals( + 6, + ((Number) entityManager.createNativeQuery(""" + SELECT COUNT(*) + FROM post + """ + ).getSingleResult()).intValue() + ); + List ids = entityManager.createNativeQuery("SELECT id FROM post").getResultList(); + for (Number id : ids) { + LOGGER.debug("Found id: {}", id); + } + for (int i = 0; i < 3; i++) { + entityManager.persist(newEntityInstance()); + entityManager.flush(); + } + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/AssignedIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/AssignedIdentifierTest.java new file mode 100644 index 000000000..7487b4926 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/AssignedIdentifierTest.java @@ -0,0 +1,82 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.assertEquals; + +public class AssignedIdentifierTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Book.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn(9789730228236L) + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea") + ); + }); + doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, 9789730228236L); + assertEquals("High-Performance Java Persistence", book.getTitle()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + private Long isbn; + + private String title; + + private String author; + + //Getters and setters omitted for brevity + + public Long getIsbn() { + return isbn; + } + + public Book setIsbn(Long isbn) { + this.isbn = isbn; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/AutoIdentifierMySQLTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/AutoIdentifierMySQLTest.java new file mode 100644 index 000000000..cbb81ad58 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/AutoIdentifierMySQLTest.java @@ -0,0 +1,66 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; + +public class AutoIdentifierMySQLTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Override + protected boolean nativeHibernateSessionFactoryBootstrap() { + return false; + } + + @Test + public void test() { + doInJPA(entityManager -> { + for (int i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setTitle( + String.format( + "High-Performance Java Persistence, Part %d", i + ) + ) + ); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/BaseClassIdentifierMySQLTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/BaseClassIdentifierMySQLTest.java new file mode 100644 index 000000000..fe32aecf0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/BaseClassIdentifierMySQLTest.java @@ -0,0 +1,232 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.*; + +public class BaseClassIdentifierMySQLTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + PostDetails.class, + Tag.class + }; + } + + @Override + protected boolean nativeHibernateSessionFactoryBootstrap() { + return false; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Tag jdbc = new Tag(); + jdbc.setName("JDBC"); + + entityManager.persist(jdbc); + + Tag hibernate = new Tag(); + hibernate.setName("Hibernate"); + + entityManager.persist(hibernate); + }); + + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + + PostDetails postDetails = new PostDetails(); + postDetails.setCreatedBy("Vlad Mihalcea"); + postDetails.setCreatedOn(new Date()); + post.addDetails(postDetails); + + Session session = entityManager.unwrap(Session.class); + + post.getTags().add(session.bySimpleNaturalId(Tag.class).getReference("jdbc")); + post.getTags().add(session.bySimpleNaturalId(Tag.class).getReference("hibernate")); + + entityManager.persist(post); + }); + } + + @MappedSuperclass + public static class BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Version + private Short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Short getVersion() { + return version; + } + + public void setVersion(Short version) { + this.version = version; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post extends BaseEntity { + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY + ) + private PostDetails details; + + @ManyToMany + @JoinTable( + name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private Set tags = new HashSet<>(); + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public PostDetails getDetails() { + return details; + } + + public Set getTags() { + return tags; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void addDetails(PostDetails details) { + this.details = details; + details.setPost(this); + } + + public void removeDetails() { + this.details.setPost(null); + this.details = null; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails extends BaseEntity { + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag extends BaseEntity { + + @NaturalId + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/EnhancedSequenceVsTableGeneratorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/EnhancedSequenceVsTableGeneratorTest.java similarity index 85% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/EnhancedSequenceVsTableGeneratorTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/EnhancedSequenceVsTableGeneratorTest.java index e124a0750..9652f7585 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/EnhancedSequenceVsTableGeneratorTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/EnhancedSequenceVsTableGeneratorTest.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; +package com.vladmihalcea.hpjp.hibernate.identifier; import java.util.Properties; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/EntityIdentifierCockroachDBTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/EntityIdentifierCockroachDBTest.java new file mode 100644 index 000000000..2784244ed --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/EntityIdentifierCockroachDBTest.java @@ -0,0 +1,110 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractCockroachDBIntegrationTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class EntityIdentifierCockroachDBTest extends AbstractCockroachDBIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Override + public void init() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + super.init(); + } + + @Test + public void test() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJPA(entityManager -> { + LocalDate startDate = LocalDate.of(2016, 11, 2); + for (int offset = 0; offset < 10; offset++) { + Post post = new Post(); + post.setTitle( + String.format( + "High-Performance Java Persistence, Review %d", + offset + ) + ); + post.setCreatedOn( + Date.from(startDate + .plusDays(offset) + .atStartOfDay(ZoneId.of("UTC")) + .toInstant() + ) + ); + entityManager.persist(post); + } + }); + + doInJPA(entityManager -> { + + List posts = entityManager.createQuery( + "select p " + + "from Post p " + + "order by p.createdOn", Post.class) + .setMaxResults(5) + .getResultList(); + + assertEquals(5, posts.size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue( + strategy = GenerationType.IDENTITY + ) + private Long id; + + @Column + @Temporal(TemporalType.DATE) + private Date createdOn; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/EntityIdentifierTimestampCockroachDBTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/EntityIdentifierTimestampCockroachDBTest.java new file mode 100644 index 000000000..6bc4985f7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/EntityIdentifierTimestampCockroachDBTest.java @@ -0,0 +1,109 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractCockroachDBIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.util.Date; + +import static org.junit.Assert.assertEquals; + +public class EntityIdentifierTimestampCockroachDBTest extends AbstractCockroachDBIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Override + protected boolean nativeHibernateSessionFactoryBootstrap() { + return false; + } + + @Override + public void init() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + super.init(); + } + + @Test + public void test() { + if (!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try (PreparedStatement preparedStatement = connection.prepareStatement( + "INSERT INTO post (title, createdOn) " + + "VALUES (?, ?)") + ) { + int index = 0; + preparedStatement.setString( + ++index, + "High-Performance Java Persistence" + ); + preparedStatement.setTimestamp( + ++index, + new Timestamp(System.currentTimeMillis()) + ); + int updateCount = preparedStatement.executeUpdate(); + + assertEquals(1, updateCount); + } + }); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(columnDefinition = "timestamptz") + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn; + + private String title; + + public Post() { + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/HiloIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/HiloIdentifierTest.java new file mode 100644 index 000000000..c3e9b4432 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/HiloIdentifierTest.java @@ -0,0 +1,77 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Parameter; +import org.junit.Test; + +public class HiloIdentifierTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testHiloIdentifierGenerator() { + doInJPA(entityManager -> { + for (int i = 0; i < 4; i++) { + Post post = new Post(); + post.setTitle( + String.format( + "High-Performance Java Persistence, Part %d", + i + 1 + ) + ); + + entityManager.persist(post); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_sequence") + @GenericGenerator( + name = "post_sequence", + strategy = "sequence", + parameters = { + @Parameter(name = "sequence_name", value = "post_sequence"), + @Parameter(name = "initial_value", value = "1"), + @Parameter(name = "increment_size", value = "3"), + @Parameter(name = "optimizer", value = "hilo") + } + ) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/HiloPooledDefaultSwitchTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/HiloPooledDefaultSwitchTest.java new file mode 100644 index 000000000..0b57e0fc6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/HiloPooledDefaultSwitchTest.java @@ -0,0 +1,44 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class HiloPooledDefaultSwitchTest { + + private HiloIdentifierTest hiloIdentifierTest = new HiloIdentifierTest(); + + private PooledSequenceIdentifierTest pooledIdentifierTest = new PooledSequenceIdentifierTest() { + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + }; + + @Test + public void testDefaultSwitch() { + try { + hiloIdentifierTest.init(); + hiloIdentifierTest.testHiloIdentifierGenerator(); + + try { + pooledIdentifierTest.init(); + + fail("Should have thrown MappingException"); + } catch (Exception e) { + assertEquals( + "The increment size of the [post_sequence] sequence is set to [3] in the entity mapping while the associated database sequence increment size is [1].", + ExceptionUtil.rootCause(e).getMessage() + ); + } + } finally { + hiloIdentifierTest.destroy(); + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/HiloPooledMigrationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/HiloPooledMigrationTest.java new file mode 100644 index 000000000..4b1f7f28f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/HiloPooledMigrationTest.java @@ -0,0 +1,57 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.Statement; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class HiloPooledMigrationTest { + + private HiloIdentifierTest hiloIdentifierTest = new HiloIdentifierTest(); + + private PooledSequenceIdentifierTest pooledIdentifierTest = new PooledSequenceIdentifierTest() { + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + }; + + @Test + public void testMigration() { + try { + DataSource dataSource = hiloIdentifierTest.database().dataSourceProvider().dataSource(); + + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + + statement.executeUpdate("DROP TABLE IF EXISTS post"); + statement.executeUpdate("DROP SEQUENCE IF EXISTS post_sequence"); + } catch (Exception e) { + fail(e.getMessage()); + } + hiloIdentifierTest.init(); + hiloIdentifierTest.testHiloIdentifierGenerator(); + + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + + statement.execute("SELECT setval('post_sequence', (SELECT MAX(id) FROM post) + 1)"); + statement.execute("ALTER SEQUENCE post_sequence INCREMENT BY 3"); + } catch (Exception e) { + fail(e.getMessage()); + } + + pooledIdentifierTest.init(); + pooledIdentifierTest.testPooledIdentifierGenerator(); + } finally { + hiloIdentifierTest.destroy(); + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/Identifiable.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/Identifiable.java similarity index 75% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/Identifiable.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/Identifiable.java index ed43a34cb..cff907762 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/Identifiable.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/Identifiable.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; +package com.vladmihalcea.hpjp.hibernate.identifier; import java.io.Serializable; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/LegacySequenceVsTableGeneratorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/LegacySequenceVsTableGeneratorTest.java similarity index 85% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/LegacySequenceVsTableGeneratorTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/LegacySequenceVsTableGeneratorTest.java index 48cae7ddc..d9b2a3d67 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/LegacySequenceVsTableGeneratorTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/LegacySequenceVsTableGeneratorTest.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; +package com.vladmihalcea.hpjp.hibernate.identifier; import java.util.Properties; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/MySQLIdentityIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/MySQLIdentityIdentifierTest.java new file mode 100644 index 000000000..af92256fd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/MySQLIdentityIdentifierTest.java @@ -0,0 +1,60 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.*; +import org.junit.Test; + +public class MySQLIdentityIdentifierTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + for (int i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setTitle( + String.format( + "High-Performance Java Persistence, Part %d", i + ) + ) + ); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/MySQLNativeIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/MySQLNativeIdentifierTest.java new file mode 100644 index 000000000..985e24af2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/MySQLNativeIdentifierTest.java @@ -0,0 +1,55 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.annotations.GenericGenerator; +import org.junit.Test; + +import jakarta.persistence.*; + +public class MySQLNativeIdentifierTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Override + protected boolean nativeHibernateSessionFactoryBootstrap() { + return false; + } + + @Test + public void test() { + doInJPA(entityManager -> { + for (int i = 1; i <= 3; i++) { + entityManager.persist( + new Post( + String.format( + "High-Performance Java Persistence, Part %d", i + ) + ) + ); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(generator="native") + @GenericGenerator(name = "native", strategy = "native") + private Long id; + + private String title; + + public Post() {} + + public Post(String title) { + this.title = title; + } + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/OracleRowIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/OracleRowIdTest.java new file mode 100644 index 000000000..19420aa6a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/OracleRowIdTest.java @@ -0,0 +1,163 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.annotations.RowId; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class OracleRowIdTest extends AbstractOracleIntegrationTest { + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + + PostComment comment1 = new PostComment(); + comment1.setReview("Great!"); + post.addComment(comment1); + + PostComment comment2 = new PostComment(); + comment2.setReview("To read"); + post.addComment(comment2); + + PostComment comment3 = new PostComment(); + comment3.setReview("Lorem Ipsum"); + post.addComment(comment3); + }); + + Post _post = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + }); + + List _comments = _post.getComments(); + + _post.getComments().get(0).setReview("Must read!"); + + doInJPA(entityManager -> { + entityManager.merge(_post); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @RowId( "ROWID" ) + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @RowId( "ROWID" ) + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostComment)) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PooledDefaultSequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PooledDefaultSequenceIdentifierTest.java new file mode 100644 index 000000000..38b65e57e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PooledDefaultSequenceIdentifierTest.java @@ -0,0 +1,56 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import org.junit.Test; + +import jakarta.persistence.*; + +public class PooledDefaultSequenceIdentifierTest extends AbstractPooledSequenceIdentifierTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + protected Object newEntityInstance() { + return new Post(); + } + + @Test + public void testOptimizer() { + insertSequences(); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pooled") + @SequenceGenerator( + name = "pooled", + sequenceName = "post_sequence", + allocationSize = 3 + ) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PooledLoSequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PooledLoSequenceIdentifierTest.java new file mode 100644 index 000000000..2c6894b31 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PooledLoSequenceIdentifierTest.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import jakarta.persistence.*; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Parameter; +import org.junit.Test; + +public class PooledLoSequenceIdentifierTest extends AbstractPooledSequenceIdentifierTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected Object newEntityInstance() { + return new Post(); + } + + @Test + public void testOptimizer() { + insertSequences(); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pooled-lo") + @GenericGenerator( + name = "pooled-lo", + strategy = "sequence", + parameters = { + @Parameter(name = "sequence_name", value = "post_sequence"), + @Parameter(name = "initial_value", value = "1"), + @Parameter(name = "increment_size", value = "3"), + @Parameter(name = "optimizer", value = "pooled-lo") + } + ) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PooledSequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PooledSequenceIdentifierTest.java new file mode 100644 index 000000000..58586c6c4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PooledSequenceIdentifierTest.java @@ -0,0 +1,73 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import org.junit.Test; + +import jakarta.persistence.*; + +public class PooledSequenceIdentifierTest extends AbstractPooledSequenceIdentifierTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + protected Object newEntityInstance() { + return new Post(); + } + + @Test + public void testOptimizer() { + insertSequences(); + } + + @Test + public void testPooledIdentifierGenerator() { + doInJPA(entityManager -> { + for (int i = 0; i < 4; i++) { + Post post = new Post(); + post.setTitle( + String.format( + "High-Performance Java Persistence, Part %d", + i + 1 + ) + ); + + entityManager.persist(post); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_sequence") + @SequenceGenerator( + name = "post_sequence", + sequenceName = "post_sequence", + allocationSize = 3 + ) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PostgreSQLSerialTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PostgreSQLSerialTest.java new file mode 100644 index 000000000..59d0e10f1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PostgreSQLSerialTest.java @@ -0,0 +1,95 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +public class PostgreSQLSerialTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + } + + @Test + public void testCurrentValue() { + doInJPA(entityManager -> { + Post post1 = new Post(); + post1.setTitle( + "High-Performance Java Persistence, Part 1" + ); + + entityManager.persist(post1); + + Post post2 = new Post(); + post2.setTitle( + "High-Performance Java Persistence, Part 2" + ); + + entityManager.persist(post2); + + entityManager.flush(); + assertEquals( + 2, + ( + (Number) entityManager + .createNativeQuery( + "select currval('post_id_seq')") + .getSingleResult() + ).intValue() + ); + }); + } + + @Test + public void testBatch() { + doInJPA(entityManager -> { + for (int i = 0; i < 3; i++) { + Post post = new Post(); + post.setTitle( + String.format("High-Performance Java Persistence, Part %d", i + 1) + ); + + entityManager.persist(post); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + private String title; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PreferredPooledLoSequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PreferredPooledLoSequenceIdentifierTest.java new file mode 100644 index 000000000..1e6d0f017 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/PreferredPooledLoSequenceIdentifierTest.java @@ -0,0 +1,64 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Properties; + +public class PreferredPooledLoSequenceIdentifierTest extends AbstractPooledSequenceIdentifierTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.PREFERRED_POOLED_OPTIMIZER, "pooled-lo"); + } + + @Override + protected Object newEntityInstance() { + return new Post(); + } + + @Test + public void testOptimizer() { + insertSequences(); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_sequence") + @SequenceGenerator( + name = "post_sequence", + sequenceName = "post_sequence", + allocationSize = 3 + ) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/SQLServerScopeIdentity.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/SQLServerScopeIdentity.java similarity index 85% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/SQLServerScopeIdentity.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/SQLServerScopeIdentity.java index 61e1119b2..b377ddd28 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/SQLServerScopeIdentity.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/SQLServerScopeIdentity.java @@ -1,13 +1,13 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; +package com.vladmihalcea.hpjp.hibernate.identifier; -import com.vladmihalcea.book.hpjp.util.AbstractSQLServerIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; import org.hibernate.Session; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.concurrent.atomic.AtomicLong; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/SequenceVsTableGeneratorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/SequenceVsTableGeneratorTest.java similarity index 76% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/SequenceVsTableGeneratorTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/SequenceVsTableGeneratorTest.java index e94190630..4481ba21e 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/SequenceVsTableGeneratorTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/SequenceVsTableGeneratorTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier; +package com.vladmihalcea.hpjp.hibernate.identifier; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Properties; public class SequenceVsTableGeneratorTest extends AbstractTest { @@ -48,8 +48,8 @@ public void testTableSequenceIdentifierGenerator() { public static class SequenceIdentifier { @Id - @GeneratedValue(generator = "sequence", strategy=GenerationType.SEQUENCE) - @SequenceGenerator(name = "sequence", allocationSize = 10) + @GeneratedValue(generator = "hib_sequence", strategy=GenerationType.SEQUENCE) + @SequenceGenerator(name = "hib_sequence", allocationSize = 10) private Long id; } @@ -57,8 +57,8 @@ public static class SequenceIdentifier { public static class TableSequenceIdentifier { @Id - @GeneratedValue(generator = "table", strategy=GenerationType.TABLE) - @TableGenerator(name = "table", allocationSize = 10) + @GeneratedValue(generator = "hib_table", strategy=GenerationType.TABLE) + @TableGenerator(name = "hib_table", allocationSize = 10) private Long id; } } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/SimpleSequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/SimpleSequenceIdentifierTest.java new file mode 100644 index 000000000..dd7c33943 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/SimpleSequenceIdentifierTest.java @@ -0,0 +1,90 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; + +public class SimpleSequenceIdentifierTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + Tag.class + }; + } + + @Test + public void testSequenceIdentifierGenerator() { + doInJPA(entityManager -> { + for (int i = 0; i < 4; i++) { + Post post = new Post(); + post.setTitle( + String.format( + "High-Performance Java Persistence, Part %d", + i + 1 + ) + ); + + entityManager.persist(post); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue( + strategy = GenerationType.SEQUENCE, + generator = "post_sequence" + ) + @SequenceGenerator( + name = "post_sequence", + sequenceName = "post_sequence", + allocationSize = 1 + ) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @NaturalId + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/SimpleTableIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/SimpleTableIdentifierTest.java new file mode 100644 index 000000000..fabd74c37 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/SimpleTableIdentifierTest.java @@ -0,0 +1,59 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; + +public class SimpleTableIdentifierTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Test + public void testSequenceIdentifierGenerator() { + doInJPA(entityManager -> { + for (int i = 0; i < 3; i++) { + Post post = new Post(); + post.setTitle( + String.format("High-Performance Java Persistence, Part %d", i + 1) + ); + entityManager.persist(post); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue( + strategy = GenerationType.TABLE + ) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/StringSequenceIdentifier.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/StringSequenceIdentifier.java new file mode 100644 index 000000000..3d2806df8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/StringSequenceIdentifier.java @@ -0,0 +1,81 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import org.hibernate.MappingException; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.config.spi.ConfigurationService; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.Configurable; +import org.hibernate.id.enhanced.SequenceStyleGenerator; +import org.hibernate.internal.util.config.ConfigurationHelper; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.Type; + +import java.io.Serializable; +import java.util.Properties; + + +/** + * @author Vlad Mihalcea + */ +public class StringSequenceIdentifier extends SequenceStyleGenerator implements Configurable { + + public static final String SEQUENCE_PREFIX = "sequence_prefix"; + + private String sequencePrefix; + + private String sequenceCallSyntax; + + @Override + public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) + throws MappingException { + super.configure(type, params, serviceRegistry); + + final JdbcEnvironment jdbcEnvironment = serviceRegistry.getService( + JdbcEnvironment.class + ); + + final Dialect dialect = jdbcEnvironment.getDialect(); + + final ConfigurationService configurationService = serviceRegistry.getService( + ConfigurationService.class + ); + + String globalEntityIdentifierPrefix = configurationService.getSetting( + "entity.identifier.prefix", + String.class, + "SEQ_" + ); + + sequencePrefix = ConfigurationHelper.getString( + SEQUENCE_PREFIX, + params, + globalEntityIdentifierPrefix + ); + + final String sequencePerEntitySuffix = ConfigurationHelper.getString( + SequenceStyleGenerator.CONFIG_SEQUENCE_PER_ENTITY_SUFFIX, + params, + SequenceStyleGenerator.DEF_SEQUENCE_SUFFIX + ); + + final String defaultSequenceName = params.getProperty(JPA_ENTITY_NAME) + sequencePerEntitySuffix; + + sequenceCallSyntax = dialect.getSequenceSupport().getSequenceNextValString( + ConfigurationHelper.getString( + SequenceStyleGenerator.SEQUENCE_PARAM, + params, + defaultSequenceName + ) + ); + } + + @Override + public Serializable generate(SharedSessionContractImplementor session, Object obj) { + long seqValue = ((Number) + session.createNativeQuery(sequenceCallSyntax).uniqueResult() + ).longValue(); + + return sequencePrefix + String.format("%011d%s", 0, seqValue); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/StringSequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/StringSequenceIdentifierTest.java new file mode 100644 index 000000000..4e864a7f8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/StringSequenceIdentifierTest.java @@ -0,0 +1,96 @@ +package com.vladmihalcea.hpjp.hibernate.identifier; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.annotations.GenericGenerator; +import org.junit.Test; + +import java.util.Properties; + +public class StringSequenceIdentifierTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Board.class, + Event.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty("entity.identifier.prefix", "ID_"); + } + + @Test + public void test() { + executeStatement("DROP SEQUENCE IF EXISTS hibernate_sequence"); + executeStatement("CREATE SEQUENCE hibernate_sequence START 1 INCREMENT 1"); + + LOGGER.debug("test"); + doInJPA(entityManager -> { + entityManager.persist(new Post()); + entityManager.persist(new Post()); + entityManager.persist(new Post()); + }); + doInJPA(entityManager -> { + entityManager.persist(new Board()); + entityManager.persist(new Board()); + entityManager.persist(new Board()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post implements Identifiable { + + @Id + @GenericGenerator( + name = "assigned-sequence", + strategy = "com.vladmihalcea.hpjp.hibernate.identifier.StringSequenceIdentifier", + parameters = { + @org.hibernate.annotations.Parameter( + name = "sequence_name", value = "hibernate_sequence"), + @org.hibernate.annotations.Parameter( + name = "sequence_prefix", value = "CTC_"), + @org.hibernate.annotations.Parameter( + name = "increment_size", value = "1"), + } + ) + @GeneratedValue( + generator = "assigned-sequence" + ) + private String id; + + @Override + public String getId() { + return id; + } + } + + @Entity(name = "Board") + public static class Board { + + @Id + @GenericGenerator( + name = "assigned-sequence", + strategy = "com.vladmihalcea.hpjp.hibernate.identifier.StringSequenceIdentifier", + parameters = { + @org.hibernate.annotations.Parameter( + name = "sequence_name", value = "hibernate_sequence"), + @org.hibernate.annotations.Parameter( + name = "increment_size", value = "1"), + } + ) + @GeneratedValue(generator = "assigned-sequence", strategy = GenerationType.SEQUENCE) + private String id; + } + + @Entity(name = "Event") + public static class Event { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private String id; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/access/EmbeddableAccessStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/access/EmbeddableAccessStrategyTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/access/EmbeddableAccessStrategyTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/access/EmbeddableAccessStrategyTest.java index 0b73e30e7..6e44e6932 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/access/EmbeddableAccessStrategyTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/access/EmbeddableAccessStrategyTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.access; +package com.vladmihalcea.hpjp.hibernate.identifier.access; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import static org.junit.Assert.assertEquals; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/access/EmbeddableCollectionAccessStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/access/EmbeddableCollectionAccessStrategyTest.java similarity index 93% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/access/EmbeddableCollectionAccessStrategyTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/access/EmbeddableCollectionAccessStrategyTest.java index 49afcc3e4..94ca5ffc7 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/access/EmbeddableCollectionAccessStrategyTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/access/EmbeddableCollectionAccessStrategyTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.access; +package com.vladmihalcea.hpjp.hibernate.identifier.access; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/access/OverrideAccessStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/access/OverrideAccessStrategyTest.java similarity index 86% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/access/OverrideAccessStrategyTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/access/OverrideAccessStrategyTest.java index 1df4d1cd9..7c4b57de2 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/access/OverrideAccessStrategyTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/access/OverrideAccessStrategyTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.access; +package com.vladmihalcea.hpjp.hibernate.identifier.access; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea @@ -39,7 +39,7 @@ public static class FieldEntity { @Version @Access(AccessType.FIELD) - private Long version; + private Short version; @Id public Integer getId() { diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/AbstractBatchIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/AbstractBatchIdentifierTest.java new file mode 100644 index 000000000..e656dd7de --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/AbstractBatchIdentifierTest.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch; + +import com.vladmihalcea.hpjp.util.AbstractTest; + +import java.util.Properties; + +public abstract class AbstractBatchIdentifierTest extends AbstractTest { + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_size", "2"); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/AssignedTableBatchIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/AssignedTableBatchIdentifierTest.java similarity index 83% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/AssignedTableBatchIdentifierTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/AssignedTableBatchIdentifierTest.java index 0b4215905..ab97671d7 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/AssignedTableBatchIdentifierTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/AssignedTableBatchIdentifierTest.java @@ -1,10 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch; +package com.vladmihalcea.hpjp.hibernate.identifier.batch; -import com.vladmihalcea.book.hpjp.hibernate.identifier.Identifiable; +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; import org.hibernate.annotations.GenericGenerator; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; public class AssignedTableBatchIdentifierTest extends AbstractBatchIdentifierTest { @@ -33,7 +33,7 @@ public void testIdentityIdentifierGenerator() { public static class Post implements Identifiable { @Id - @GenericGenerator(name = "table", strategy = "com.vladmihalcea.book.hpjp.hibernate.identifier.batch.AssignedTableGenerator", + @GenericGenerator(name = "table", strategy = "com.vladmihalcea.hpjp.hibernate.identifier.batch.AssignedTableGenerator", parameters = { @org.hibernate.annotations.Parameter(name = "table_name", value = "sequence_table") }) diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/AssignedTableGenerator.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/AssignedTableGenerator.java new file mode 100644 index 000000000..a027d760d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/AssignedTableGenerator.java @@ -0,0 +1,27 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch; + +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.enhanced.TableGenerator; + +import java.io.Serializable; + +/** + * AssignedTableGenerator - Assigned TableGenerator + * + * @author Vlad Mihalcea + */ +public class AssignedTableGenerator extends TableGenerator { + + @Override + public Object generate(SharedSessionContractImplementor session, Object obj) { + if(obj instanceof Identifiable) { + Identifiable identifiable = (Identifiable) obj; + Serializable id = identifiable.getId(); + if(id != null) { + return id; + } + } + return super.generate(session, obj); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/AutoIdentifierWithSequenceGeneratorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/AutoIdentifierWithSequenceGeneratorTest.java new file mode 100644 index 000000000..6d0539616 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/AutoIdentifierWithSequenceGeneratorTest.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; + +public class AutoIdentifierWithSequenceGeneratorTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Test + public void test() { + int batchSize = 10; + doInJPA(entityManager -> { + for (int i = 0; i < batchSize; i++) { + entityManager.persist(new Post()); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "custom_sequence") + @SequenceGenerator(name = "custom_sequence", initialValue = 10) + private Long id; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/BatchSequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/BatchSequenceIdentifierTest.java new file mode 100644 index 000000000..2a45a8bcd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/BatchSequenceIdentifierTest.java @@ -0,0 +1,90 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch; + +import com.vladmihalcea.hpjp.util.providers.Database; +import io.hypersistence.utils.hibernate.id.BatchSequence; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Parameter; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Properties; + +public class BatchSequenceIdentifierTest extends AbstractBatchIdentifierTest { + + private static final int POST_SIZE = 10; + private static final int BATCH_SIZE = 5; + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + } + + @Test + public void testSequenceIdentifierGenerator() { + executeStatement("DROP SEQUENCE IF EXISTS post_sequence"); + executeStatement(""" + CREATE SEQUENCE post_sequence + INCREMENT BY 1 + START WITH 1 + CACHE 5 + """); + + doInJPA(entityManager -> { + for (int i = 1; i <= POST_SIZE; i++) { + entityManager.persist( + new Post() + .setTitle( + String.format( + "High-Performance Java Persistence, Chapter %d", + i + ) + ) + ); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @BatchSequence( + name = "post_sequence", + fetchSize = 5 + ) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/IdentityIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/IdentityIdentifierTest.java new file mode 100644 index 000000000..66d2d7979 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/IdentityIdentifierTest.java @@ -0,0 +1,81 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch; + +import org.junit.Test; + +import jakarta.persistence.*; + +public class IdentityIdentifierTest extends AbstractBatchIdentifierTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Test + public void testIdentityIdentifierGenerator() { + doInJPA(entityManager -> { + for (int i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setTitle( + String.format("High-Performance Java Persistence, Part %d", i) + ) + ); + } + }); + } + + @Test + public void testIdentityIdentifierGeneratorOutsideTransaction() { + LOGGER.debug("testIdentityIdentifierGeneratorOutsideTransaction"); + EntityManager entityManager = null; + EntityTransaction txn = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + for (int i = 0; i < 5; i++) { + entityManager.persist(new Post()); + } + txn = entityManager.getTransaction(); + txn.begin(); + txn.commit(); + } catch (RuntimeException e) { + if ( txn != null && txn.isActive()) txn.rollback(); + throw e; + } finally { + if (entityManager != null) { + entityManager.close(); + } + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/MariaDBIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/MariaDBIdentifierTest.java new file mode 100644 index 000000000..8ae9ad14f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/MariaDBIdentifierTest.java @@ -0,0 +1,74 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch; + +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.Properties; + +public class MariaDBIdentifierTest extends AbstractBatchIdentifierTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Override + protected Database database() { + return Database.MARIADB; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.jdbc.batch_size", "10"); + return properties; + } + + @Test + @Ignore + public void testSequenceIdentifierGenerator() { + doInJPA(entityManager -> { + for (int i = 0; i < 3; i++) { + Post post = new Post(); + post.setTitle( + String.format("High-Performance Java Persistence, Part %d", i + 1) + ); + entityManager.persist(post); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue( + strategy = GenerationType.SEQUENCE + ) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/PooledLoGenericGeneratorIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/PooledLoGenericGeneratorIdentifierTest.java new file mode 100644 index 000000000..be47c89aa --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/PooledLoGenericGeneratorIdentifierTest.java @@ -0,0 +1,74 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch; + +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; + +public class PooledLoGenericGeneratorIdentifierTest extends AbstractBatchIdentifierTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testSequenceIdentifierGenerator() { + doInJPA(entityManager -> { + for (int i = 1; i <= 5; i++) { + entityManager.persist( + new Post().setTitle( + String.format( + "High-Performance Java Persistence, Part %d", + i + ) + ) + ); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue( + strategy = GenerationType.SEQUENCE, + generator = "seq_post" + ) + @SequenceGenerator( + name = "seq_post", + allocationSize = 5 + ) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/PostgresTableGeneratorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/PostgresTableGeneratorTest.java similarity index 86% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/PostgresTableGeneratorTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/PostgresTableGeneratorTest.java index 81eeb072f..b8be1e9b5 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/PostgresTableGeneratorTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/PostgresTableGeneratorTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch; +package com.vladmihalcea.hpjp.hibernate.identifier.batch; -import com.vladmihalcea.book.hpjp.hibernate.identifier.Identifiable; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.hibernate.annotations.GenericGenerator; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; public class PostgresTableGeneratorTest extends AbstractPostgreSQLIntegrationTest { diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/SequenceAllocationSizeIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/SequenceAllocationSizeIdentifierTest.java similarity index 93% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/SequenceAllocationSizeIdentifierTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/SequenceAllocationSizeIdentifierTest.java index de6c929f5..8bc82d9af 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/SequenceAllocationSizeIdentifierTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/SequenceAllocationSizeIdentifierTest.java @@ -1,10 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch; +package com.vladmihalcea.hpjp.hibernate.identifier.batch; import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.Parameter; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; public class SequenceAllocationSizeIdentifierTest extends AbstractBatchIdentifierTest { diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/SequenceGeneratorIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/SequenceGeneratorIdentifierTest.java new file mode 100644 index 000000000..a62ec0b84 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/SequenceGeneratorIdentifierTest.java @@ -0,0 +1,74 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch; + +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; + +public class SequenceGeneratorIdentifierTest extends AbstractBatchIdentifierTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testSequenceIdentifierGenerator() { + doInJPA(entityManager -> { + for (int i = 1; i <= 5; i++) { + entityManager.persist( + new Post().setTitle( + String.format( + "High-Performance Java Persistence, Part %d", + i + ) + ) + ); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue( + strategy = GenerationType.SEQUENCE, + generator = "seq_post" + ) + @SequenceGenerator( + name = "seq_post", + allocationSize = 5 + ) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/SequenceIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/SequenceIdentifierTest.java new file mode 100644 index 000000000..759bea3aa --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/SequenceIdentifierTest.java @@ -0,0 +1,66 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch; + +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; + +public class SequenceIdentifierTest extends AbstractBatchIdentifierTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testSequenceIdentifierGenerator() { + doInJPA(entityManager -> { + for (int i = 1; i <= 5; i++) { + entityManager.persist( + new Post().setTitle( + String.format( + "High-Performance Java Persistence, Part %d", + i + ) + ) + ); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/TableAllocationSizeIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/TableAllocationSizeIdentifierTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/TableAllocationSizeIdentifierTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/TableAllocationSizeIdentifierTest.java index bbc9dff25..7be534ecf 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/TableAllocationSizeIdentifierTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/TableAllocationSizeIdentifierTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch; +package com.vladmihalcea.hpjp.hibernate.identifier.batch; import org.hibernate.annotations.GenericGenerator; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; public class TableAllocationSizeIdentifierTest extends AbstractBatchIdentifierTest { diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/TableIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/TableIdentifierTest.java new file mode 100644 index 000000000..5c3d3032a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/TableIdentifierTest.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch; + +import org.junit.Test; + +import jakarta.persistence.*; + +public class TableIdentifierTest extends AbstractBatchIdentifierTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Test + public void testTableIdentifierGenerator() { + LOGGER.debug("testTableIdentifierGenerator"); + doInJPA(entityManager -> { + for (int i = 1; i <= 5; i++) { + entityManager.persist( + new Post().setTitle( + String.format( + "High-Performance Java Persistence, Part %d", + i + ) + ) + ); + } + LOGGER.debug("Flush is triggered at commit-time"); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy=GenerationType.TABLE) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/ConcurrentBatchIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/ConcurrentBatchIdentifierTest.java similarity index 88% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/ConcurrentBatchIdentifierTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/ConcurrentBatchIdentifierTest.java index 5ed3c471d..d5eedbf40 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/ConcurrentBatchIdentifierTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/ConcurrentBatchIdentifierTest.java @@ -1,17 +1,18 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch.concurrent; +package com.vladmihalcea.hpjp.hibernate.identifier.batch.concurrent; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.Timer; -import com.vladmihalcea.book.hpjp.hibernate.identifier.batch.concurrent.providers.IdentityPostEntityProvider; -import com.vladmihalcea.book.hpjp.hibernate.identifier.batch.concurrent.providers.PostEntityProvider; -import com.vladmihalcea.book.hpjp.hibernate.identifier.batch.concurrent.providers.SequencePostEntityProvider; -import com.vladmihalcea.book.hpjp.hibernate.identifier.batch.concurrent.providers.TablePostEntityProvider; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; - +import com.vladmihalcea.hpjp.hibernate.identifier.batch.concurrent.providers.IdentityPostEntityProvider; +import com.vladmihalcea.hpjp.hibernate.identifier.batch.concurrent.providers.PostEntityProvider; +import com.vladmihalcea.hpjp.hibernate.identifier.batch.concurrent.providers.SequencePostEntityProvider; +import com.vladmihalcea.hpjp.hibernate.identifier.batch.concurrent.providers.TablePostEntityProvider; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.PostgreSQLDataSourceProvider; + +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -90,6 +91,7 @@ protected DataSourceProvider dataSourceProvider() { } @Test + @Ignore public void testIdentifierGenerator() throws InterruptedException, ExecutionException { LOGGER.debug("testIdentifierGenerator, database: {}, entityProvider: {}, threadCount: {}", dataSourceProvider.database(), entityProvider.getClass().getSimpleName(), threadCount); //warming-up diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/providers/IdentityPostEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/providers/IdentityPostEntityProvider.java similarity index 81% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/providers/IdentityPostEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/providers/IdentityPostEntityProvider.java index 86b49eddf..b83add387 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/providers/IdentityPostEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/providers/IdentityPostEntityProvider.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch.concurrent.providers; +package com.vladmihalcea.hpjp.hibernate.identifier.batch.concurrent.providers; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/providers/PostEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/providers/PostEntityProvider.java new file mode 100644 index 000000000..0f8bcf9fe --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/providers/PostEntityProvider.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch.concurrent.providers; + +import com.vladmihalcea.hpjp.util.EntityProvider; + +/** + * @author Vlad Mihalcea + */ +public abstract class PostEntityProvider implements EntityProvider { + + private final Class clazz; + + protected PostEntityProvider(Class clazz) { + this.clazz = clazz; + } + + public abstract T newPost(); + + @Override + public Class[] entities() { + return new Class[] { + clazz + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/providers/SequencePostEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/providers/SequencePostEntityProvider.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/providers/SequencePostEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/providers/SequencePostEntityProvider.java index c17eaa319..ed11a99a3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/providers/SequencePostEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/providers/SequencePostEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch.concurrent.providers; +package com.vladmihalcea.hpjp.hibernate.identifier.batch.concurrent.providers; import org.hibernate.annotations.GenericGenerator; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/providers/TablePostEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/providers/TablePostEntityProvider.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/providers/TablePostEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/providers/TablePostEntityProvider.java index 6c7ac6bb1..be3eab0da 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/batch/concurrent/providers/TablePostEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/concurrent/providers/TablePostEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.batch.concurrent.providers; +package com.vladmihalcea.hpjp.hibernate.identifier.batch.concurrent.providers; import org.hibernate.annotations.GenericGenerator; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/jta/JTATableIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/jta/JTATableIdentifierTest.java new file mode 100644 index 000000000..4252f2a43 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/jta/JTATableIdentifierTest.java @@ -0,0 +1,41 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch.jta; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = JTATableIdentifierTestConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public class JTATableIdentifierTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private TransactionTemplate transactionTemplate; + + @Test + public void testTableIdentifierGenerator() { + LOGGER.debug("testIdentityIdentifierGenerator"); + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + for (int i = 0; i < 5; i++) { + entityManager.persist(new Post()); + } + LOGGER.debug("Flush is triggered at commit-time"); + return null; + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/jta/JTATableIdentifierTestConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/jta/JTATableIdentifierTestConfiguration.java new file mode 100644 index 000000000..5ba9699d6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/jta/JTATableIdentifierTestConfiguration.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch.jta; + +import com.vladmihalcea.hpjp.util.spring.config.jta.PostgreSQLJTATransactionManagerConfiguration; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JTATableIdentifierTestConfiguration extends PostgreSQLJTATransactionManagerConfiguration { + + @Override + protected Class configurationClass() { + return Post.class; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/jta/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/jta/Post.java new file mode 100644 index 000000000..26511b0e0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/batch/jta/Post.java @@ -0,0 +1,20 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.batch.jta; + +import org.hibernate.annotations.GenericGenerator; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + @GenericGenerator(name = "table", strategy = "enhanced-table", parameters = { + @org.hibernate.annotations.Parameter(name = "table_name", value = "sequence_table") + }) + @GeneratedValue(generator = "table", strategy = GenerationType.TABLE) + private Long id; +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdGeneratedIdClassTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdGeneratedIdClassTest.java new file mode 100644 index 000000000..7bcd385d7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdGeneratedIdClassTest.java @@ -0,0 +1,142 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.composite; + +import java.io.Serializable; +import java.util.Objects; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; + +import org.junit.Test; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class CompositeIdGeneratedIdClassTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Book.class + }; + } + + @Test + public void test() { + LOGGER.debug("test"); + + Book _book = doInJPA(entityManager -> { + Book book = new Book(); + book.setPublisherId( 1 ); + book.setTitle( "High-Performance Java Persistence"); + + entityManager.persist(book); + + return book; + }); + + assertNotNull(_book.getRegistrationNumber()); + + doInJPA(entityManager -> { + PK key = new PK( _book.getRegistrationNumber(), 1); + + Book book = entityManager.find(Book.class, key); + assertEquals( "High-Performance Java Persistence", book.getTitle() ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + @IdClass( PK.class ) + public static class Book { + + @Id + @Column(name = "registration_number") + @GeneratedValue + private Long registrationNumber; + + @Id + @Column(name = "publisher_id") + private Integer publisherId; + + private String title; + + public Long getRegistrationNumber() { + return registrationNumber; + } + + public void setRegistrationNumber(Long registrationNumber) { + this.registrationNumber = registrationNumber; + } + + public int getPublisherId() { + return publisherId; + } + + public void setPublisherId(int publisherId) { + this.publisherId = publisherId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + public static class PK implements Serializable { + + private Long registrationNumber; + + private Integer publisherId; + + public PK(Long registrationNumber, Integer publisherId) { + this.registrationNumber = registrationNumber; + this.publisherId = publisherId; + } + + private PK() { + } + + public Long getRegistrationNumber() { + return registrationNumber; + } + + public void setRegistrationNumber(Long registrationNumber) { + this.registrationNumber = registrationNumber; + } + + public Integer getPublisherId() { + return publisherId; + } + + public void setPublisherId(Integer publisherId) { + this.publisherId = publisherId; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + PK pk = (PK) o; + return Objects.equals( registrationNumber, pk.registrationNumber ) && + Objects.equals( publisherId, pk.publisherId ); + } + + @Override + public int hashCode() { + return Objects.hash( registrationNumber, publisherId ); + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdIdentityGeneratedTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdIdentityGeneratedTest.java new file mode 100644 index 000000000..c5df8b3c3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdIdentityGeneratedTest.java @@ -0,0 +1,157 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.composite; + +import java.io.Serializable; +import java.sql.Statement; +import java.util.Objects; +import java.util.Properties; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +import org.hibernate.Session; +import org.hibernate.annotations.SQLInsert; + +import org.junit.Test; + +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; + +import static org.junit.Assert.assertEquals; + +public class CompositeIdIdentityGeneratedTest extends AbstractSQLServerIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Book.class + }; + } + + protected void additionalProperties(Properties properties) { + properties.put("hibernate.hbm2ddl.auto", "none"); + } + + @Test + public void test() { + LOGGER.debug("test"); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap( Session.class ); + + session.doWork( connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate( "drop table book" ); + } + catch (Exception ignore) { + } + + try (Statement statement = connection.createStatement()) { + statement.executeUpdate( + "create table book (publisher_id int not null, registration_number bigint IDENTITY not null, title varchar(255), version int, primary key (publisher_id, registration_number))" ); + } + catch (Exception ignore) { + } + } ); + }); + + doInJPA(entityManager -> { + Book book = new Book(); + book.setTitle( "High-Performance Java Persistence"); + + EmbeddedKey key = new EmbeddedKey(); + key.setPublisherId(1); + book.setKey(key); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + EmbeddedKey key = new EmbeddedKey(); + + key.setPublisherId(1); + key.setRegistrationNumber(1L); + + Book book = entityManager.find(Book.class, key); + assertEquals( "High-Performance Java Persistence", book.getTitle() ); + }); + + } + + @Entity(name = "Book") + @Table(name = "book") + @SQLInsert( sql = "insert into book (title, publisher_id, version) values (?, ?, ?)") + public static class Book implements Serializable { + + @EmbeddedId + private EmbeddedKey key; + + private String title; + + @Version + @Column(insertable = false) + private Short version; + + public EmbeddedKey getKey() { + return key; + } + + public void setKey(EmbeddedKey key) { + this.key = key; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Embeddable + public static class EmbeddedKey implements Serializable { + + @Column(name = "registration_number") + private Long registrationNumber; + + @Column(name = "publisher_id") + private Integer publisherId; + + public Long getRegistrationNumber() { + return registrationNumber; + } + + public void setRegistrationNumber(Long registrationNumber) { + this.registrationNumber = registrationNumber; + } + + public int getPublisherId() { + return publisherId; + } + + public void setPublisherId(int publisherId) { + this.publisherId = publisherId; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + EmbeddedKey that = (EmbeddedKey) o; + return Objects.equals( registrationNumber, that.registrationNumber ) && + Objects.equals( publisherId, that.publisherId ); + } + + @Override + public int hashCode() { + return Objects.hash( registrationNumber, publisherId ); + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/composite/CompositeIdManyToOneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdManyToOneTest.java similarity index 88% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/composite/CompositeIdManyToOneTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdManyToOneTest.java index 25c63bf24..a48ec6672 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/composite/CompositeIdManyToOneTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdManyToOneTest.java @@ -1,10 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.composite; +package com.vladmihalcea.hpjp.hibernate.identifier.composite; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serializable; +import java.util.List; import java.util.Objects; import static org.junit.Assert.assertEquals; @@ -36,6 +37,7 @@ public void test() { employee.setName("Vlad Mihalcea"); entityManager.persist(employee); }); + doInJPA(entityManager -> { Employee employee = entityManager.find(Employee.class, new EmployeeId(1L, 100L)); Phone phone = new Phone(); @@ -43,12 +45,24 @@ public void test() { phone.setNumber("012-345-6789"); entityManager.persist(phone); }); + doInJPA(entityManager -> { Phone phone = entityManager.find(Phone.class, "012-345-6789"); assertNotNull(phone); assertEquals(new EmployeeId(1L, 100L), phone.getEmployee().getId()); }); + doInJPA(entityManager -> { + List employees = entityManager + .createQuery( + "select e " + + "from Employee e " + + "where e.id.companyId = :companyId") + .setParameter("companyId", 1L) + .getResultList(); + + assertEquals(new EmployeeId(1L, 100L), employees.get(0).getId()); + }); } @Entity(name = "Company") diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/composite/CompositeIdManyToOneWithCompanyInIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdManyToOneWithCompanyInIdTest.java similarity index 97% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/composite/CompositeIdManyToOneWithCompanyInIdTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdManyToOneWithCompanyInIdTest.java index 6cf8c9d27..d76e3bfe8 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/composite/CompositeIdManyToOneWithCompanyInIdTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdManyToOneWithCompanyInIdTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.composite; +package com.vladmihalcea.hpjp.hibernate.identifier.composite; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serializable; import java.util.Objects; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/composite/CompositeIdOneToOneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdOneToOneTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/composite/CompositeIdOneToOneTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdOneToOneTest.java index 07166f7cb..11c8c6ab2 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/composite/CompositeIdOneToOneTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/composite/CompositeIdOneToOneTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.composite; +package com.vladmihalcea.hpjp.hibernate.identifier.composite; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serializable; import java.util.Objects; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/global/MySQLIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/global/MySQLIdentifierTest.java new file mode 100644 index 000000000..389d4ba12 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/global/MySQLIdentifierTest.java @@ -0,0 +1,48 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.global; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import java.util.Properties; + +public class MySQLIdentifierTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Override + protected String[] resources() { + return new String[] { + "mappings/identifier/global/mysql-orm.xml" + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + for (int i = 0; i < 5; i++) { + Post post = new Post(); + post.setTitle(String.format("Post nr %d", i + 1)); + entityManager.persist(post); + } + }); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/global/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/global/Post.java new file mode 100644 index 000000000..bd37c4f39 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/global/Post.java @@ -0,0 +1,34 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.global; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + @GeneratedValue(generator = "post_sequence", strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "post_sequence", allocationSize = 10) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/global/PostgreSQLIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/global/PostgreSQLIdentifierTest.java new file mode 100644 index 000000000..856c3e611 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/global/PostgreSQLIdentifierTest.java @@ -0,0 +1,41 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.global; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import java.util.Properties; + +public class PostgreSQLIdentifierTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + for (int i = 0; i < 5; i++) { + Post post = new Post(); + post.setTitle(String.format("Post nr %d", i + 1)); + entityManager.persist(post); + } + }); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/globalsequence/GlobalIdentifierGeneratorScopeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/globalsequence/GlobalIdentifierGeneratorScopeTest.java similarity index 83% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/globalsequence/GlobalIdentifierGeneratorScopeTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/globalsequence/GlobalIdentifierGeneratorScopeTest.java index 445ea4d99..051ba94fd 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/globalsequence/GlobalIdentifierGeneratorScopeTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/globalsequence/GlobalIdentifierGeneratorScopeTest.java @@ -1,12 +1,12 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.globalsequence; +package com.vladmihalcea.hpjp.hibernate.identifier.globalsequence; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; import org.junit.Test; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; public class GlobalIdentifierGeneratorScopeTest extends AbstractPostgreSQLIntegrationTest { diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/globalsequence/package-info.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/globalsequence/package-info.java new file mode 100644 index 000000000..66e0e9454 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/globalsequence/package-info.java @@ -0,0 +1,14 @@ +@GenericGenerator( + name = "pooled", + strategy = "sequence", + parameters = { + @Parameter(name = "sequence_name", value = "sequence"), + @Parameter(name = "initial_value", value = "1"), + @Parameter(name = "increment_size", value = "5"), + @Parameter(name = "optimizer", value = "pooled-lo"), + } +) +package com.vladmihalcea.hpjp.hibernate.identifier.globalsequence; + +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Parameter; \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/SequenceOptimizerIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/SequenceOptimizerIdentifierTest.java similarity index 91% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/SequenceOptimizerIdentifierTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/SequenceOptimizerIdentifierTest.java index 5ee7f4a9b..514c5e7b3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/SequenceOptimizerIdentifierTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/SequenceOptimizerIdentifierTest.java @@ -1,14 +1,15 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.optimizer; +package com.vladmihalcea.hpjp.hibernate.identifier.optimizer; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.Timer; -import com.vladmihalcea.book.hpjp.hibernate.identifier.optimizer.providers.*; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; +import com.vladmihalcea.hpjp.hibernate.identifier.optimizer.providers.*; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.OracleDataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.PostgreSQLDataSourceProvider; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -77,6 +78,7 @@ protected DataSourceProvider dataSourceProvider() { } @Test + @Ignore public void testIdentifierGenerator() throws InterruptedException, ExecutionException { LOGGER.debug("testIdentifierGenerator, database: {}, entityProvider: {}", dataSourceProvider.database(), entityProvider.getClass().getSimpleName()); //warming-up diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/PostEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/PostEntityProvider.java new file mode 100644 index 000000000..5476e80a4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/PostEntityProvider.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.optimizer.providers; + +import com.vladmihalcea.hpjp.util.EntityProvider; + +/** + * @author Vlad Mihalcea + */ +public abstract class PostEntityProvider implements EntityProvider { + + private final Class clazz; + + protected PostEntityProvider(Class clazz) { + this.clazz = clazz; + } + + public abstract T newPost(); + + @Override + public Class[] entities() { + return new Class[] { + clazz + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Sequence10PostEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Sequence10PostEntityProvider.java similarity index 90% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Sequence10PostEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Sequence10PostEntityProvider.java index cc5330536..87b95f341 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Sequence10PostEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Sequence10PostEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.optimizer.providers; +package com.vladmihalcea.hpjp.hibernate.identifier.optimizer.providers; import org.hibernate.annotations.GenericGenerator; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Sequence1PostEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Sequence1PostEntityProvider.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Sequence1PostEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Sequence1PostEntityProvider.java index 9004786b8..70184625a 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Sequence1PostEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Sequence1PostEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.optimizer.providers; +package com.vladmihalcea.hpjp.hibernate.identifier.optimizer.providers; import org.hibernate.annotations.GenericGenerator; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Sequence50PostEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Sequence50PostEntityProvider.java similarity index 90% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Sequence50PostEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Sequence50PostEntityProvider.java index cfff8adf8..71402cf8e 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Sequence50PostEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Sequence50PostEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.optimizer.providers; +package com.vladmihalcea.hpjp.hibernate.identifier.optimizer.providers; import org.hibernate.annotations.GenericGenerator; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Sequence5PostEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Sequence5PostEntityProvider.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Sequence5PostEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Sequence5PostEntityProvider.java index 6daec0e87..c344873a4 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Sequence5PostEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Sequence5PostEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.optimizer.providers; +package com.vladmihalcea.hpjp.hibernate.identifier.optimizer.providers; import org.hibernate.annotations.GenericGenerator; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Table10PostEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Table10PostEntityProvider.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Table10PostEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Table10PostEntityProvider.java index c05457534..091ade33b 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Table10PostEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Table10PostEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.optimizer.providers; +package com.vladmihalcea.hpjp.hibernate.identifier.optimizer.providers; import org.hibernate.annotations.GenericGenerator; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Table1PostEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Table1PostEntityProvider.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Table1PostEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Table1PostEntityProvider.java index 21a4859cb..c24de566a 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Table1PostEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Table1PostEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.optimizer.providers; +package com.vladmihalcea.hpjp.hibernate.identifier.optimizer.providers; import org.hibernate.annotations.GenericGenerator; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Table50PostEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Table50PostEntityProvider.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Table50PostEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Table50PostEntityProvider.java index 6d1b631c3..dfd5410b3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Table50PostEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Table50PostEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.optimizer.providers; +package com.vladmihalcea.hpjp.hibernate.identifier.optimizer.providers; import org.hibernate.annotations.GenericGenerator; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Table5PostEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Table5PostEntityProvider.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Table5PostEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Table5PostEntityProvider.java index 945f03ec8..74c5b312a 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/identifier/optimizer/providers/Table5PostEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/optimizer/providers/Table5PostEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.hibernate.identifier.optimizer.providers; +package com.vladmihalcea.hpjp.hibernate.identifier.optimizer.providers; import org.hibernate.annotations.GenericGenerator; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/tsid/TsidTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/tsid/TsidTest.java new file mode 100644 index 000000000..b11a0a551 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/tsid/TsidTest.java @@ -0,0 +1,156 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.tsid; + +import com.vladmihalcea.hpjp.util.TsidUtils; +import io.hypersistence.tsid.TSID; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.SecureRandom; +import java.text.DecimalFormat; +import java.util.Random; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.IntSupplier; + +import static org.junit.Assert.assertNull; + +public class TsidTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Test + public void test() { + TSID tsid = TSID.fast(); + long tsidLong = tsid.toLong(); + String tsidString = tsid.toString(); + long tsidMillis = tsid.getUnixMilliseconds(); + + LOGGER.info("TSID numerical value: {}", tsidLong); + LOGGER.info("TSID string value: {}", tsidString); + LOGGER.info("TSID time millis since epoch value: {}", tsidMillis); + + for (int i = 0; i < 10; i++) { + LOGGER.info( + "TSID numerical value: {}", + TSID.fast().toLong() + ); + } + } + + @Test + public void testNodeCount() { + TSID.Factory tsidFactory = TsidUtils.getTsidFactory(27, 11); + tsidFactory.generate(); + } + + @Test + public void testConcurrency() throws InterruptedException { + int threadCount = 16; + int iterationCount = 100_000; + + CountDownLatch endLatch = new CountDownLatch(threadCount); + + ConcurrentMap tsidMap = new ConcurrentHashMap<>(); + + long startNanos = System.nanoTime(); + + AtomicLong collisionCount = new AtomicLong(); + + int nodeCount = 2; + + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + new Thread(() -> { + TSID.Factory tsidFactory = TsidUtils.getTsidFactory(nodeCount, threadId % nodeCount); + + for (int j = 0; j < iterationCount; j++) { + TSID tsid = tsidFactory.generate(); + Integer existingTsid = tsidMap.put(tsid, (threadId * iterationCount) + j); + if(existingTsid != null) { + collisionCount.incrementAndGet(); + } + } + + endLatch.countDown(); + }).start(); + } + LOGGER.info("Starting threads"); + endLatch.await(); + + LOGGER.info( + "{} threads generated {} TSIDs in {} ms with {} collisions", + threadCount, + new DecimalFormat("###,###,###").format( + threadCount * iterationCount + ), + TimeUnit.NANOSECONDS.toMillis( + System.nanoTime() - startNanos + ), + collisionCount + ); + } + + @Test + public void testConcurrencyNoConflict() throws InterruptedException { + int threadCount = 16; + int iterationCount = 100_000; + + CountDownLatch endLatch = new CountDownLatch(threadCount); + + ConcurrentMap tsidMap = new ConcurrentHashMap<>(); + + long startNanos = System.nanoTime(); + + AtomicLong collisionCount = new AtomicLong(); + + int nodeCount = 2; + + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + new Thread(() -> { + int nodeId = threadId % nodeCount; + int nodeBits = (int) (Math.log(nodeCount) / Math.log(2)); + + final Random random = new SecureRandom(); + + TSID.Factory.Builder builder = TSID.Factory.builder(); + builder.withNodeBits(nodeBits); + builder.withNode(nodeId); + builder.withRandomFunction(new IntSupplier() { + @Override + public synchronized int getAsInt() { + return random.nextInt(); + } + }); + TSID.Factory tsidFactory = builder + .build(); + + for (int j = 0; j < iterationCount; j++) { + TSID tsid = tsidFactory.generate(); + Integer existingTsid = tsidMap.put(tsid, (threadId * iterationCount) + j); + if(existingTsid != null) { + collisionCount.incrementAndGet(); + } + } + + endLatch.countDown(); + }).start(); + } + LOGGER.info("Starting threads"); + endLatch.await(); + + LOGGER.info( + "{} threads generated {} TSIDs in {} ms with {} collisions", + threadCount, + new DecimalFormat("###,###,###").format( + threadCount * iterationCount + ), + TimeUnit.NANOSECONDS.toMillis( + System.nanoTime() - startNanos + ), + collisionCount + ); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/AssignedUUIDIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/AssignedUUIDIdentifierTest.java new file mode 100644 index 000000000..3bb85652f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/AssignedUUIDIdentifierTest.java @@ -0,0 +1,82 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.uuid; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.UUID; + +import static org.junit.Assert.*; + +public class AssignedUUIDIdentifierTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Test + public void testAssignedIdentifierGenerator() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + + assertEquals( + "High-Performance Java Persistence", + entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getSingleResult() + .getTitle() + ); + + byte[] uuid = (byte[]) entityManager.createNativeQuery( + "select id from Post") + .getSingleResult(); + + assertNotNull(uuid); + + entityManager.merge( + new Post() + .setTitle("High-Performance Java Persistence") + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @Column(columnDefinition = "BINARY(16)") + private UUID id = UUID.randomUUID(); + + private String title; + + public UUID getId() { + return id; + } + + public Post setId(UUID id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/AutoUUIDIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/AutoUUIDIdentifierTest.java new file mode 100644 index 000000000..713c23254 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/AutoUUIDIdentifierTest.java @@ -0,0 +1,77 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.uuid; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Properties; +import java.util.UUID; + +public class AutoUUIDIdentifierTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Test + public void testPersist() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + }); + } + + @Test + public void testBatch() { + doInJPA(entityManager -> { + for (int i = 0; i < 3; i++) { + Post post = new Post(); + post.setTitle( + String.format("High-Performance Java Persistence, Part %d", i + 1) + ); + + entityManager.persist(post); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private UUID id; + + private String title; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/PostgreSQLUUIDGenerationStrategy.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/PostgreSQLUUIDGenerationStrategy.java new file mode 100644 index 000000000..1908580dd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/PostgreSQLUUIDGenerationStrategy.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.uuid; + +import org.hibernate.Session; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.UUIDGenerationStrategy; + +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.UUID; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLUUIDGenerationStrategy implements UUIDGenerationStrategy { + + @Override + public int getGeneratedVersion() { + return 4; + } + + @Override + public UUID generateUUID(SharedSessionContractImplementor session) { + return ((Session) session).doReturningWork(connection -> { + try(Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select uuid_generate_v4()")) { + while (resultSet.next()) { + return (UUID) resultSet.getObject(1); + } + } + throw new IllegalArgumentException("Can't fetch a new UUID"); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/PostgreSQLUUIDIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/PostgreSQLUUIDIdentifierTest.java new file mode 100644 index 000000000..353581c2f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/PostgreSQLUUIDIdentifierTest.java @@ -0,0 +1,92 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.uuid; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.annotations.*; +import org.hibernate.annotations.Parameter; +import org.junit.Test; + +import jakarta.persistence.*; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import java.util.Properties; +import java.util.UUID; + +public class PostgreSQLUUIDIdentifierTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Override + protected void afterInit() { + executeStatement("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\""); + } + + @Test + public void testPersist() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + }); + } + + @Test + public void testBatch() { + doInJPA(entityManager -> { + for (int i = 0; i < 3; i++) { + Post post = new Post(); + post.setTitle( + String.format("High-Performance Java Persistence, Part %d", i + 1) + ); + + entityManager.persist(post); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "pg-uuid") + @GenericGenerator(name = "pg-uuid", strategy = "uuid2", + parameters = @Parameter( + name = "uuid_gen_strategy_class", + value = "com.vladmihalcea.hpjp.hibernate.identifier.uuid.PostgreSQLUUIDGenerationStrategy" + ) + ) + private UUID id; + + private String title; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/UUID2IdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/UUID2IdentifierTest.java new file mode 100644 index 000000000..7f1578e8e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/UUID2IdentifierTest.java @@ -0,0 +1,64 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.uuid; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.GenericGenerator; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.UUID; + +public class UUID2IdentifierTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Test + public void testUUID2IdentifierGenerator() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + entityManager.flush(); + entityManager.merge( + new Post() + .setTitle("High-Performance Java Persistence") + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(generator = "uuid2") + @GenericGenerator(name = "uuid2", strategy = "uuid2") + @Column(columnDefinition = "BINARY(16)") + private UUID id; + + private String title; + + public UUID getId() { + return id; + } + + public Post setId(UUID id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/UUIDIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/UUIDIdentifierTest.java new file mode 100644 index 000000000..6edb6a0a6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/identifier/uuid/UUIDIdentifierTest.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.hibernate.identifier.uuid; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.GenericGenerator; +import org.junit.Test; + +import jakarta.persistence.*; + +public class UUIDIdentifierTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Test + public void testUUIDIdentifierGenerator() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + entityManager.flush(); + entityManager.merge( + new Post() + .setTitle("High-Performance Java Persistence") + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(generator = "uuid") + @GenericGenerator(name = "uuid", strategy = "uuid") + @Column(columnDefinition = "CHAR(32)") + private String id; + + private String title; + + public String getId() { + return id; + } + + public Post setId(String id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/DefaultDatabaseIndexTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/DefaultDatabaseIndexTest.java new file mode 100644 index 000000000..78600d807 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/DefaultDatabaseIndexTest.java @@ -0,0 +1,278 @@ +package com.vladmihalcea.hpjp.hibernate.index; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import jakarta.persistence.*; +import java.util.*; + +/** + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public class DefaultDatabaseIndexTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + private final Database database; + + public DefaultDatabaseIndexTest(Database database) { + this.database = database; + } + + @Parameterized.Parameters + public static Collection rdbmsDataSourceProvider() { + List databases = new ArrayList<>(); + databases.add(new Database[] {Database.ORACLE}); + databases.add(new Database[] {Database.SQLSERVER}); + databases.add(new Database[] {Database.POSTGRESQL}); + databases.add(new Database[] {Database.MYSQL}); + return databases; + } + + @Override + protected Database database() { + return database; + } + + private final String[] tables = new String[] { + "post", + "post_comment" + }; + + @Test + public void test() { + doInJPA(this::findIndexes); + } + + private void findIndexes(EntityManager entityManager) { + switch(database) { + case ORACLE -> Arrays.stream(tables).forEach(table -> findOracleTableIndexes(entityManager, table)); + case SQLSERVER -> Arrays.stream(tables).forEach(table -> findSQLServerTableIndexes(entityManager, table)); + case POSTGRESQL -> Arrays.stream(tables).forEach(table -> findPostgreSQLTableIndexes(entityManager, table)); + case MYSQL -> Arrays.stream(tables).forEach(table -> findMySQLTableIndexes(entityManager, table)); + } + } + + private void findOracleTableIndexes(EntityManager entityManager, String tableName) { + findTableIndexes(entityManager, tableName, """ + SELECT + ind.index_name AS index_name, + CASE + WHEN ind.uniqueness = 'UNIQUE' THEN 1 + WHEN ind.uniqueness = 'NONUNIQUE' THEN 0 + END AS is_unique, + ind_col.column_name AS column_name + FROM + sys.all_indexes ind + INNER JOIN + sys.all_ind_columns ind_col ON + ind.owner = ind_col.index_owner AND + ind.index_name = ind_col.index_name + WHERE + lower(ind.table_name) = :tableName + """ + ); + } + + private void findSQLServerTableIndexes(EntityManager entityManager, String tableName) { + findTableIndexes(entityManager, tableName, """ + SELECT + ind.name AS index_name, + ind.is_unique AS is_unique, + col.name AS column_name + FROM + sys.indexes ind + INNER JOIN + sys.index_columns ic ON ind.object_id = ic.object_id AND ind.index_id = ic.index_id + INNER JOIN + sys.columns col ON ic.object_id = col.object_id AND ic.column_id = col.column_id + INNER JOIN + sys.tables t ON ind.object_id = t.object_id + WHERE + t.name = :tableName + """ + ); + } + + private void findPostgreSQLTableIndexes(EntityManager entityManager, String tableName) { + findTableIndexes(entityManager, addSchema(tableName), """ + SELECT + i.relname AS index_name, + ix.indisunique AS is_unique, + a.attname AS column_name + FROM + pg_class c + INNER JOIN + pg_index ix ON c.oid = ix.indrelid + INNER JOIN + pg_class i ON ix.indexrelid = i.oid + INNER JOIN + pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(ix.indkey) + WHERE + c.oid = CAST(CAST(:tableName AS regclass) AS oid) + ORDER BY + array_position(ix.indkey, a.attnum) + """ + ); + } + + private void findMySQLTableIndexes(EntityManager entityManager, String tableName) { + findTableIndexes(entityManager, tableName, """ + SELECT + INDEX_NAME AS index_name, + !NON_UNIQUE AS is_unique, + COLUMN_NAME as column_name + FROM + INFORMATION_SCHEMA.STATISTICS + WHERE + TABLE_NAME = :tableName + """ + ); + } + + private void findTableIndexes(EntityManager entityManager, String tableName, String query) { + List indexes = entityManager.createNativeQuery(query, Tuple.class) + .setParameter("tableName", tableName) + .getResultList(); + + for (Tuple index : indexes) { + LOGGER.info( + "Database [{}], Table [{}], Column [{}], Index [{}], Unique [{}]", + database, + tableName, + index.get("column_name"), + index.get("index_name"), + index.get("is_unique") + ); + } + } + + private String addSchema(String tableName) { + return String.format("public.%s", tableName); + } + + @Entity(name = "Post") + @Table( + name = "post", + uniqueConstraints = @UniqueConstraint( + name = "UK_POST_SLUG", + columnNames = "slug" + ) + ) + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @Column + private String slug; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Post post = (Post) o; + return Objects.equals(slug, post.getSlug()); + } + + @Override + public int hashCode() { + return Objects.hash(slug); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn( + name = "post_id", + foreignKey = @ForeignKey( + name = "FK_POST_COMMENT_POST_ID" + ) + ) + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostComment)) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/SQLServerSendStringParametersAsUnicodeComparisonTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/SQLServerSendStringParametersAsUnicodeComparisonTest.java new file mode 100644 index 000000000..d6ab8ff02 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/SQLServerSendStringParametersAsUnicodeComparisonTest.java @@ -0,0 +1,150 @@ +package com.vladmihalcea.hpjp.hibernate.index; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.SQLServerDataSourceProvider; +import org.hibernate.Session; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import jakarta.persistence.*; +import java.sql.PreparedStatement; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public class SQLServerSendStringParametersAsUnicodeComparisonTest extends AbstractTest { + + private final boolean sendStringParametersAsUnicode; + + public SQLServerSendStringParametersAsUnicodeComparisonTest(boolean sendStringParametersAsUnicode) { + this.sendStringParametersAsUnicode = sendStringParametersAsUnicode; + } + + @Parameterized.Parameters + public static Collection parameters() { + return Arrays.asList(new Boolean[][] { + { true }, + { false } + }); + } + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + SQLServerDataSourceProvider dataSourceProvider = new SQLServerDataSourceProvider(); + dataSourceProvider.setSendStringParametersAsUnicode(sendStringParametersAsUnicode); + return dataSourceProvider; + } + + public static final int POST_COUNT = 250; + + @Override + public void afterInit() { + doInJPA(entityManager -> { + for (long i = 1; i <= POST_COUNT; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle( + String.format("High-Performance Java Persistence, part %d", i) + ) + ); + + if(i % 100 == 0) { + entityManager.flush(); + } + } + }); + } + + @Test + public void test() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + + doInJPA(entityManager -> { + executeStatement(entityManager, "SET STATISTICS IO, TIME, PROFILE ON"); + + LOGGER.info("Test with sendStringParametersAsUnicode=" + sendStringParametersAsUnicode); + + findByTitle( + entityManager, String.format( + "High-Performance Java Persistence, part %d", + random.nextLong(POST_COUNT) + ) + ); + + executeStatement(entityManager, "SET STATISTICS IO, TIME, PROFILE OFF"); + }); + } + + private void findByTitle(EntityManager entityManager, String title) { + LOGGER.info("Find post by title: {}", title); + + entityManager.unwrap(Session.class).doWork(connection -> { + try (PreparedStatement statement = connection.prepareStatement(""" + SELECT PostId, Title + FROM Post + WHERE Title = ? + """.replaceAll("\n", " ") + )) { + + statement.setString(1, title); + + if (statement.execute() && statement.getMoreResults()) { + LOGGER.info("Execution plan: {}{}", + System.lineSeparator(), + resultSetToString(statement.getResultSet()) + ); + } + } + }); + } + + @Entity(name = "Post") + @Table( + name = "Post", + indexes = @Index( + name = "IDX_Post_Title", + columnList = "Title" + ) + ) + public static class Post { + + @Id + @Column(name = "PostID") + private Long id; + + @Column(name = "Title") + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/Book.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/Book.java new file mode 100644 index 000000000..8c59d2689 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/Book.java @@ -0,0 +1,81 @@ +package com.vladmihalcea.hpjp.hibernate.index.postgres; + +import com.fasterxml.jackson.databind.JsonNode; +import io.hypersistence.utils.hibernate.type.json.JsonType; +import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Book") +@Table(name = "book") +public class Book { + + @Id + private Long id; + + @Column(length = 100) + private String title; + + @Column(name = "published_on") + private LocalDateTime publishedOn = LocalDateTime.now(); + + @Column(name = "author", length = 50) + private String author; + + @Type(JsonType.class) + @Column(columnDefinition = "jsonb") + private String properties; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public LocalDateTime getPublishedOn() { + return publishedOn; + } + + public Book setPublishedOn(LocalDateTime publishedOn) { + this.publishedOn = publishedOn; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + + public JsonNode getJsonNodeProperties() { + return JacksonUtil.toJsonNode(properties); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/PostgreSQLBRINIndexTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/PostgreSQLBRINIndexTest.java new file mode 100644 index 000000000..8b7254ca9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/PostgreSQLBRINIndexTest.java @@ -0,0 +1,202 @@ +package com.vladmihalcea.hpjp.hibernate.index.postgres; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLBRINIndexTest extends AbstractTest { + + public static final int ROW_COUNT = 1_000; + public static final int BATCH_SIZE = 500; + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty("hibernate.jdbc.batch_size", String.valueOf(BATCH_SIZE)); + properties.setProperty("hibernate.order_inserts", "true"); + properties.setProperty("hibernate.order_updates", "true"); + } + + @Override + public void afterInit() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJPA(entityManager -> { + ThreadLocalRandom random = ThreadLocalRandom.current(); + LocalDateTime timestamp = LocalDateTime.of(2024, 2, 29, 12, 0, 0); + for (long i = 1; i <= ROW_COUNT; i++) { + timestamp = timestamp.plusHours(6); + entityManager.persist( + new Post() + .setId(i) + .setTitle( + String.format("Post nr. %d", i) + ) + .setCreatedOn(timestamp) + .setCreatedBy( + random.nextInt(10) > 5 ? "Vlad Mihalcea" : "Alex Mihalcea" + ) + ); + + if(i % BATCH_SIZE == 0) { + entityManager.flush(); + } + } + }); + + executeStatement( + "DROP INDEX IF EXISTS idx_post_created_on", + """ + CREATE INDEX IF NOT EXISTS idx_post_created_on + ON post USING BRIN(created_on) + """, + "ANALYZE VERBOSE" + ); + } + + @Test + public void testEquality() { + List executionPlanLines = doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + EXPLAIN (ANALYZE, BUFFERS) + SELECT p.title + FROM post p + WHERE p.created_on = :timestamp + """, String.class) + .setParameter("timestamp", LocalDateTime.of(2024, 3, 4, 12, 0, 0)) + .getResultList(); + }); + + LOGGER.info("Execution plan: \n{}", String.join("\n", executionPlanLines)); + } + + @Test + public void testRangeScan() { + List executionPlanLines = doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + EXPLAIN (ANALYZE, BUFFERS) + SELECT p.title + FROM post p + WHERE + p.created_on >= :startTimestamp AND + p.created_on < :endTimestamp + """, String.class) + .setParameter("startTimestamp", LocalDateTime.of(2024, 3, 4, 12, 0, 0)) + .setParameter("endTimestamp", LocalDateTime.of(2024, 3, 19, 0, 0, 0)) + .getResultList(); + }); + + LOGGER.info("Execution plan: \n{}", String.join("\n", executionPlanLines)); + } + + @Test + public void testOrderBy() { + List executionPlanLines = doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + EXPLAIN (ANALYZE, BUFFERS) + SELECT p.title + FROM post p + ORDER BY created_on DESC + FETCH FIRST 10 ROWS ONLY + """, String.class) + .getResultList(); + }); + + LOGGER.info("Execution plan: \n{}", String.join("\n", executionPlanLines)); + } + + @Test + public void testTableScan() { + List executionPlanLines = doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + EXPLAIN (ANALYZE, BUFFERS) + SELECT p.id, p.title, p.created_by, p.created_on + FROM post p + WHERE + p.created_by = :createdBy + ORDER BY + p.created_on + """, String.class) + .setParameter("createdBy", "Vlad Mihalcea") + .getResultList(); + }); + + LOGGER.info("Execution plan: \n{}", String.join("\n", executionPlanLines)); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public Post setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/PostgreSQLBalancedTreeIndexTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/PostgreSQLBalancedTreeIndexTest.java new file mode 100644 index 000000000..9c2e0f75b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/PostgreSQLBalancedTreeIndexTest.java @@ -0,0 +1,139 @@ +package com.vladmihalcea.hpjp.hibernate.index.postgres; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.RandomUtils; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLBalancedTreeIndexTest extends AbstractTest { + + public static final int ROW_COUNT = 5000; + public static final int BATCH_SIZE = 500; + + @Override + protected Class[] entities() { + return new Class[] { + Book.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty("hibernate.jdbc.batch_size", String.valueOf(BATCH_SIZE)); + properties.setProperty("hibernate.order_inserts", "true"); + properties.setProperty("hibernate.order_updates", "true"); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + LocalDateTime timestamp = LocalDateTime.of(2024, 2, 29, 12, 0, 0); + for (long i = 1; i <= ROW_COUNT; i++) { + timestamp = timestamp.plusHours(6); + entityManager.persist( + new Book() + .setId(i) + .setTitle(RandomUtils.randomTitle()) + .setPublishedOn(timestamp) + .setAuthor(RandomUtils.GENERATOR.nextInt(10) > 5 ? "Vlad Mihalcea" : "Alex Mihalcea") + ); + + if(i % BATCH_SIZE == 0) { + entityManager.flush(); + } + } + }); + + executeStatement( + "DROP INDEX IF EXISTS idx_book_published_on", + """ + CREATE INDEX IF NOT EXISTS idx_book_published_on + ON book (published_on) + INCLUDE (title) + """, + "ANALYZE VERBOSE" + ); + } + + @Test + public void testEquality() { + List executionPlanLines = doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + EXPLAIN (ANALYZE, BUFFERS) + SELECT title + FROM book + WHERE published_on = :timestamp + """, String.class) + .setParameter("timestamp", LocalDateTime.of(2024, 3, 4, 12, 0, 0)) + .getResultList(); + }); + + LOGGER.info("Execution plan: \n{}", String.join("\n", executionPlanLines)); + } + + @Test + public void testRangeScan() { + List executionPlanLines = doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + EXPLAIN (ANALYZE, BUFFERS) + SELECT title + FROM book + WHERE + published_on >= :startTimestamp AND + published_on < :endTimestamp + """, String.class) + .setParameter("startTimestamp", LocalDateTime.of(2024, 3, 1, 12, 0, 0)) + .setParameter("endTimestamp", LocalDateTime.of(2024, 3, 29, 18, 0, 0)) + .getResultList(); + }); + + LOGGER.info("Execution plan: \n{}", String.join("\n", executionPlanLines)); + } + + @Test + public void testOrderBy() { + List executionPlanLines = doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + EXPLAIN (ANALYZE, BUFFERS) + SELECT title + FROM book + ORDER BY published_on DESC + FETCH FIRST 10 ROWS ONLY + """, String.class) + .getResultList(); + }); + + LOGGER.info("Execution plan: \n{}", String.join("\n", executionPlanLines)); + } + + @Test + public void testTableScan() { + List executionPlanLines = doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + EXPLAIN (ANALYZE, BUFFERS) + SELECT id, title, author, published_on + FROM book + WHERE + author = :author + ORDER BY + published_on + """, String.class) + .setParameter("author", "Vlad Mihalcea") + .getResultList(); + }); + + LOGGER.info("Execution plan: \n{}", String.join("\n", executionPlanLines)); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/PostgreSQLGINIndexTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/PostgreSQLGINIndexTest.java new file mode 100644 index 000000000..b69597074 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/PostgreSQLGINIndexTest.java @@ -0,0 +1,200 @@ +package com.vladmihalcea.hpjp.hibernate.index.postgres; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.RandomUtils; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLGINIndexTest extends AbstractTest { + + public static final int ROW_COUNT = 5000; + public static final int BATCH_SIZE = 500; + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty("hibernate.jdbc.batch_size", String.valueOf(BATCH_SIZE)); + properties.setProperty("hibernate.order_inserts", "true"); + properties.setProperty("hibernate.order_updates", "true"); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea") + .setProperties(""" + { + "publisher": "Amazon", + "price": 44.99, + "reviews": [ + { + "reviewer": "Cristiano", + "review": "Excellent book to understand Java Persistence", + "date": "2017-11-14", + "rating": 5 + }, + { + "reviewer": "T.W", + "review": "The best JPA ORM book out there", + "date": "2019-01-27", + "rating": 5 + }, + { + "reviewer": "Shaikh", + "review": "The most informative book", + "date": "2016-12-24", + "rating": 4 + } + ] + } + """) + ); + + for (long i = 2; i <= ROW_COUNT; i++) { + LocalDateTime timestamp = LocalDateTime.of(2024, 2, 29, 12, 0, 0); + entityManager.persist( + new Book() + .setId(i) + .setTitle(RandomUtils.randomTitle()) + .setProperties( + String.format(""" + { + "reviews": [ + { + "reviewer": "Reviewer id: %1$d", + "review": "Review: %1$d", + "date": "Date: %2$s", + "rating": "Rating: %1$d" + } + ] + } + """, i, timestamp.plusHours(i) + ) + ) + ); + } + }); + } + + @Test + public void testGin() { + executeStatement( + "DROP INDEX IF EXISTS idx_book_properties_gin", + """ + CREATE INDEX idx_book_properties_gin + ON book USING GIN (properties) + """, + "ANALYZE VERBOSE" + ); + + List executionPlanLines = doInJPA(entityManager -> { + return entityManager.unwrap(Session.class) + .doReturningWork(connection -> selectColumnList( + connection, + """ + EXPLAIN (ANALYZE, BUFFERS) + SELECT title, author, published_on + FROM book + WHERE + properties ? 'publisher' + """, String.class + ) + ); + }); + + LOGGER.info("Execution plan: \n{}", String.join("\n", executionPlanLines)); + + executeStatement( + "DROP INDEX IF EXISTS idx_book_properties_gin" + ); + } + + @Test + public void testGinJsonbPathOps() { + executeStatement( + "DROP INDEX IF EXISTS idx_book_properties_gin", + """ + CREATE INDEX idx_book_properties_gin + ON book USING GIN (properties jsonb_path_ops) + """, + "ANALYZE VERBOSE" + ); + + List executionPlanLines = doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + EXPLAIN (ANALYZE, BUFFERS) + SELECT + title, author, published_on + FROM book + WHERE + properties @> '{"title": "High-Performance Java Persistence"}' + """, String.class) + .getResultList(); + }); + + LOGGER.info("Execution plan: \n{}", String.join("\n", executionPlanLines)); + + executeStatement( + "DROP INDEX IF EXISTS idx_book_properties_gin" + ); + } + + @Test + public void testGinPathExpression() { + executeStatement( + "DROP INDEX IF EXISTS idx_book_properties_gin", + """ + CREATE INDEX idx_book_properties_gin + ON book USING GIN ((properties -> 'reviews')) + """, + "ANALYZE VERBOSE" + ); + + List executionPlanLines = doInJPA(entityManager -> { + return entityManager + .unwrap(Session.class) + .doReturningWork( + connection -> selectColumnList( + connection, + """ + EXPLAIN (ANALYZE, BUFFERS) + SELECT + title, author, published_on + FROM book + WHERE + properties -> 'reviews' @> '[{"rating":5}]' + """, String.class + ) + ); + }); + + LOGGER.info("Execution plan: \n{}", String.join("\n", executionPlanLines)); + + executeStatement( + "DROP INDEX IF EXISTS idx_book_properties_gin" + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/PostgreSQLHashIndexTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/PostgreSQLHashIndexTest.java new file mode 100644 index 000000000..59e48ddb3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/PostgreSQLHashIndexTest.java @@ -0,0 +1,133 @@ +package com.vladmihalcea.hpjp.hibernate.index.postgres; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.RandomUtils; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLHashIndexTest extends AbstractTest { + + public static final int ROW_COUNT = 5000; + public static final int BATCH_SIZE = 500; + + @Override + protected Class[] entities() { + return new Class[] { + Book.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty("hibernate.jdbc.batch_size", String.valueOf(BATCH_SIZE)); + properties.setProperty("hibernate.order_inserts", "true"); + properties.setProperty("hibernate.order_updates", "true"); + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + + @Override + protected void beforeInit() { + executeStatement( + "DROP TABLE IF EXISTS book CASCADE", + """ + CREATE TABLE book ( + id bigint not null, + title varchar(100), + author varchar(50), + published_on timestamp(6), + properties jsonb, + PRIMARY KEY (id) + ) + """ + ); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + for (long i = 1; i <= ROW_COUNT; i++) { + entityManager.persist( + new Book() + .setId(i) + .setTitle(RandomUtils.randomTitle()) + .setAuthor("Vlad Mihalcea") + ); + + if(i % BATCH_SIZE == 0) { + entityManager.flush(); + } + } + }); + + executeStatement( + "DROP INDEX IF EXISTS idx_book_title_hash", + "DROP INDEX IF EXISTS idx_book_title_btree", + """ + CREATE INDEX IF NOT EXISTS idx_book_title_hash + ON book USING HASH (title) + """, + """ + CREATE INDEX IF NOT EXISTS idx_book_title_btree + ON book (title) + """, + "ANALYZE VERBOSE" + ); + } + + @Test + public void testEquality() { + List executionPlanLines = doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, RandomUtils.GENERATOR.nextLong(ROW_COUNT)); + String title = book.getTitle(); + + return entityManager.createNativeQuery(""" + EXPLAIN (ANALYZE, BUFFERS) + SELECT title, author, published_on + FROM book + WHERE title = :title + """, String.class) + .setParameter("title", title) + .getResultList(); + }); + + LOGGER.info("Execution plan: \n{}", String.join("\n", executionPlanLines)); + } + + @Test + @Ignore + public void testDuplicate() { + try { + doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, RandomUtils.GENERATOR.nextLong(ROW_COUNT)); + String title = book.getTitle(); + + entityManager.persist( + new Book() + .setId(1L + ROW_COUNT) + .setTitle(title) + ); + }); + fail("Should thrown ConstraintViolationException!"); + } catch (Exception expected) { + assertTrue(ExceptionUtil.isCausedBy(expected, ConstraintViolationException.class)); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/hot/PostgreSQLCtidDefaultUpdateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/hot/PostgreSQLCtidDefaultUpdateTest.java new file mode 100644 index 000000000..c7aa9414e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/hot/PostgreSQLCtidDefaultUpdateTest.java @@ -0,0 +1,115 @@ +package com.vladmihalcea.hpjp.hibernate.index.postgres.hot; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDateTime; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLCtidDefaultUpdateTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + @Test + public void testHOTDefaultUpdate() { + AtomicInteger revision = new AtomicInteger(); + checkHeapOnlyTuples(); + while (revision.incrementAndGet() <= 5){ + doInJPA(session -> { + Post post = new Post() + .setId((long) revision.get()) + .setTitle( + String.format( + "High-Performance Java Persistence, revision %d", + revision.get() + ) + ); + session.persist(post); + }); + } + + executeStatement( + "DROP INDEX IF EXISTS idx_post_created_on", + """ + CREATE INDEX IF NOT EXISTS idx_post_created_on ON post (created_on) + """, + "ANALYZE VERBOSE" + ); + doInJPA(entityManager -> { + for (long id = 1; id <= 5; id++) { + Post post = entityManager.find(Post.class, id); + post.setTitle("Changed"); + } + }); + + checkHeapOnlyTuples(); + } + + private void checkHeapOnlyTuples() { + doInJDBC(connection -> { + try (Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT n_tup_upd, n_tup_hot_upd + FROM pg_stat_user_tables + WHERE relname = 'post' + """ + ); + while (resultSet.next()) { + int i = 0; + long n_tup_upd = resultSet.getLong(++i); + long n_tup_hot_upd = resultSet.getLong(++i); + + LOGGER.info( + "n_tup_upd: {}, n_tup_hot_upd: {}", + n_tup_upd, + n_tup_hot_upd + ); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn = LocalDateTime.now(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/hot/PostgreSQLCtidTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/hot/PostgreSQLCtidTest.java new file mode 100644 index 000000000..41ffdc189 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/hot/PostgreSQLCtidTest.java @@ -0,0 +1,165 @@ +package com.vladmihalcea.hpjp.hibernate.index.postgres.hot; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import org.junit.Test; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLCtidTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testCtid() { + AtomicInteger revision = new AtomicInteger(); + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle( + String.format( + "High-Performance Java Persistence, revision %d", + revision.incrementAndGet() + ) + ) + ); + }); + checkCtid(); + doInJPA(entityManager -> { + entityManager + .find(Post.class, 1L) + .setTitle( + String.format( + "High-Performance Java Persistence, revision %d", + revision.incrementAndGet() + ) + ); + }); + checkCtid(); + doInJPA(entityManager -> { + entityManager + .find(Post.class, 1L) + .setTitle( + String.format( + "High-Performance Java Persistence, revision %d", + revision.incrementAndGet() + ) + ); + }); + checkCtid(); + } + + private void checkCtid() { + doInJPA(entityManager -> { + Tuple tuple = (Tuple) entityManager + .createNativeQuery(""" + SELECT + ctid, + id, + title + FROM + post + WHERE + id = :id + """, Tuple.class) + .setParameter("id", 1L) + .getSingleResult(); + + LOGGER.info( + "Ctid: {} for post with id: {} and title: {}", + tuple.get("ctid"), + tuple.get("id"), + tuple.get("title") + ); + }); + } + + @Test + public void testHOT() { + AtomicInteger revision = new AtomicInteger(); + checkHeapOnlyTuples(); + while (revision.incrementAndGet() <= 5){ + doInStatelessSession(session -> { + String title = String.format( + "High-Performance Java Persistence, revision %d", + revision.get() + ); + session.upsert( + new Post() + .setId(1L) + .setTitle(title) + ); + }); + } + checkHeapOnlyTuples(); + } + + private void checkHeapOnlyTuples() { + doInJDBC(connection -> { + try (Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT n_tup_upd, n_tup_hot_upd + FROM pg_stat_user_tables + WHERE relname = 'post' + """ + ); + while (resultSet.next()) { + int i = 0; + long n_tup_upd = resultSet.getLong(++i); + long n_tup_hot_upd = resultSet.getLong(++i); + + LOGGER.info( + "n_tup_upd: {}, n_tup_hot_upd: {}", + n_tup_upd, + n_tup_hot_upd + ); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/hot/PostgreSQLCtidVersionIndexTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/hot/PostgreSQLCtidVersionIndexTest.java new file mode 100644 index 000000000..4d229da02 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/hot/PostgreSQLCtidVersionIndexTest.java @@ -0,0 +1,219 @@ +package com.vladmihalcea.hpjp.hibernate.index.postgres.hot; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLCtidVersionIndexTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void afterInit() { + executeStatement( + "DROP INDEX IF EXISTS idx_post_version", + """ + CREATE INDEX IF NOT EXISTS idx_post_version ON post (version) + """, + "ANALYZE VERBOSE" + ); + } + + @Test + public void testCtid() { + AtomicInteger revision = new AtomicInteger(); + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle( + String.format( + "High-Performance Java Persistence, revision %d", + revision.incrementAndGet() + ) + ) + ); + }); + checkCtid(); + doInJPA(entityManager -> { + entityManager + .find(Post.class, 1L) + .setTitle( + String.format( + "High-Performance Java Persistence, revision %d", + revision.incrementAndGet() + ) + ); + }); + checkCtid(); + doInJPA(entityManager -> { + entityManager + .find(Post.class, 1L) + .setTitle( + String.format( + "High-Performance Java Persistence, revision %d", + revision.incrementAndGet() + ) + ); + }); + checkCtid(); + } + + @Test + public void testVersionUpdate() { + AtomicInteger revision = new AtomicInteger(); + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle( + String.format( + "High-Performance Java Persistence, revision %d", + revision.incrementAndGet() + ) + ) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + post.setTitle( + String.format( + "High-Performance Java Persistence, revision %d", + revision.incrementAndGet() + ) + ); + }); + } + + private void checkCtid() { + doInJPA(entityManager -> { + Tuple tuple = (Tuple) entityManager + .createNativeQuery(""" + SELECT + ctid, + id, + title + FROM + post + WHERE + id = :id + """, Tuple.class) + .setParameter("id", 1L) + .getSingleResult(); + + LOGGER.info( + "Ctid: {} for post with id: {} and title: {}", + tuple.get("ctid"), + tuple.get("id"), + tuple.get("title") + ); + }); + } + + @Test + public void testHOT() { + AtomicInteger revision = new AtomicInteger(); + + doInJPA(entityManager -> { + String title = String.format( + "High-Performance Java Persistence, revision %d", + revision.get() + ); + entityManager.persist( + new Post() + .setId(1L) + .setTitle(title) + ); + }); + + checkHeapOnlyTuples(); + + while (revision.incrementAndGet() <= 5){ + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + post.setTitle( + String.format( + "High-Performance Java Persistence, revision %d", + revision.get() + ) + ); + }); + } + + checkHeapOnlyTuples(); + } + + private void checkHeapOnlyTuples() { + doInJDBC(connection -> { + try (Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT n_tup_upd, n_tup_hot_upd + FROM pg_stat_user_tables + WHERE relname = 'post' + """ + ); + while (resultSet.next()) { + int i = 0; + long n_tup_upd = resultSet.getLong(++i); + long n_tup_hot_upd = resultSet.getLong(++i); + + LOGGER.info( + "n_tup_upd: {}, n_tup_hot_upd: {}", + n_tup_upd, + n_tup_hot_upd + ); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @Column(length = 100) + private String title; + + @Version + private short version; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/hot/PostgreSQLHOTDynamicUpdateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/hot/PostgreSQLHOTDynamicUpdateTest.java new file mode 100644 index 000000000..64e37bbc9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/index/postgres/hot/PostgreSQLHOTDynamicUpdateTest.java @@ -0,0 +1,282 @@ +package com.vladmihalcea.hpjp.hibernate.index.postgres.hot; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.json.JsonType; +import jakarta.persistence.*; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLHOTDynamicUpdateTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class, + Author.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Author author = new Author() + .setId(1L) + .setFirstName("Vlad") + .setLastName("Mihalcea") + .setCountry("România"); + + entityManager.persist(author); + + Book book = new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor(author) + .addProperty("title", "High-Performance Java Persistence") + .addProperty("author", "Vlad Mihalcea") + .addProperty("publisher", "Amazon") + .addProperty("price", "$44.95"); + entityManager.persist( + book + ); + }); + executeStatement( + "DROP INDEX IF EXISTS idx_book_author_id", + "DROP INDEX IF EXISTS idx_book_isbn", + """ + CREATE INDEX IF NOT EXISTS idx_book_isbn ON book (isbn) + """, + """ + CREATE INDEX idx_book_author_id ON book (author_id) + """, + "ANALYZE VERBOSE" + ); + } + + @Test + public void testHOT() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + for (int i = 1; i <= 50; i++) { + final int revision = i; + checkHeapOnlyTuples(); + doInJPA(entityManager -> { + Book book = entityManager.createQuery(""" + select b + from Book b + where b.isbn = :isbn + """, Book.class) + .setParameter("isbn", "978-9730228236") + .getSingleResult(); + + book.setTitle( + String.format( + "High-Performance Java Persistence, revision %d", + revision + ) + ); + + Map props = book.getProperties(); + book.setProperties( + revision % 2 == 0 ? new HashMap<>(props) : new TreeMap<>(props) + ); + }); + checkHeapOnlyTuples(); + } + } + + private void checkHeapOnlyTuples() { + doInJDBC(connection -> { + try { + try (Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery( + """ + SELECT n_tup_upd, n_tup_hot_upd + FROM pg_stat_user_tables + WHERE relname = 'book' + """ + ); + while (resultSet.next()) { + int i = 0; + long n_tup_upd = resultSet.getLong(++i); + long n_tup_hot_upd = resultSet.getLong(++i); + + LOGGER.info("HOT: n_tup_upd: {}, n_tup_hot_upd: {}", n_tup_upd, n_tup_hot_upd); + } + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + }); + + } + + @Entity(name = "Book") + @Table(name = "book") + //@DynamicUpdate + public static class Book { + + @Id + @GeneratedValue + private Long id; + + private String isbn; + + private String title; + + @ManyToOne(fetch = FetchType.LAZY) + private Author author; + + @Column(name = "price_in_cents") + private int priceInCents; + + private String publisher; + + @Column(columnDefinition = "json") + @Type(JsonType.class) + private Map properties = new HashMap<>(); + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public Author getAuthor() { + return author; + } + + public Book setAuthor(Author author) { + this.author = author; + return this; + } + + public int getPriceInCents() { + return priceInCents; + } + + public Book setPriceInCents(int priceInCents) { + this.priceInCents = priceInCents; + return this; + } + + public String getPublisher() { + return publisher; + } + + public Book setPublisher(String publisher) { + this.publisher = publisher; + return this; + } + + public Map getProperties() { + return properties; + } + + public Book setProperties(Map properties) { + this.properties = properties; + return this; + } + + public Book addProperty(String key, String value) { + properties.put(key, value); + return this; + } + } + + @Entity(name = "Author") + @Table(name = "author") + @DynamicUpdate + public static class Author { + + @Id + private Long id; + + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + private String country; + + @Column(name = "tax_treaty_claiming") + private boolean taxTreatyClaiming; + + public Long getId() { + return id; + } + + public Author setId(Long id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public Author setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public Author setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public String getCountry() { + return country; + } + + public Author setCountry(String country) { + this.country = country; + return this; + } + + public boolean isTaxTreatyClaiming() { + return taxTreatyClaiming; + } + + public Author setTaxTreatyClaiming(boolean taxTreatyClaiming) { + this.taxTreatyClaiming = taxTreatyClaiming; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/ImplicitPolymorphismTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/ImplicitPolymorphismTest.java new file mode 100644 index 000000000..05aedd3c1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/ImplicitPolymorphismTest.java @@ -0,0 +1,199 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance; + +import com.vladmihalcea.hpjp.hibernate.type.json.model.*; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.Hibernate; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ImplicitPolymorphismTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Board.class, + Post.class, + Announcement.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Board board = new Board() + .setId(1L) + .setName("Hibernate"); + + entityManager.persist(board); + + Post post = new Post() + .setOwner("Vlad Mihalcea") + .setTitle("High-Performance Java Persistence") + .setContent("Best practices") + .setBoard(board); + + entityManager.persist(post); + + Announcement announcement = new Announcement() + .setOwner("Vlad Mihalcea") + .setTitle("Release 1.2.3") + .setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))) + .setBoard(board); + + entityManager.persist(announcement); + }); + doInJPA(entityManager -> { + List topics = entityManager.createQuery(""" + select e + from com.vladmihalcea.hpjp.hibernate.inheritance.ImplicitPolymorphismTest$Topic e + """) + .getResultList(); + + assertEquals(2, topics.size()); + topics.sort(Comparator.comparing(e -> e.getClass().getName())); + + assertEquals(Announcement.class, topics.get(0).getClass()); + assertEquals(Post.class, topics.get(1).getClass()); + }); + } + + @Entity(name = "Board") + @Table(name = "board") + public static class Board { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Board setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Board setName(String name) { + this.name = name; + return this; + } + } + + @MappedSuperclass + public static abstract class Topic> { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + @ManyToOne(fetch = FetchType.LAZY) + private Board board; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public T setTitle(String title) { + this.title = title; + return (T) this; + } + + public String getOwner() { + return owner; + } + + public T setOwner(String owner) { + this.owner = owner; + return (T) this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public T setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return (T) this; + } + + public Board getBoard() { + return board; + } + + public T setBoard(Board board) { + this.board = board; + return (T) this; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public Post setContent(String content) { + this.content = content; + return this; + } + } + + @Entity(name = "Announcement") + @Table(name = "announcement") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public Announcement setValidUntil(Date validUntil) { + this.validUntil = validUntil; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTableBulkDeleteTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTableBulkDeleteTest.java new file mode 100644 index 000000000..f5cb7be1a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTableBulkDeleteTest.java @@ -0,0 +1,189 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JoinedTableBulkDeleteTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Board.class, + Topic.class, + Post.class, + Announcement.class, + }; + } + + @Test + public void test() { + Topic topic = doInJPA(entityManager -> { + Board board = new Board(); + board.setName("Hibernate"); + + entityManager.persist(board); + + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + post.setBoard(board); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + announcement.setBoard(board); + + entityManager.persist(announcement); + + return post; + }); + + doInJPA(entityManager -> { + int updateCount = entityManager + .createQuery("delete from Topic") + .executeUpdate(); + assertEquals(2, updateCount); + }); + } + + @Entity(name = "Board") + @Table(name = "board") + public static class Board { + + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(mappedBy = "board") + private List topics = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getTopics() { + return topics; + } + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.JOINED) + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + @ManyToOne(fetch = FetchType.LAZY) + private Board board; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public Board getBoard() { + return board; + } + + public void setBoard(Board board) { + this.board = board; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + @Table(name = "announcement") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTableDiscriminatorColumnTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTableDiscriminatorColumnTest.java new file mode 100644 index 000000000..d8ce10f4e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTableDiscriminatorColumnTest.java @@ -0,0 +1,388 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class JoinedTableDiscriminatorColumnTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Board.class, + Topic.class, + Post.class, + Announcement.class, + TopicStatistics.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void test() { + Topic topic = doInJPA(entityManager -> { + Board board = new Board(); + board.setName("Hibernate"); + + entityManager.persist(board); + + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + post.setBoard(board); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + announcement.setBoard(board); + + entityManager.persist(announcement); + + TopicStatistics postStatistics = new TopicStatistics(post); + postStatistics.incrementViews(); + entityManager.persist(postStatistics); + + TopicStatistics announcementStatistics = new TopicStatistics(announcement); + announcementStatistics.incrementViews(); + entityManager.persist(announcementStatistics); + + return post; + }); + + doInJPA(entityManager -> { + Board board = topic.getBoard(); + LOGGER.info("Fetch Topics"); + List topics = entityManager + .createQuery("select t from Topic t where t.board = :board", Topic.class) + .setParameter("board", board) + .getResultList(); + }); + + doInJPA(entityManager -> { + Board board = topic.getBoard(); + LOGGER.info("Fetch Topic projection"); + List titles = entityManager + .createQuery("select t.title from Topic t where t.board = :board", String.class) + .setParameter("board", board) + .getResultList(); + assertEquals(2, titles.size()); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetch just one Topic"); + Topic _topic = entityManager.find(Topic.class, topic.getId()); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetch Board topics"); + entityManager.find(Board.class, topic.getBoard().getId()).getTopics().size(); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetch Board topics eagerly"); + Long id = topic.getBoard().getId(); + Board board = entityManager.createQuery( + "select b from Board b join fetch b.topics where b.id = :id", Board.class) + .setParameter("id", id) + .getSingleResult(); + }); + + doInJPA(entityManager -> { + Long topicId = topic.getId(); + LOGGER.info("Fetch statistics"); + TopicStatistics statistics = entityManager + .createQuery("select s from TopicStatistics s join fetch s.topic t where t.id = :topicId", TopicStatistics.class) + .setParameter("topicId", topicId) + .getSingleResult(); + }); + + TopicStatistics statistics = doInJPA(entityManager -> { + Long topicId = topic.getId(); + LOGGER.info("Fetch one TopicStatistic"); + return entityManager.find(TopicStatistics.class, topicId); + }); + + try { + statistics.getTopic().getCreatedOn(); + } + catch (Exception expected) { + LOGGER.info( "Topic was not fetched" ); + } + + doInJPA(entityManager -> { + + List results = entityManager.createQuery(""" + select count(t), t.class + from Topic t + group by t.class + order by t.class + """) + .getResultList(); + + assertEquals(2, results.size()); + }); + + doInJPA(entityManager -> { + Board board = topic.getBoard(); + + List topics = entityManager.createQuery(""" + select t + from Topic t + where t.board = :board + order by t.class + """, Topic.class) + .setParameter("board", board) + .getResultList(); + + assertEquals(2, topics.size()); + assertTrue(topics.get(0) instanceof Post); + assertTrue(topics.get(1) instanceof Announcement); + }); + } + + @Test + public void testQueryUsingAll() { + doInJPA(entityManager -> { + Board board1 = new Board(); + board1.setName("Hibernate"); + + entityManager.persist(board1); + + Post post1 = new Post(); + post1.setOwner("John Doe"); + post1.setTitle("Inheritance"); + post1.setContent("Best practices"); + post1.setBoard(board1); + + entityManager.persist(post1); + + Announcement announcement1 = new Announcement(); + announcement1.setOwner("John Doe"); + announcement1.setTitle("Release x.y.z.Final"); + announcement1.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + announcement1.setBoard(board1); + + entityManager.persist(announcement1); + + Board board2 = new Board(); + board2.setName("JPA"); + + entityManager.persist(board2); + + Post post2 = new Post(); + post2.setOwner("John Doe"); + post2.setTitle("Inheritance"); + post2.setContent("Best practices"); + post2.setBoard(board2); + + entityManager.persist(post2); + + Post post3 = new Post(); + post3.setOwner("John Doe"); + post3.setTitle("Inheritance"); + post3.setContent("More best practices"); + post3.setBoard(board2); + + entityManager.persist(post3); + }); + + doInJPA(entityManager -> { + List postOnlyBoards = entityManager + .createQuery( + "select b " + + "from Board b " + + "where Post = all (" + + " select type(t) from Topic t where t.board = b" + + ")", Board.class) + .getResultList(); + assertEquals(1, postOnlyBoards.size()); + assertEquals("JPA", postOnlyBoards.get(0).getName()); + }); + } + + @Entity(name = "Board") + @Table(name = "board") + public static class Board { + + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(mappedBy = "board") + private List topics = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getTopics() { + return topics; + } + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.JOINED) + @DiscriminatorColumn + @DiscriminatorValue("100") + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + @ManyToOne(fetch = FetchType.LAZY) + private Board board; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public Board getBoard() { + return board; + } + + public void setBoard(Board board) { + this.board = board; + } + } + + @Entity(name = "Post") + @Table(name = "post") + @DiscriminatorValue("101") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + @Table(name = "announcement") + @DiscriminatorValue("102") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } + + @Entity(name = "TopicStatistics") + @Table(name = "topic_statistics") + public static class TopicStatistics { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Topic topic; + + private long views; + + public TopicStatistics() {} + + public TopicStatistics(Topic topic) { + this.topic = topic; + } + + public Long getId() { + return id; + } + + public Topic getTopic() { + return topic; + } + + public long getViews() { + return views; + } + + public void incrementViews() { + this.views++; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTableDiscriminatorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTableDiscriminatorTest.java new file mode 100644 index 000000000..c37e0142d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTableDiscriminatorTest.java @@ -0,0 +1,320 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JoinedTableDiscriminatorTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Board.class, + Topic.class, + Post.class, + Announcement.class, + TopicStatistics.class + }; + } + + @Test + public void test() { + Topic topic = doInJPA(entityManager -> { + Board board = new Board(); + board.setName("Hibernate"); + + entityManager.persist(board); + + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + post.setBoard(board); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + announcement.setBoard(board); + + entityManager.persist(announcement); + + TopicStatistics postStatistics = new TopicStatistics(post); + postStatistics.incrementViews(); + entityManager.persist(postStatistics); + + TopicStatistics announcementStatistics = new TopicStatistics(announcement); + announcementStatistics.incrementViews(); + entityManager.persist(announcementStatistics); + + return post; + }); + + doInJPA(entityManager -> { + Board board = topic.getBoard(); + LOGGER.info("Fetch Topics"); + List topics = entityManager + .createQuery("select t from Topic t where t.board = :board", Topic.class) + .setParameter("board", board) + .getResultList(); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetch Board topics"); + entityManager.find(Board.class, topic.getBoard().getId()).getTopics().size(); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetch Board topics eagerly"); + Long id = topic.getBoard().getId(); + Board board = entityManager.createQuery( + "select b from Board b join fetch b.topics where b.id = :id", Board.class) + .setParameter("id", id) + .getSingleResult(); + }); + + doInJPA(entityManager -> { + Long topicId = topic.getId(); + LOGGER.info("Fetch statistics"); + TopicStatistics statistics = entityManager + .createQuery("select s from TopicStatistics s join fetch s.topic t where t.id = :topicId", TopicStatistics.class) + .setParameter("topicId", topicId) + .getSingleResult(); + }); + } + + @Test + public void testQueryUsingAll() { + doInJPA(entityManager -> { + Board board1 = new Board(); + board1.setName("Hibernate"); + + entityManager.persist(board1); + + Post post1 = new Post(); + post1.setOwner("John Doe"); + post1.setTitle("Inheritance"); + post1.setContent("Best practices"); + post1.setBoard(board1); + + entityManager.persist(post1); + + Announcement announcement1 = new Announcement(); + announcement1.setOwner("John Doe"); + announcement1.setTitle("Release x.y.z.Final"); + announcement1.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + announcement1.setBoard(board1); + + entityManager.persist(announcement1); + + Board board2 = new Board(); + board2.setName("JPA"); + + entityManager.persist(board2); + + Post post2 = new Post(); + post2.setOwner("John Doe"); + post2.setTitle("Inheritance"); + post2.setContent("Best practices"); + post2.setBoard(board2); + + entityManager.persist(post2); + + Post post3 = new Post(); + post3.setOwner("John Doe"); + post3.setTitle("Inheritance"); + post3.setContent("More best practices"); + post3.setBoard(board2); + + entityManager.persist(post3); + }); + + doInJPA(entityManager -> { + List postOnlyBoards = entityManager + .createQuery( + "select b " + + "from Board b " + + "where Post = all (" + + " select type(t) from Topic t where t.board = b" + + ")", Board.class) + .getResultList(); + assertEquals(1, postOnlyBoards.size()); + assertEquals("JPA", postOnlyBoards.get(0).getName()); + }); + } + + @Entity(name = "Board") + @Table(name = "board") + public static class Board { + + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(mappedBy = "board") + private List topics = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getTopics() { + return topics; + } + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.JOINED) + @DiscriminatorColumn(name="class_type") + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + @ManyToOne(fetch = FetchType.LAZY) + private Board board; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public Board getBoard() { + return board; + } + + public void setBoard(Board board) { + this.board = board; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + @Table(name = "announcement") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } + + @Entity(name = "TopicStatistics") + @Table(name = "topic_statistics") + public static class TopicStatistics { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Topic topic; + + private long views; + + public TopicStatistics() {} + + public TopicStatistics(Topic topic) { + this.topic = topic; + } + + public Long getId() { + return id; + } + + public Topic getTopic() { + return topic; + } + + public long getViews() { + return views; + } + + public void incrementViews() { + this.views++; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTablePrimaryKeyJoinColumnTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTablePrimaryKeyJoinColumnTest.java new file mode 100644 index 000000000..791824623 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTablePrimaryKeyJoinColumnTest.java @@ -0,0 +1,277 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class JoinedTablePrimaryKeyJoinColumnTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Board.class, + Topic.class, + Post.class, + Announcement.class, + TopicStatistics.class + }; + } + + @Test + public void test() { + Topic topic = doInJPA(entityManager -> { + Board board = new Board(); + board.setName("Hibernate"); + + entityManager.persist(board); + + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + post.setBoard(board); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + announcement.setBoard(board); + + entityManager.persist(announcement); + + TopicStatistics postStatistics = new TopicStatistics(post); + postStatistics.incrementViews(); + entityManager.persist(postStatistics); + + TopicStatistics announcementStatistics = new TopicStatistics(announcement); + announcementStatistics.incrementViews(); + entityManager.persist(announcementStatistics); + + return post; + }); + + doInJPA(entityManager -> { + Board board = topic.getBoard(); + LOGGER.info("Fetch Topics"); + List topics = entityManager + .createQuery("select t from Topic t where t.board = :board", Topic.class) + .setParameter("board", board) + .getResultList(); + }); + + doInJPA(entityManager -> { + Board board = topic.getBoard(); + LOGGER.info("Fetch Topic projection"); + List titles = entityManager + .createQuery("select t.title from Topic t where t.board = :board", String.class) + .setParameter("board", board) + .getResultList(); + assertEquals(2, titles.size()); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetch just one Topic"); + Topic _topic = entityManager.find(Topic.class, topic.getId()); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetch Board topics"); + entityManager.find(Board.class, topic.getBoard().getId()).getTopics().size(); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetch Board topics eagerly"); + Long id = topic.getBoard().getId(); + Board board = entityManager.createQuery( + "select b from Board b join fetch b.topics where b.id = :id", Board.class) + .setParameter("id", id) + .getSingleResult(); + }); + + doInJPA(entityManager -> { + Long topicId = topic.getId(); + LOGGER.info("Fetch statistics"); + TopicStatistics statistics = entityManager + .createQuery("select s from TopicStatistics s join fetch s.topic t where t.id = :topicId", TopicStatistics.class) + .setParameter("topicId", topicId) + .getSingleResult(); + }); + } + + @Entity(name = "Board") + @Table(name = "board") + public static class Board { + + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(mappedBy = "board") + private List topics = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getTopics() { + return topics; + } + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.JOINED) + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + @ManyToOne(fetch = FetchType.LAZY) + private Board board; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public Board getBoard() { + return board; + } + + public void setBoard(Board board) { + this.board = board; + } + } + + @Entity(name = "Post") + @Table(name = "post") + @PrimaryKeyJoinColumn(name = "topic_id", referencedColumnName = "id") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + @Table(name = "announcement") + @PrimaryKeyJoinColumn(name = "topic_id") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } + + @Entity(name = "TopicStatistics") + @Table(name = "topic_statistics") + public static class TopicStatistics { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Topic topic; + + private long views; + + public TopicStatistics() {} + + public TopicStatistics(Topic topic) { + this.topic = topic; + } + + public Long getId() { + return id; + } + + public Topic getTopic() { + return topic; + } + + public long getViews() { + return views; + } + + public void incrementViews() { + this.views++; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTableTest.java new file mode 100644 index 000000000..0a380444b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/JoinedTableTest.java @@ -0,0 +1,495 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class JoinedTableTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Board.class, + Topic.class, + Post.class, + Announcement.class, + TopicStatistics.class + }; + } + + @Test + public void test() { + Topic topic = doInJPA(entityManager -> { + Board board = new Board() + .setId(1L) + .setName("Hibernate"); + + entityManager.persist(board); + + Post post = new Post() + .setOwner("Vlad Mihalcea") + .setTitle("High-Performance Java Persistence") + .setContent("Best practices") + .setBoard(board); + + entityManager.persist(post); + + Announcement announcement = new Announcement() + .setOwner("Vlad Mihalcea") + .setTitle("Release 1.2.3") + .setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))) + .setBoard(board); + + entityManager.persist(announcement); + + entityManager.persist( + new TopicStatistics() + .setTopic(post) + .incrementViews() + ); + + entityManager.persist( + new TopicStatistics() + .setTopic(announcement) + .incrementViews() + ); + + return post; + }); + + doInJPA(entityManager -> { + Board board = topic.getBoard(); + LOGGER.info("Fetch Topics"); + List topics = entityManager.createQuery(""" + select t + from Topic t + where t.board = :board + """, Topic.class) + .setParameter("board", board) + .getResultList(); + }); + + doInJPA(entityManager -> { + List statistics = entityManager.createQuery(""" + select s + from TopicStatistics s + join fetch s.topic t + """, TopicStatistics.class) + .getResultList(); + + assertEquals(2, statistics.size()); + }); + + doInJPA(entityManager -> { + Board board = topic.getBoard(); + LOGGER.info("Fetch Topic projection"); + List titles = entityManager.createQuery(""" + select t.title + from Topic t + where t.board = :board + """, String.class) + .setParameter("board", board) + .getResultList(); + assertEquals(2, titles.size()); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetch just one Topic"); + Topic _topic = entityManager.find(Topic.class, topic.getId()); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetch Board topics"); + entityManager.find(Board.class, topic.getBoard().getId()).getTopics().size(); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetch Board topics eagerly"); + Long id = topic.getBoard().getId(); + Board board = entityManager.createQuery(""" + select b + from Board b + join fetch b.topics + where b.id = :id + """, Board.class) + .setParameter("id", 1L) + .getSingleResult(); + + assertTrue(board.getTopics().stream().anyMatch(t -> t instanceof Post)); + assertTrue(board.getTopics().stream().anyMatch(t -> t instanceof Announcement)); + }); + + + + doInJPA(entityManager -> { + Long topicId = topic.getId(); + LOGGER.info("Fetch statistics"); + TopicStatistics statistics = entityManager.createQuery(""" + select s + from TopicStatistics s + join fetch s.topic t + where t.id = :topicId + """, TopicStatistics.class) + .setParameter("topicId", topicId) + .getSingleResult(); + }); + + TopicStatistics statistics = doInJPA(entityManager -> { + Long topicId = topic.getId(); + LOGGER.info("Fetch one TopicStatistic"); + return entityManager.find(TopicStatistics.class, topicId); + }); + + try { + statistics.getTopic().getCreatedOn(); + } + catch (Exception expected) { + LOGGER.info( "Topic was not fetched" ); + } + + doInJPA(entityManager -> { + + List results = entityManager.createQuery(""" + select count(t), t.class + from Topic t + group by t.class + order by t.class + """) + .getResultList(); + + assertEquals(2, results.size()); + }); + + doInJPA(entityManager -> { + Board board = topic.getBoard(); + + List topics = entityManager.createQuery(""" + select t + from Topic t + where t.board = :board + order by t.class + """, Topic.class) + .setParameter("board", board) + .getResultList(); + + assertEquals(2, topics.size()); + assertTrue(topics.get(0) instanceof Post); + assertTrue(topics.get(1) instanceof Announcement); + }); + + doInJPA(entityManager -> { + Board board = topic.getBoard(); + + List topics = entityManager.createQuery(""" + select t + from Topic t + where t.board = :board + order by + case + when type(t) = Announcement then 10 + when type(t) = Post then 20 + end + """, Topic.class) + .setParameter("board", board) + .getResultList(); + + assertEquals(2, topics.size()); + assertTrue(topics.get(0) instanceof Announcement); + assertTrue(topics.get(1) instanceof Post); + }); + + doInJPA(entityManager -> { + Long boardId = topic.getBoard().getId(); + LOGGER.info("Fetch Board topics"); + + Board board = entityManager.find(Board.class, boardId); + + List topics = board.getTopics(); + + assertTrue(topics.stream().anyMatch(t -> t instanceof Post)); + assertTrue(topics.stream().anyMatch(t -> t instanceof Announcement)); + }); + } + + @Test + public void testQueryUsingAll() { + doInJPA(entityManager -> { + Board board1 = new Board(); + board1.setId(1L); + board1.setName("Hibernate"); + + entityManager.persist(board1); + + Post post1 = new Post(); + post1.setOwner("John Doe"); + post1.setTitle("Inheritance"); + post1.setContent("Best practices"); + post1.setBoard(board1); + + entityManager.persist(post1); + + Announcement announcement1 = new Announcement(); + announcement1.setOwner("John Doe"); + announcement1.setTitle("Release x.y.z.Final"); + announcement1.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + announcement1.setBoard(board1); + + entityManager.persist(announcement1); + + Board board2 = new Board(); + board2.setId(2L); + board2.setName("JPA"); + + entityManager.persist(board2); + + Post post2 = new Post(); + post2.setOwner("John Doe"); + post2.setTitle("Inheritance"); + post2.setContent("Best practices"); + post2.setBoard(board2); + + entityManager.persist(post2); + + Post post3 = new Post(); + post3.setOwner("John Doe"); + post3.setTitle("Inheritance"); + post3.setContent("More best practices"); + post3.setBoard(board2); + + entityManager.persist(post3); + }); + + doInJPA(entityManager -> { + List postOnlyBoards = entityManager.createQuery(""" + select b + from Board b + where not exists ( + select 1 + from Topic t + where t.board = b and Post != type(t) + ) + """, Board.class) + .getResultList(); + assertEquals(1, postOnlyBoards.size()); + assertEquals("JPA", postOnlyBoards.get(0).getName()); + }); + } + + @Test + public void testBatching() { + doInJPA(entityManager -> { + Board board1 = new Board(); + board1.setId(1L); + board1.setName("Hibernate"); + + entityManager.persist(board1); + + for (int i = 0; i < 10; i++) { + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + post.setBoard(board1); + + entityManager.persist(post); + } + + LOGGER.info("Before flush"); + }); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Entity(name = "Board") + @Table(name = "board") + public static class Board { + + @Id + private Long id; + + private String name; + + @OneToMany(mappedBy = "board") + private List topics = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Board setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Board setName(String name) { + this.name = name; + return this; + } + + public List getTopics() { + return topics; + } + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.JOINED) + public static class Topic> { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + @ManyToOne(fetch = FetchType.LAZY) + private Board board; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public T setTitle(String title) { + this.title = title; + return (T) this; + } + + public String getOwner() { + return owner; + } + + public T setOwner(String owner) { + this.owner = owner; + return (T) this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public T setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return (T) this; + } + + public Board getBoard() { + return board; + } + + public T setBoard(Board board) { + this.board = board; + return (T) this; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public Post setContent(String content) { + this.content = content; + return this; + } + } + + @Entity(name = "Announcement") + @Table(name = "announcement") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public Announcement setValidUntil(Date validUntil) { + this.validUntil = validUntil; + return this; + } + } + + @Entity(name = "TopicStatistics") + @Table(name = "topic_statistics") + public static class TopicStatistics { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Topic topic; + + private long views; + + public Long getId() { + return id; + } + + public TopicStatistics setId(Long id) { + this.id = id; + return this; + } + + public Topic getTopic() { + return topic; + } + + public TopicStatistics setTopic(Topic topic) { + this.topic = topic; + return this; + } + + public long getViews() { + return views; + } + + public TopicStatistics incrementViews() { + this.views++; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/MappedSuperclassTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/MappedSuperclassTest.java new file mode 100644 index 000000000..14361f66b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/MappedSuperclassTest.java @@ -0,0 +1,291 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MappedSuperclassTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Board.class, + Post.class, + Announcement.class, + PostStatistics.class, + AnnouncementStatistics.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void test() { + Topic topic = doInJPA(entityManager -> { + Board board = new Board() + .setId(1L) + .setName("Hibernate"); + + entityManager.persist(board); + + Post post = new Post() + .setOwner("Vlad Mihalcea") + .setTitle("High-Performance Java Persistence") + .setContent("Best practices") + .setBoard(board); + + entityManager.persist(post); + + Announcement announcement = new Announcement() + .setOwner("Vlad Mihalcea") + .setTitle("Release 1.2.3") + .setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))) + .setBoard(board); + + entityManager.persist(announcement); + + entityManager.persist( + new PostStatistics() + .setTopic(post) + .incrementViews() + ); + + entityManager.persist( + new AnnouncementStatistics() + .setTopic(announcement) + .incrementViews() + ); + + return post; + }); + + doInJPA(entityManager -> { + Board board = topic.getBoard(); + + List posts = entityManager.createQuery(""" + select p + from Post p + where p.board = :board + """, Post.class) + .setParameter("board", board) + .getResultList(); + }); + + doInJPA(entityManager -> { + Long postId = topic.getId(); + LOGGER.info("Fetch statistics"); + PostStatistics postStatistics = entityManager.createQuery(""" + select ps + from PostStatistics ps + join fetch ps.topic t + where t.id = :postId + """, PostStatistics.class) + .setParameter("postId", postId) + .getSingleResult(); + + assertEquals(postId, postStatistics.getTopic().getId()); + }); + } + + @Entity(name = "Board") + @Table(name = "board") + public static class Board { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Board setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Board setName(String name) { + this.name = name; + return this; + } + } + + @MappedSuperclass + public static abstract class Topic> { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + @ManyToOne(fetch = FetchType.LAZY) + private Board board; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public T setTitle(String title) { + this.title = title; + return (T) this; + } + + public String getOwner() { + return owner; + } + + public T setOwner(String owner) { + this.owner = owner; + return (T) this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public T setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return (T) this; + } + + public Board getBoard() { + return board; + } + + public T setBoard(Board board) { + this.board = board; + return (T) this; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public Post setContent(String content) { + this.content = content; + return this; + } + } + + @Entity(name = "Announcement") + @Table(name = "announcement") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public Announcement setValidUntil(Date validUntil) { + this.validUntil = validUntil; + return this; + } + } + + @MappedSuperclass + public abstract static class TopicStatistics { + + @Id + private Long id; + + private long views; + + public Long getId() { + return id; + } + + public abstract T getTopic(); + + public long getViews() { + return views; + } + + public TopicStatistics incrementViews() { + this.views++; + return this; + } + } + + @Entity(name = "PostStatistics") + @Table(name = "post_statistics") + public static class PostStatistics extends TopicStatistics { + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Post topic; + + @Override + public Post getTopic() { + return topic; + } + + public PostStatistics setTopic(Post topic) { + this.topic = topic; + return this; + } + } + + @Entity(name = "AnnouncementStatistics") + @Table(name = "announcement_statistics") + public static class AnnouncementStatistics extends TopicStatistics { + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Announcement topic; + + @Override + public Announcement getTopic() { + return topic; + } + + public AnnouncementStatistics setTopic(Announcement topic) { + this.topic = topic; + return this; + } + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/SingleTableCheckConstraintTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/SingleTableCheckConstraintTest.java new file mode 100644 index 000000000..c640b28c9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/SingleTableCheckConstraintTest.java @@ -0,0 +1,280 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.junit.Test; + +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class SingleTableCheckConstraintTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Board.class, + Topic.class, + Post.class, + Announcement.class, + TopicStatistics.class + }; + } + + @Override + protected Database database() { + //return Database.POSTGRESQL; + //Since MySQL 8.0.16 + return Database.MYSQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement st = connection.createStatement()) { + st.executeUpdate(""" + ALTER TABLE topic + ADD CONSTRAINT post_content_check CHECK + ( + CASE + WHEN DTYPE = 'Post' THEN + CASE + WHEN content IS NOT NULL + THEN 1 + ELSE 0 + END + ELSE 1 + END = 1 + ) + """ + ); + st.executeUpdate(""" + ALTER TABLE topic + ADD CONSTRAINT announcement_validUntil_check CHECK + ( + CASE + WHEN DTYPE = 'Announcement' THEN + CASE + WHEN validUntil IS NOT NULL + THEN 1 + ELSE 0 + END + ELSE 1 + END = 1 + ) + """ + ); + } + }); + + Board board = new Board(); + board.setName("Hibernate"); + + entityManager.persist(board); + + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + post.setBoard(board); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + announcement.setBoard(board); + + entityManager.persist(announcement); + }); + + try { + doInJPA(entityManager -> { + entityManager.persist(new Post()); + }); + fail("content_check should fail"); + } catch (Exception expected) { + LOGGER.info("Constraint violation", expected); + } + + try { + doInJPA(entityManager -> { + entityManager.persist(new Announcement()); + }); + fail("announcement_validUntil_check should fail"); + } catch (Exception expected) { + LOGGER.info("Constraint violation", expected); + } + } + + @Entity(name = "Board") + @Table(name = "board") + public static class Board { + + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(mappedBy = "board") + private List topics = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getTopics() { + return topics; + } + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + @ManyToOne(fetch = FetchType.LAZY) + private Board board; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public Board getBoard() { + return board; + } + + public void setBoard(Board board) { + this.board = board; + } + } + + @Entity(name = "Post") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } + + @Entity(name = "TopicStatistics") + @Table(name = "topic_statistics") + public static class TopicStatistics { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Topic topic; + + private long views; + + public TopicStatistics() {} + + public TopicStatistics(Topic topic) { + this.topic = topic; + } + + public Long getId() { + return id; + } + + public Topic getTopic() { + return topic; + } + + public long getViews() { + return views; + } + + public void incrementViews() { + this.views++; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/SingleTableMySQLTriggerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/SingleTableMySQLTriggerTest.java new file mode 100644 index 000000000..75f8d636b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/SingleTableMySQLTriggerTest.java @@ -0,0 +1,341 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.junit.Test; + +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class SingleTableMySQLTriggerTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Board.class, + Topic.class, + Post.class, + Announcement.class, + TopicStatistics.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement st = connection.createStatement()) { + st.executeUpdate(""" + CREATE + TRIGGER post_content_insert_check BEFORE INSERT + ON Topic + FOR EACH ROW + BEGIN + IF NEW.DTYPE = 'Post' + THEN + IF NEW.content IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Post content cannot be NULL'; + END IF; + END IF; + END; + """ + ); + st.executeUpdate(""" + CREATE + TRIGGER post_content_update_check BEFORE UPDATE + ON Topic + FOR EACH ROW + BEGIN + IF NEW.DTYPE = 'Post' + THEN + IF NEW.content IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Post content cannot be NULL'; + END IF; + END IF; + END; + """ + ); + st.executeUpdate(""" + CREATE + TRIGGER announcement_validUntil_insert_check BEFORE INSERT + ON Topic + FOR EACH ROW + BEGIN + IF NEW.DTYPE = 'Announcement' + THEN + IF NEW.validUntil IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Announcement validUntil cannot be NULL'; + END IF; + END IF; + END; + """ + ); + st.executeUpdate(""" + CREATE + TRIGGER announcement_validUntil_update_check BEFORE UPDATE + ON Topic + FOR EACH ROW + BEGIN + IF NEW.DTYPE = 'Announcement' + THEN + IF NEW.validUntil IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Announcement validUntil cannot be NULL'; + END IF; + END IF; + END; + """ + ); + } + }); + + Board board = new Board(); + board.setName("Hibernate"); + + entityManager.persist(board); + + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + post.setBoard(board); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + announcement.setBoard(board); + + entityManager.persist(announcement); + }); + + try { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + where p.content = :content + """, Post.class) + .setParameter("content", "Best practices") + .getSingleResult(); + + post.setContent(null); + }); + fail("content_check should fail"); + } catch (Exception expected) { + LOGGER.info("Constraint violation", expected); + } + + try { + doInJPA(entityManager -> { + Announcement announcement = entityManager.createQuery("select a from Announcement a", Announcement.class).getSingleResult(); + announcement.setValidUntil(null); + }); + fail("valid_until_check should fail"); + } catch (Exception expected) { + LOGGER.info("Constraint violation", expected); + } + + try { + doInJPA(entityManager -> { + entityManager.persist(new Post()); + }); + fail("content_check should fail"); + } catch (Exception expected) { + LOGGER.info("Constraint violation", expected); + } + + try { + doInJPA(entityManager -> { + entityManager.persist(new Announcement()); + }); + fail("content_check should fail"); + } catch (Exception expected) { + LOGGER.info("Constraint violation", expected); + } + } + + @Entity(name = "Board") + @Table(name = "board") + public static class Board { + + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(mappedBy = "board") + private List topics = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getTopics() { + return topics; + } + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + @ManyToOne(fetch = FetchType.LAZY) + private Board board; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public Board getBoard() { + return board; + } + + public void setBoard(Board board) { + this.board = board; + } + } + + @Entity(name = "Post") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } + + @Entity(name = "TopicStatistics") + @Table(name = "topic_statistics") + public static class TopicStatistics { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Topic topic; + + private long views; + + public TopicStatistics() {} + + public TopicStatistics(Topic topic) { + this.topic = topic; + } + + public Long getId() { + return id; + } + + public Topic getTopic() { + return topic; + } + + public long getViews() { + return views; + } + + public void incrementViews() { + this.views++; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/SingleTableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/SingleTableTest.java new file mode 100644 index 000000000..8792bb159 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/SingleTableTest.java @@ -0,0 +1,327 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class SingleTableTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Board.class, + Topic.class, + Post.class, + Announcement.class, + TopicStatistics.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Board board = new Board() + .setId(1L) + .setName("Hibernate"); + + entityManager.persist(board); + + Post post = new Post() + .setOwner("Vlad Mihalcea") + .setTitle("High-Performance Java Persistence") + .setContent("Best practices") + .setBoard(board); + + entityManager.persist(post); + + Announcement announcement = new Announcement() + .setOwner("Vlad Mihalcea") + .setTitle("Release 1.2.3") + .setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))) + .setBoard(board); + + entityManager.persist(announcement); + + entityManager.persist( + new TopicStatistics() + .setTopic(post) + .incrementViews() + ); + + entityManager.persist( + new TopicStatistics() + .setTopic(announcement) + .incrementViews() + ); + }); + } + + @Test + public void testPolymorphicQuery() { + doInJPA(entityManager -> { + Board board = entityManager.getReference(Board.class, 1L); + + List topics = entityManager.createQuery(""" + select t + from Topic t + where t.board = :board + """, Topic.class) + .setParameter("board", board) + .getResultList(); + + assertEquals(2, topics.size()); + }); + } + + @Test + public void testSubclassQuery() { + doInJPA(entityManager -> { + Board board = entityManager.getReference(Board.class, 1L); + + List posts = entityManager.createQuery(""" + select p + from Post p + where p.board = :board + """, Post.class) + .setParameter("board", board) + .getResultList(); + + assertEquals(1, posts.size()); + + return posts.get(0); + }); + } + + @Test + public void testPolymorphicAssociation() { + doInJPA(entityManager -> { + Board board = entityManager.createQuery(""" + select b + from Board b + join fetch b.topics + where b.id = :id + """, Board.class) + .setParameter("id", 1L) + .getSingleResult(); + + assertEquals(2, board.getTopics().size()); + }); + + doInJPA(entityManager -> { + List statistics = entityManager.createQuery(""" + select s + from TopicStatistics s + join fetch s.topic t + """, TopicStatistics.class) + .getResultList(); + + assertEquals(2, statistics.size()); + }); + } + + @Test + public void testOrderByClassType() { + doInJPA(entityManager -> { + Board board = entityManager.getReference(Board.class, 1L); + + List topics = entityManager.createQuery(""" + select t + from Topic t + where t.board = :board + order by t.class + """, Topic.class) + .setParameter("board", board) + .getResultList(); + + assertEquals(2, topics.size()); + + assertTrue(topics.get(0) instanceof Announcement); + assertTrue(topics.get(1) instanceof Post); + }); + } + + @Entity(name = "Board") + @Table(name = "board") + public static class Board { + + @Id + private Long id; + + private String name; + + @OneToMany(mappedBy = "board") + private List topics = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Board setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Board setName(String name) { + this.name = name; + return this; + } + + public List getTopics() { + return topics; + } + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance + public static class Topic> { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + @ManyToOne(fetch = FetchType.LAZY) + private Board board; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public T setTitle(String title) { + this.title = title; + return (T) this; + } + + public String getOwner() { + return owner; + } + + public T setOwner(String owner) { + this.owner = owner; + return (T) this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public T setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return (T) this; + } + + public Board getBoard() { + return board; + } + + public T setBoard(Board board) { + this.board = board; + return (T) this; + } + } + + @Entity(name = "Post") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public Post setContent(String content) { + this.content = content; + return this; + } + } + + @Entity(name = "Announcement") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public Announcement setValidUntil(Date validUntil) { + this.validUntil = validUntil; + return this; + } + } + + @Entity(name = "TopicStatistics") + @Table(name = "topic_statistics") + public static class TopicStatistics { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Topic topic; + + private long views; + + public Long getId() { + return id; + } + + public TopicStatistics setId(Long id) { + this.id = id; + return this; + } + + public Topic getTopic() { + return topic; + } + + public TopicStatistics setTopic(Topic topic) { + this.topic = topic; + return this; + } + + public long getViews() { + return views; + } + + public TopicStatistics incrementViews() { + this.views++; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/SortIndexCollectionInheritance.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/SortIndexCollectionInheritance.java similarity index 94% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/SortIndexCollectionInheritance.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/SortIndexCollectionInheritance.java index 3e8386e2c..3a5caf71b 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/SortIndexCollectionInheritance.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/SortIndexCollectionInheritance.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance; +package com.vladmihalcea.hpjp.hibernate.inheritance; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/TablePerClassMySQLUnionAllTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/TablePerClassMySQLUnionAllTest.java new file mode 100644 index 000000000..119c22e35 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/TablePerClassMySQLUnionAllTest.java @@ -0,0 +1,28 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; +import org.hibernate.dialect.MySQL8Dialect; + +/** + * @author Vlad Mihalcea + */ +public class TablePerClassMySQLUnionAllTest extends TablePerClassTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new MySQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return MySQLUnionAllSupportDialect.class.getName(); + } + }; + } + + public static class MySQLUnionAllSupportDialect extends MySQL8Dialect { + @Override + public boolean supportsUnionAll() { + return true; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/TablePerClassMySQLUnionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/TablePerClassMySQLUnionTest.java new file mode 100644 index 000000000..e025f696b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/TablePerClassMySQLUnionTest.java @@ -0,0 +1,28 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; +import org.hibernate.dialect.MySQL8Dialect; + +/** + * @author Vlad Mihalcea + */ +public class TablePerClassMySQLUnionTest extends TablePerClassTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new MySQLDataSourceProvider() { + @Override + public String hibernateDialect() { + return MySQLUnionSupportDialect.class.getName(); + } + }; + } + + public static class MySQLUnionSupportDialect extends MySQL8Dialect { + @Override + public boolean supportsUnionAll() { + return false; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/TablePerClassTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/TablePerClassTest.java new file mode 100644 index 000000000..1a8e098c3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/TablePerClassTest.java @@ -0,0 +1,308 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class TablePerClassTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Board.class, + Topic.class, + Post.class, + Announcement.class, + TopicStatistics.class + }; + } + + @Override + protected Database database() { + // When using MySQL with older Hibernate versions, + // UNION is used instead of UNION ALL + // return Database.MYSQL; + return Database.POSTGRESQL; + } + + @Test + public void test() { + Topic topic = doInJPA(entityManager -> { + Board board = new Board() + .setId(1L) + .setName("Hibernate"); + + entityManager.persist(board); + + Post post = new Post() + .setOwner("Vlad Mihalcea") + .setTitle("High-Performance Java Persistence") + .setContent("Best practices") + .setBoard(board); + + entityManager.persist(post); + + Announcement announcement = new Announcement() + .setOwner("Vlad Mihalcea") + .setTitle("Release 1.2.3") + .setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))) + .setBoard(board); + + entityManager.persist(announcement); + + entityManager.persist( + new TopicStatistics() + .setTopic(post) + .incrementViews() + ); + + entityManager.persist( + new TopicStatistics() + .setTopic(announcement) + .incrementViews() + ); + + return post; + }); + + doInJPA(entityManager -> { + Board board = topic.getBoard(); + LOGGER.info("Fetch Topics"); + List topics = entityManager.createQuery(""" + select t + from Topic t + where t.board = :board + """, Topic.class) + .setParameter("board", board) + .getResultList(); + }); + + doInJPA(entityManager -> { + List statistics = entityManager.createQuery(""" + select s + from TopicStatistics s + join fetch s.topic t + """, TopicStatistics.class) + .getResultList(); + + assertEquals(2, statistics.size()); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetch Board topics"); + entityManager.find(Board.class, topic.getBoard().getId()).getTopics().size(); + }); + + doInJPA(entityManager -> { + LOGGER.info("Fetch Board topics eagerly"); + Long id = topic.getBoard().getId(); + Board board = entityManager.createQuery(""" + select b + from Board b + join fetch b.topics + where b.id = :id + """, Board.class) + .setParameter("id", id) + .getSingleResult(); + }); + + doInJPA(entityManager -> { + Long topicId = topic.getId(); + LOGGER.info("Fetch statistics"); + TopicStatistics statistics = entityManager.createQuery(""" + select s + from TopicStatistics s + join fetch s.topic t + where t.id = :topicId + """, TopicStatistics.class) + .setParameter("topicId", topicId) + .getSingleResult(); + }); + } + + @Entity(name = "Board") + @Table(name = "board") + public static class Board { + + @Id + private Long id; + + private String name; + + @OneToMany(mappedBy = "board") + private List topics = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Board setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Board setName(String name) { + this.name = name; + return this; + } + + public List getTopics() { + return topics; + } + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) + public static class Topic> { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + @ManyToOne(fetch = FetchType.LAZY) + private Board board; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public T setTitle(String title) { + this.title = title; + return (T) this; + } + + public String getOwner() { + return owner; + } + + public T setOwner(String owner) { + this.owner = owner; + return (T) this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public T setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return (T) this; + } + + public Board getBoard() { + return board; + } + + public T setBoard(Board board) { + this.board = board; + return (T) this; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public Post setContent(String content) { + this.content = content; + return this; + } + } + + @Entity(name = "Announcement") + @Table(name = "announcement") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public Announcement setValidUntil(Date validUntil) { + this.validUntil = validUntil; + return this; + } + } + + @Entity(name = "TopicStatistics") + @Table(name = "topic_statistics") + public static class TopicStatistics { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Topic topic; + + private long views; + + public Long getId() { + return id; + } + + public TopicStatistics setId(Long id) { + this.id = id; + return this; + } + + public Topic getTopic() { + return topic; + } + + public TopicStatistics setTopic(Topic topic) { + this.topic = topic; + return this; + } + + public long getViews() { + return views; + } + + public TopicStatistics incrementViews() { + this.views++; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/CharDiscriminatorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/CharDiscriminatorTest.java new file mode 100644 index 000000000..e066396c3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/CharDiscriminatorTest.java @@ -0,0 +1,189 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.discriminator; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class CharDiscriminatorTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Topic.class, + Post.class, + Announcement.class + }; + } + + @Override + protected void afterInit() { + doInJPA(this::addConsistencyTriggers); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + + entityManager.persist(announcement); + }); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + @DiscriminatorColumn( + discriminatorType = DiscriminatorType.CHAR, + name = "topic_type_id" + ) + @DiscriminatorValue("T") + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } + + private void addConsistencyTriggers(EntityManager entityManager) { + entityManager.unwrap(Session.class).doWork(connection -> { + try (Statement st = connection.createStatement()) { + st.executeUpdate(""" + CREATE + TRIGGER post_content_check BEFORE INSERT + ON Topic + FOR EACH ROW + BEGIN + IF NEW.topic_type_id = 'P' + THEN + IF NEW.content IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Post content cannot be NULL'; + END IF; + END IF; + END; + """ + ); + st.executeUpdate(""" + CREATE + TRIGGER announcement_validUntil_check BEFORE INSERT + ON Topic + FOR EACH ROW + BEGIN + IF NEW.topic_type_id = 'A' + THEN + IF NEW.validUntil IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Announcement validUntil cannot be NULL'; + END IF; + END IF; + END; + """ + ); + } + }); + } + + @Entity(name = "Post") + @DiscriminatorValue("P") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + @DiscriminatorValue("A") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/DefaultDiscriminatorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/DefaultDiscriminatorTest.java new file mode 100644 index 000000000..e14353ad1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/DefaultDiscriminatorTest.java @@ -0,0 +1,182 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.discriminator; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DefaultDiscriminatorTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Topic.class, + Post.class, + Announcement.class + }; + } + + @Override + protected void afterInit() { + doInJPA(this::addConsistencyTriggers); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + + entityManager.persist(announcement); + }); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + private void addConsistencyTriggers(EntityManager entityManager) { + entityManager.unwrap(Session.class).doWork(connection -> { + try (Statement st = connection.createStatement()) { + st.executeUpdate(""" + CREATE + TRIGGER post_content_check BEFORE INSERT + ON Topic + FOR EACH ROW + BEGIN + IF NEW.DTYPE = 'Post' + THEN + IF NEW.content IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Post content cannot be NULL'; + END IF; + END IF; + END; + """ + ); + st.executeUpdate(""" + CREATE + TRIGGER announcement_validUntil_check BEFORE INSERT + ON Topic + FOR EACH ROW + BEGIN + IF NEW.DTYPE = 'Announcement' + THEN + IF NEW.validUntil IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Announcement validUntil cannot be NULL'; + END IF; + END IF; + END; + """ + ); + } + }); + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } + + @Entity(name = "Post") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/DefaultIntegerDiscriminatorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/DefaultIntegerDiscriminatorTest.java new file mode 100644 index 000000000..849754f41 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/DefaultIntegerDiscriminatorTest.java @@ -0,0 +1,189 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.discriminator; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DefaultIntegerDiscriminatorTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Topic.class, + Post.class, + Announcement.class + }; + } + + @Override + protected void afterInit() { + doInJPA(this::addConsistencyTriggers); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + + entityManager.persist(announcement); + }); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + private void addConsistencyTriggers(EntityManager entityManager) { + entityManager.unwrap(Session.class).doWork(connection -> { + try (Statement st = connection.createStatement()) { + st.executeUpdate(""" + CREATE + TRIGGER post_content_check BEFORE INSERT + ON Topic + FOR EACH ROW + BEGIN + IF NEW.topic_type_id = 1 + THEN + IF NEW.content IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Post content cannot be NULL'; + END IF; + END IF; + END; + """ + ); + st.executeUpdate(""" + CREATE + TRIGGER announcement_validUntil_check BEFORE INSERT + ON Topic + FOR EACH ROW + BEGIN + IF NEW.topic_type_id = 2 + THEN + IF NEW.validUntil IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Announcement validUntil cannot be NULL'; + END IF; + END IF; + END; + """ + ); + } + }); + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + @DiscriminatorColumn( + discriminatorType = DiscriminatorType.INTEGER, + name = "topic_type_id" + ) + @DiscriminatorValue("0") + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } + + @Entity(name = "Post") + @DiscriminatorValue("1") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + @DiscriminatorValue("2") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/JoinedStringDiscriminatorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/JoinedStringDiscriminatorTest.java new file mode 100644 index 000000000..17a40cde4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/JoinedStringDiscriminatorTest.java @@ -0,0 +1,151 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.discriminator; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JoinedStringDiscriminatorTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Topic.class, + Post.class, + Announcement.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + + entityManager.persist(announcement); + }); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + assertEquals(1, posts.size()); + }); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select t + from Topic t + where type(t) = Post + """, Topic.class) + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.JOINED) + @DiscriminatorColumn( + discriminatorType = DiscriminatorType.STRING + ) + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + @Table(name = "announcement") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/JoinedStringWithoutDiscriminatorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/JoinedStringWithoutDiscriminatorTest.java new file mode 100644 index 000000000..83360d82d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/JoinedStringWithoutDiscriminatorTest.java @@ -0,0 +1,147 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.discriminator; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JoinedStringWithoutDiscriminatorTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Topic.class, + Post.class, + Announcement.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + + entityManager.persist(announcement); + }); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + assertEquals(1, posts.size()); + }); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select t + from Topic t + where type(t) = Post + """, Topic.class) + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.JOINED) + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + @Table(name = "announcement") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/MySQLIntegerDiscriminatorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/MySQLIntegerDiscriminatorTest.java new file mode 100644 index 000000000..fd76febea --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/MySQLIntegerDiscriminatorTest.java @@ -0,0 +1,199 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.discriminator; + +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; +import jakarta.persistence.*; + +import org.hibernate.Session; + +import org.junit.Test; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLIntegerDiscriminatorTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Topic.class, + Post.class, + Announcement.class + }; + } + + @Override + protected void afterInit() { + doInJPA(this::addConsistencyTriggers); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + + entityManager.persist(announcement); + }); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + private void addConsistencyTriggers(EntityManager entityManager) { + entityManager.unwrap(Session.class).doWork(connection -> { + try (Statement st = connection.createStatement()) { + st.executeUpdate(""" + CREATE + TRIGGER post_content_check BEFORE INSERT + ON Topic + FOR EACH ROW + BEGIN + IF NEW.topic_type_id = 1 + THEN + IF NEW.content IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Post content cannot be NULL'; + END IF; + END IF; + END; + """ + ); + st.executeUpdate(""" + CREATE + TRIGGER announcement_validUntil_check BEFORE INSERT + ON Topic + FOR EACH ROW + BEGIN + IF NEW.topic_type_id = 2 + THEN + IF NEW.validUntil IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Announcement validUntil cannot be NULL'; + END IF; + END IF; + END; + """ + ); + + st.executeUpdate(""" + ALTER TABLE topic + MODIFY COLUMN topic_type_id + TINYINT(1) NOT NULL COMMENT '0 - Topic, 1 - Post, 2 - Announcement' + """ + ); + } + }); + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + @DiscriminatorColumn( + discriminatorType = DiscriminatorType.INTEGER, + name = "topic_type_id", + columnDefinition = "TINYINT(1)" + ) + @DiscriminatorValue("0") + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } + + @Entity(name = "Post") + @DiscriminatorValue("1") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + @DiscriminatorValue("2") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/PostgreSQLIntegerDiscriminatorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/PostgreSQLIntegerDiscriminatorTest.java new file mode 100644 index 000000000..df029fe17 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/PostgreSQLIntegerDiscriminatorTest.java @@ -0,0 +1,188 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.discriminator; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.junit.Test; + +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLIntegerDiscriminatorTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Topic.class, + Post.class, + Announcement.class + }; + } + + @Override + protected void afterInit() { + doInJPA(this::addConsistencyCheck); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + + entityManager.persist(announcement); + }); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + private void addConsistencyCheck(EntityManager entityManager) { + entityManager.unwrap(Session.class).doWork(connection -> { + try (Statement st = connection.createStatement()) { + st.executeUpdate(""" + ALTER TABLE topic + ADD CONSTRAINT post_content_check CHECK + ( + CASE + WHEN topic_type_id = 1 THEN + CASE + WHEN content IS NOT NULL + THEN 1 + ELSE 0 + END + ELSE 1 + END = 1 + ) + """ + ); + st.executeUpdate(""" + ALTER TABLE topic + ADD CONSTRAINT announcement_validUntil_check CHECK + ( + CASE + WHEN topic_type_id = 2 THEN + CASE + WHEN validUntil IS NOT NULL + THEN 1 + ELSE 0 + END + ELSE 1 + END = 1 + ) + """ + ); + } + }); + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + @DiscriminatorColumn( + discriminatorType = DiscriminatorType.INTEGER, + name = "topic_type_id", + columnDefinition = "NUMERIC(1)" + ) + @DiscriminatorValue("0") + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } + + @Entity(name = "Post") + @DiscriminatorValue("1") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + @DiscriminatorValue("2") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/StringDiscriminatorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/StringDiscriminatorTest.java new file mode 100644 index 000000000..0c180fd4a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/StringDiscriminatorTest.java @@ -0,0 +1,190 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.discriminator; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class StringDiscriminatorTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Topic.class, + Post.class, + Announcement.class + }; + } + + @Override + protected void afterInit() { + doInJPA(this::addConsistencyTriggers); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + + entityManager.persist(announcement); + }); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + private void addConsistencyTriggers(EntityManager entityManager) { + entityManager.unwrap(Session.class).doWork(connection -> { + try (Statement st = connection.createStatement()) { + st.executeUpdate(""" + CREATE + TRIGGER post_content_check BEFORE INSERT + ON Topic + FOR EACH ROW + BEGIN + IF NEW.topic_type_id = 'PST' + THEN + IF NEW.content IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Post content cannot be NULL'; + END IF; + END IF; + END; + """ + ); + st.executeUpdate(""" + CREATE + TRIGGER announcement_validUntil_check BEFORE INSERT + ON Topic + FOR EACH ROW + BEGIN + IF NEW.topic_type_id = 'ANN' + THEN + IF NEW.validUntil IS NULL + THEN + signal sqlstate '45000' + set message_text = 'Announcement validUntil cannot be NULL'; + END IF; + END IF; + END; + """ + ); + } + }); + } + + @Entity(name = "Topic") + @Table(name = "topic") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + @DiscriminatorColumn( + discriminatorType = DiscriminatorType.STRING, + name = "topic_type_id", + columnDefinition = "VARCHAR(3)" + ) + @DiscriminatorValue("TPC") + public static class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } + + @Entity(name = "Post") + @DiscriminatorValue("PST") + public static class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + + @Entity(name = "Announcement") + @DiscriminatorValue("ANN") + public static class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/Announcement.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/Announcement.java new file mode 100644 index 000000000..9665e7170 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/Announcement.java @@ -0,0 +1,27 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.discriminator.description; + +import java.util.Date; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.Temporal; +import jakarta.persistence.TemporalType; + +/** + * @author Vlad Mihalcea + */ +@Entity +@DiscriminatorValue("2") +public class Announcement extends Topic { + + @Temporal(TemporalType.TIMESTAMP) + private Date validUntil; + + public Date getValidUntil() { + return validUntil; + } + + public void setValidUntil(Date validUntil) { + this.validUntil = validUntil; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/IntegerDiscriminatorDescriptionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/IntegerDiscriminatorDescriptionTest.java new file mode 100644 index 000000000..9da489f61 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/IntegerDiscriminatorDescriptionTest.java @@ -0,0 +1,119 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.discriminator.description; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Tuple; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class IntegerDiscriminatorDescriptionTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Topic.class, + Post.class, + Announcement.class, + TopicType.class, + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + for (Class entityClass : entities()) { + if (Topic.class.isAssignableFrom(entityClass)) { + + DiscriminatorValue discriminatorValue = (DiscriminatorValue) + entityClass.getAnnotation(DiscriminatorValue.class); + + TopicType topicType = new TopicType(); + topicType.setId(Byte.valueOf(discriminatorValue.value())); + topicType.setName(entityClass.getSimpleName()); + topicType.setDescription( + Topic.class.equals(entityClass) ? + "Topic is the base class of the Topic entity hierarchy" : + String.format( + "%s is a subclass of the Topic base class", + entityClass.getSimpleName() + ) + ); + + entityManager.persist(topicType); + } + } + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setOwner("John Doe"); + post.setTitle("Inheritance"); + post.setContent("Best practices"); + + entityManager.persist(post); + + Announcement announcement = new Announcement(); + announcement.setOwner("John Doe"); + announcement.setTitle("Release x.y.z.Final"); + announcement.setValidUntil(Timestamp.valueOf(LocalDateTime.now().plusMonths(1))); + + entityManager.persist(announcement); + }); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + assertEquals(1, posts.size()); + + List results = entityManager.createNativeQuery(""" + SELECT + t.*, + CAST(tt.id AS SIGNED) AS "discriminator", + tt.name AS "type_name", + tt.description AS "type_description" + FROM topic t + INNER JOIN topic_type tt ON t.topic_type_id = tt.id + WHERE t.content IS NOT NULL + """, Tuple.class) + .getResultList(); + + assertEquals(1, results.size()); + + Tuple postTuple = results.get(0); + + assertEquals( + "Best practices", + postTuple.get("content") + ); + assertEquals( + 1, + ((Number) postTuple.get("discriminator")).intValue() + ); + assertEquals( + Post.class.getSimpleName(), + postTuple.get("type_name") + ); + assertEquals( + "Post is a subclass of the Topic base class", + postTuple.get("type_description") + ); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/Post.java new file mode 100644 index 000000000..c612c8ee1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/Post.java @@ -0,0 +1,23 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.discriminator.description; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity +@DiscriminatorValue("1") +public class Post extends Topic { + + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/Topic.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/Topic.java new file mode 100644 index 000000000..c257252c3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/Topic.java @@ -0,0 +1,74 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.discriminator.description; + +import java.util.Date; +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "topic") +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn( + discriminatorType = DiscriminatorType.INTEGER, + name = "topic_type_id", + columnDefinition = "TINYINT(1)" +) +@DiscriminatorValue("0") +public class Topic { + + @Id + @GeneratedValue + private Long id; + + private String title; + + private String owner; + + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn = new Date(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn( + name = "topic_type_id", + insertable = false, + updatable = false + ) + private TopicType type; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public TopicType getType() { + return type; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/TopicType.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/TopicType.java similarity index 77% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/TopicType.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/TopicType.java index 0798d6bd6..3db29c573 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/inheritance/discriminator/TopicType.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/discriminator/description/TopicType.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.inheritance.discriminator; +package com.vladmihalcea.hpjp.hibernate.inheritance.discriminator.description; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/embeddable/EmbeddedInheritanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/embeddable/EmbeddedInheritanceTest.java new file mode 100644 index 000000000..ce1a466b4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/embeddable/EmbeddedInheritanceTest.java @@ -0,0 +1,205 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.embeddable; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.junit.Test; + +import java.io.Serializable; +import java.util.*; + +import static java.util.stream.Collectors.groupingBy; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class EmbeddedInheritanceTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Subscriber.class, + Subscription.class, + EmailSubscription.class, + SmsSubscription.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Subscriber() + .setId(1L) + .setFirstName("Vlad") + .setLastName("Mihalcea") + .addSubscription( + new EmailSubscription() + .setOptIn(true) + .setEmailAddress("vm@acme.com") + ) + .addSubscription( + new SmsSubscription() + .setOptIn(true) + .setPhoneNumber(123_456_7890L) + ) + ); + }); + + doInJPA(entityManager -> { + Subscriber subscriber = entityManager.createQuery(""" + select s + from Subscriber s + left join fetch s.subscriptions + where s.id =:id + """, Subscriber.class) + .setParameter("id", 1L) + .getSingleResult(); + + assertEquals(2, subscriber.getSubscriptions().size()); + Map> subscriptionMap = subscriber + .getSubscriptions() + .stream() + .collect(groupingBy(Object::getClass)); + + EmailSubscription emailSubscription = (EmailSubscription) + subscriptionMap.get(EmailSubscription.class).get(0); + assertEquals( + "vm@acme.com", emailSubscription.getEmailAddress() + ); + + SmsSubscription smsSubscription = (SmsSubscription) + subscriptionMap.get(SmsSubscription.class).get(0); + assertEquals( + 123_456_7890L, smsSubscription.getPhoneNumber().longValue() + ); + }); + } + + @Entity(name = "Subscriber") + @Table(name = "subscriber") + public static class Subscriber { + + @Id + private Long id; + + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + @Temporal(TemporalType.TIMESTAMP) + @CreationTimestamp + @Column(name = "created_on") + private Date createdOn; + + @ElementCollection + @CollectionTable(name = "subscriptions", joinColumns = @JoinColumn(name = "parent_id")) + private Set subscriptions = new HashSet<>(); + + public Long getId() { + return id; + } + + public Subscriber setId(Long id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public Subscriber setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public Subscriber setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public Subscriber setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public Set getSubscriptions() { + return subscriptions; + } + + public Subscriber addSubscription(Subscription subscription) { + subscriptions.add(subscription); + return this; + } + } + + @Embeddable + @DiscriminatorColumn(name = "subscription_type") + public static class Subscription> { + + @Column(name = "opt_in") + private boolean optIn; + + public boolean isOptIn() { + return optIn; + } + + public T setOptIn(boolean optIn) { + this.optIn = optIn; + return (T) this; + } + } + + @Embeddable + @DiscriminatorValue("email") + public static class EmailSubscription extends Subscription { + + @Column(name = "email_address") + private String emailAddress; + + public String getEmailAddress() { + return emailAddress; + } + + public EmailSubscription setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + return this; + } + } + + @Embeddable + @DiscriminatorValue("sms") + public static class SmsSubscription extends Subscription { + + @Column(name = "phone_number") + private Long phoneNumber; + + public Long getPhoneNumber() { + return phoneNumber; + } + + public SmsSubscription setPhoneNumber(Long phoneNumber) { + this.phoneNumber = phoneNumber; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/BehaviorDrivenInheritanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/BehaviorDrivenInheritanceTest.java new file mode 100644 index 000000000..abffacebc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/BehaviorDrivenInheritanceTest.java @@ -0,0 +1,120 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring; + +import com.vladmihalcea.hpjp.hibernate.inheritance.spring.config.BehaviorDrivenInheritanceConfiguration; +import com.vladmihalcea.hpjp.hibernate.inheritance.spring.model.EmailSubscriber; +import com.vladmihalcea.hpjp.hibernate.inheritance.spring.model.SmsSubscriber; +import com.vladmihalcea.hpjp.hibernate.inheritance.spring.model.Subscriber; +import com.vladmihalcea.hpjp.hibernate.inheritance.spring.service.CampaignService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = BehaviorDrivenInheritanceConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class BehaviorDrivenInheritanceTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Autowired + private TransactionTemplate transactionTemplate; + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private CampaignService campaignService; + + @Test + public void test() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + EmailSubscriber email = new EmailSubscriber(); + email.setEmailAddress("vlad@acme.com"); + email.setFirstName("Vlad"); + email.setLastName("Mihalcea"); + + entityManager.persist(email); + + SmsSubscriber sms = new SmsSubscriber(); + sms.setPhoneNumber("012-345-67890"); + sms.setFirstName("Vlad"); + sms.setLastName("Mihalcea"); + + entityManager.persist(sms); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + + campaignService.send("Black Friday", "High-Performance Java Persistence is 40% OFF"); + } + + @Test + public void testStaticTypeHandlingIfElse() { + List subscribers = entityManager.createQuery(""" + select s + from Subscriber s + order by s.id + """) + .getResultList(); + + subscribers.stream() + .forEach(sub -> { + if(sub instanceof EmailSubscriber emailSub) { + LOGGER.info("Send email to address [{}]", emailSub.getEmailAddress()); + } else if(sub instanceof SmsSubscriber smsSub) { + LOGGER.info("Send SMS to phone number [{}]", smsSub.getPhoneNumber()); + } else { + throw new IllegalStateException( + String.format("The [%s] type is not supported!", sub.getClass()) + ); + } + }); + } + + @Test + public void testStaticTypeHandlingSwitch() { + List subscribers = entityManager.createQuery(""" + select s + from Subscriber s + order by s.id + """, Subscriber.class) + .getResultList(); + + //Preview feature - requires Java 19 language preview + /*subscribers.stream() + .forEach(sub -> { + switch (sub) { + case EmailSubscriber emailSub -> + LOGGER.info("Send email to address [{}]", emailSub.getEmailAddress()); + case SmsSubscriber smsSub -> + LOGGER.info("Send SMS to phone number [{}]", smsSub.getPhoneNumber()); + default -> throw new IllegalStateException( + String.format( + "The [%s] type is not supported!", + sub.getClass() + ) + ); + } + });*/ + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/config/BehaviorDrivenInheritanceConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/config/BehaviorDrivenInheritanceConfiguration.java new file mode 100644 index 000000000..74f62ba86 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/config/BehaviorDrivenInheritanceConfiguration.java @@ -0,0 +1,115 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring.config; + +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.*; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +import jakarta.persistence.EntityManagerFactory; +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +@Configuration +@PropertySource({"/META-INF/jdbc-mysql.properties"}) +@ComponentScan(basePackages = "com.vladmihalcea.hpjp.hibernate.inheritance.spring") +@EnableTransactionManagement +@EnableAspectJAutoProxy +public class BehaviorDrivenInheritanceConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + @Value("${jdbc.dataSourceClassName}") + private String dataSourceClassName; + + @Value("${jdbc.url}") + private String jdbcUrl; + + @Value("${jdbc.username}") + private String jdbcUser; + + @Value("${jdbc.password}") + private String jdbcPassword; + + @Bean + public static PropertySourcesPlaceholderConfigurer properties() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Bean(destroyMethod = "close") + public DataSource actualDataSource() { + Properties driverProperties = new Properties(); + driverProperties.setProperty("url", jdbcUrl); + driverProperties.setProperty("user", jdbcUser); + driverProperties.setProperty("password", jdbcPassword); + + Properties properties = new Properties(); + properties.put("dataSourceClassName", dataSourceClassName); + properties.put("dataSourceProperties", driverProperties); + properties.setProperty("maximumPoolSize", String.valueOf(3)); + return new HikariDataSource(new HikariConfig(properties)); + } + + @Bean + public DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + return ProxyDataSourceBuilder + .create(actualDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + localContainerEntityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + localContainerEntityManagerFactoryBean.setPersistenceProvider(new HibernatePersistenceProvider()); + localContainerEntityManagerFactoryBean.setDataSource(dataSource()); + localContainerEntityManagerFactoryBean.setPackagesToScan(packagesToScan()); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + localContainerEntityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + localContainerEntityManagerFactoryBean.setJpaProperties(additionalProperties()); + return localContainerEntityManagerFactoryBean; + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { + return new TransactionTemplate(transactionManager(entityManagerFactory)); + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + return properties; + } + + protected String[] packagesToScan() { + return new String[]{ + "com.vladmihalcea.hpjp.hibernate.inheritance.spring.model" + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/dao/GenericDAO.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/dao/GenericDAO.java new file mode 100644 index 000000000..5bdd390d9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/dao/GenericDAO.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring.dao; + +import java.io.Serializable; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface GenericDAO { + + T findById(ID id); + + List findAll(); + + T persist(T entity); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/dao/GenericDAOImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/dao/GenericDAOImpl.java new file mode 100644 index 000000000..ff1db230e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/dao/GenericDAOImpl.java @@ -0,0 +1,54 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring.dao; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import java.io.Serializable; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +@Transactional +public abstract class GenericDAOImpl implements GenericDAO { + + private final Class entityClass; + + @PersistenceContext + private EntityManager entityManager; + + protected GenericDAOImpl(Class entityClass) { + this.entityClass = entityClass; + } + + protected EntityManager getEntityManager() { + return entityManager; + } + + @Override + public T findById(ID id) { + return entityManager.find(entityClass, id); + } + + @Override + public List findAll() { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteria = builder.createQuery(entityClass); + Root root = criteria.from(entityClass); + criteria.orderBy(builder.asc(root.get("id"))); + + return entityManager.createQuery(criteria).getResultList(); + } + + @Override + public T persist(T entity) { + entityManager.persist(entity); + return entity; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/dao/SubscriberDAO.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/dao/SubscriberDAO.java new file mode 100644 index 000000000..48e7cddfe --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/dao/SubscriberDAO.java @@ -0,0 +1,10 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring.dao; + +import com.vladmihalcea.hpjp.hibernate.inheritance.spring.model.Subscriber; + +/** + * @author Vlad Mihalcea + */ +public interface SubscriberDAO extends GenericDAO { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/dao/SubscriberDAOImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/dao/SubscriberDAOImpl.java new file mode 100644 index 000000000..4d98fa6d3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/dao/SubscriberDAOImpl.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring.dao; + +import com.vladmihalcea.hpjp.hibernate.inheritance.spring.model.Subscriber; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public class SubscriberDAOImpl + extends GenericDAOImpl + implements SubscriberDAO { + + protected SubscriberDAOImpl() { + super(Subscriber.class); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/model/EmailSubscriber.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/model/EmailSubscriber.java new file mode 100644 index 000000000..04309308a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/model/EmailSubscriber.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "email_subscriber") +public class EmailSubscriber extends Subscriber { + + @Column(name = "email_address", nullable = false) + private String emailAddress; + + public String getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/model/SmsSubscriber.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/model/SmsSubscriber.java new file mode 100644 index 000000000..b95a1a003 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/model/SmsSubscriber.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "sms_subscriber") +public class SmsSubscriber extends Subscriber { + + @Column(name = "phone_number", nullable = false) + private String phoneNumber; + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/model/Subscriber.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/model/Subscriber.java new file mode 100644 index 000000000..ee96b6e88 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/model/Subscriber.java @@ -0,0 +1,62 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring.model; + +import org.hibernate.annotations.CreationTimestamp; + +import jakarta.persistence.*; +import java.util.Date; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "subscriber") +@Inheritance(strategy = InheritanceType.JOINED) +public class Subscriber { + + @Id + @GeneratedValue + private Long id; + + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + @Temporal(TemporalType.TIMESTAMP) + @CreationTimestamp + @Column(name = "created_on") + private Date createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/CampaignService.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/CampaignService.java new file mode 100644 index 000000000..1d9646b76 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/CampaignService.java @@ -0,0 +1,9 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring.service; + +/** + * @author Vlad Mihalcea + */ +public interface CampaignService { + + void send(String title, String message); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/CampaignServiceImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/CampaignServiceImpl.java new file mode 100644 index 000000000..d847b97c9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/CampaignServiceImpl.java @@ -0,0 +1,54 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring.service; + +import com.vladmihalcea.hpjp.hibernate.inheritance.spring.dao.SubscriberDAO; +import com.vladmihalcea.hpjp.hibernate.inheritance.spring.model.Subscriber; +import com.vladmihalcea.hpjp.hibernate.inheritance.spring.service.sender.CampaignSender; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +@Service +public class CampaignServiceImpl + implements CampaignService { + + @Autowired + private SubscriberDAO subscriberDAO; + + @Autowired + private List campaignSenders; + + private Map, CampaignSender> + campaignSenderMap = new HashMap<>(); + + @PostConstruct + @SuppressWarnings("unchecked") + public void init() { + for (CampaignSender campaignSender : campaignSenders) { + campaignSenderMap.put( + campaignSender.appliesTo(), + campaignSender + ); + } + } + + @Override + @Transactional + @SuppressWarnings("unchecked") + public void send(String title, String message) { + List subscribers = subscriberDAO.findAll(); + + for (Subscriber subscriber : subscribers) { + campaignSenderMap + .get(subscriber.getClass()) + .send(title, message, subscriber); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/sender/CampaignSender.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/sender/CampaignSender.java new file mode 100644 index 000000000..053a9151f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/sender/CampaignSender.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring.service.sender; + +import com.vladmihalcea.hpjp.hibernate.inheritance.spring.model.Subscriber; + +/** + * @author Vlad Mihalcea + */ +public interface CampaignSender { + + Class appliesTo(); + + void send(String title, String message, S subscriber); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/sender/EmailCampaignSender.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/sender/EmailCampaignSender.java new file mode 100644 index 000000000..cf35c967e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/sender/EmailCampaignSender.java @@ -0,0 +1,29 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring.service.sender; + +import com.vladmihalcea.hpjp.hibernate.inheritance.spring.model.EmailSubscriber; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * @author Vlad Mihalcea + */ +@Component +public class EmailCampaignSender implements CampaignSender { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Override + public Class appliesTo() { + return EmailSubscriber.class; + } + + @Override + public void send(String title, String message, EmailSubscriber subscriber) { + LOGGER.info("Send Email: {} - {} to address: {}", + title, + message, + subscriber.getEmailAddress() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/sender/SmsCampaignSender.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/sender/SmsCampaignSender.java new file mode 100644 index 000000000..77cbdf75f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/inheritance/spring/service/sender/SmsCampaignSender.java @@ -0,0 +1,29 @@ +package com.vladmihalcea.hpjp.hibernate.inheritance.spring.service.sender; + +import com.vladmihalcea.hpjp.hibernate.inheritance.spring.model.SmsSubscriber; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * @author Vlad Mihalcea + */ +@Component +public class SmsCampaignSender implements CampaignSender { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Override + public Class appliesTo() { + return SmsSubscriber.class; + } + + @Override + public void send(String title, String message, SmsSubscriber subscriber) { + LOGGER.info("Send SMS: {} - {} to phone number: {}", + title, + message, + subscriber.getPhoneNumber() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/listener/EntityReplicationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/listener/EntityReplicationTest.java new file mode 100644 index 000000000..431953c17 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/listener/EntityReplicationTest.java @@ -0,0 +1,249 @@ +package com.vladmihalcea.hpjp.hibernate.listener; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.boot.Metadata; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.*; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; +import org.junit.Test; + +import java.time.LocalDate; + +/** + * @author Vlad Mihalcea + */ +public class EntityReplicationTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + OldPost.class, + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post1 = new Post(); + post1.setId(1L); + post1.setTitle("The High-Performance Java Persistence book is to be released!"); + + entityManager.persist(post1); + }); + + doInJPA(entityManager -> { + Post post1 = entityManager.find(Post.class, 1L); + post1.setTitle(post1.getTitle().replace("to be ", "")); + + Post post2 = new Post(); + post2.setId(2L); + post2.setTitle("The High-Performance Java Persistence book is awesome!"); + + entityManager.persist(post2); + }); + + doInJPA(entityManager -> { + entityManager.remove(entityManager.getReference(Post.class, 1L)); + }); + } + + @Override + protected Integrator integrator() { + return ReplicationEventListenerIntegrator.INSTANCE; + } + + public static class ReplicationEventListenerIntegrator implements Integrator { + + public static final ReplicationEventListenerIntegrator INSTANCE = new ReplicationEventListenerIntegrator(); + + @Override + public void integrate( + Metadata metadata, + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + final EventListenerRegistry eventListenerRegistry = + serviceRegistry.getService(EventListenerRegistry.class); + + eventListenerRegistry.appendListeners(EventType.POST_INSERT, ReplicationInsertEventListener.INSTANCE); + eventListenerRegistry.appendListeners(EventType.POST_UPDATE, ReplicationUpdateEventListener.INSTANCE); + eventListenerRegistry.appendListeners(EventType.PRE_DELETE, ReplicationDeleteEventListener.INSTANCE); + } + + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + } + } + + public static class ReplicationInsertEventListener implements PostInsertEventListener { + + public static final ReplicationInsertEventListener INSTANCE = new ReplicationInsertEventListener(); + + @Override + public void onPostInsert(PostInsertEvent event) throws HibernateException { + final Object entity = event.getEntity(); + + if(entity instanceof Post) { + Post post = (Post) entity; + + event.getSession().createNativeQuery( + "INSERT INTO old_post (id, title, version) " + + "VALUES (:id, :title, :version)") + .setParameter("id", post.getId()) + .setParameter("title", post.getTitle()) + .setParameter("version", post.getVersion()) + .setHibernateFlushMode(FlushMode.MANUAL) + .executeUpdate(); + } + } + + @Override + public boolean requiresPostCommitHandling(EntityPersister persister) { + return false; + } + } + + public static class ReplicationUpdateEventListener implements PostUpdateEventListener { + + public static final ReplicationUpdateEventListener INSTANCE = new ReplicationUpdateEventListener(); + + @Override + public void onPostUpdate(PostUpdateEvent event) { + final Object entity = event.getEntity(); + + if(entity instanceof Post) { + Post post = (Post) entity; + + event.getSession().createNativeQuery( + "UPDATE old_post " + + "SET title = :title, version = :version " + + "WHERE id = :id") + .setParameter("id", post.getId()) + .setParameter("title", post.getTitle()) + .setParameter("version", post.getVersion()) + .setHibernateFlushMode(FlushMode.MANUAL) + .executeUpdate(); + } + } + + @Override + public boolean requiresPostCommitHandling(EntityPersister persister) { + return false; + } + } + + public static class ReplicationDeleteEventListener implements PreDeleteEventListener { + + public static final ReplicationDeleteEventListener INSTANCE = new ReplicationDeleteEventListener(); + + @Override + public boolean onPreDelete(PreDeleteEvent event) { + final Object entity = event.getEntity(); + + if(entity instanceof Post) { + Post post = (Post) entity; + + event.getSession().createNativeQuery( + "DELETE FROM old_post " + + "WHERE id = :id") + .setParameter("id", post.getId()) + .setHibernateFlushMode(FlushMode.MANUAL) + .executeUpdate(); + } + + return false; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDate createdOn = LocalDate.now(); + + @Version + private short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDate createdOn) { + this.createdOn = createdOn; + } + + public int getVersion() { + return version; + } + } + + @Entity(name = "OldPost") + @Table(name = "old_post") + public static class OldPost { + + @Id + private Long id; + + private String title; + + @Version + private short version; + + @MapsId + @OneToOne + @JoinColumn(name = "id") + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getVersion() { + return version; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/listener/Updatable.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/listener/Updatable.java new file mode 100644 index 000000000..7d3919b0c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/listener/Updatable.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.hibernate.listener; + +import java.util.Date; + +/** + * @author Vlad Mihalcea + */ +public interface Updatable { + + void setTimestamp(Date timestamp); + + Date getTimestamp(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/listener/UpdatableListener.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/listener/UpdatableListener.java new file mode 100644 index 000000000..52b1fe3f1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/listener/UpdatableListener.java @@ -0,0 +1,21 @@ +package com.vladmihalcea.hpjp.hibernate.listener; + +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import java.util.Date; + +/** + * @author Vlad Mihalcea + */ +public class UpdatableListener { + + @PrePersist + @PreUpdate + private void setCurrentTimestamp(Object entity) { + if(entity instanceof Updatable) { + Updatable updatable = (Updatable) entity; + updatable.setTimestamp(new Date()); + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/listener/UpdatableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/listener/UpdatableTest.java similarity index 91% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/listener/UpdatableTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/listener/UpdatableTest.java index 3621db309..07cde65b7 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/listener/UpdatableTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/listener/UpdatableTest.java @@ -1,12 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.listener; +package com.vladmihalcea.hpjp.hibernate.listener; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.Criteria; -import org.hibernate.Session; -import org.hibernate.criterion.Restrictions; +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; import org.junit.Test; -import javax.persistence.*; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -43,11 +40,6 @@ public void test() { post.addComment(comment1); post.addComment(comment2); entityManager.persist(post); - - Session session = entityManager.unwrap(Session.class); - Criteria criteria = session.createCriteria(Post.class) - .add(Restrictions.eq("title", "post")); - LOGGER.info("Criteria: {}", criteria); }); doInJPA(entityManager -> { @@ -173,7 +165,6 @@ public PostDetails() { } @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") @MapsId private Post post; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/DataSourceProxySlowQueryLogTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/DataSourceProxySlowQueryLogTest.java new file mode 100644 index 000000000..eee1266fd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/DataSourceProxySlowQueryLogTest.java @@ -0,0 +1,155 @@ +package com.vladmihalcea.hpjp.hibernate.logging; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JSlowQueryListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.junit.Test; + +import javax.sql.DataSource; +import java.util.Collection; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.*; +import java.util.stream.IntStream; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DataSourceProxySlowQueryLogTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + } + + @Override + protected boolean connectionPooling() { + return true; + } + + @Test + public void testSlowQueryLog() throws InterruptedException { + doInJPA(entityManager -> { + LongStream + .rangeClosed(1, 50 * 100) + .forEach(i -> { + entityManager.persist( + new Post() + .setId(i) + .setTitle( + String.format( + "High-Performance Java Persistence book - page %d", + i + ) + ) + ); + if(i % 50 == 0 && i > 0) { + entityManager.flush(); + entityManager.clear(); + } + }); + }); + + LOGGER.info("Check slow JPQL query"); + + int threadCount = connectionPoolSize(); + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + + Collection> callables = IntStream.range(0, threadCount) + .mapToObj(i -> (Callable) () -> { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + where lower(title) like :titlePattern + order by p.id desc + """, Post.class) + .setParameter("titlePattern", "%Java%book%".toLowerCase()) + .setFirstResult(4000) + .setMaxResults(100) + .getResultList(); + + assertEquals(100, posts.size()); + }); + return null; + }) + .toList(); + + long startNanos = System.nanoTime(); + List> futures = executorService.invokeAll(callables); + for (Future future : futures) { + try { + future.get(); + } catch (InterruptedException| ExecutionException e) { + LOGGER.error(e.getMessage()); + } + } + LOGGER.info( + "{} threads ran in [{}] ms", + threadCount, + TimeUnit.NANOSECONDS.toMillis( + System.nanoTime() - startNanos + )); + } + + protected DataSource dataSourceProxy(DataSource dataSource) { + String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + SLF4JSlowQueryListener slowQueryListener = new SLF4JSlowQueryListener(); + slowQueryListener.setThreshold(25); + slowQueryListener.setThresholdTimeUnit(TimeUnit.MILLISECONDS); + + DataSource proxyDataSource = ProxyDataSourceBuilder + .create(dataSource) + .name(DATA_SOURCE_PROXY_NAME) + .listener(slowQueryListener) + .build(); + + return proxyDataSource; + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/DataSourceProxyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/DataSourceProxyTest.java new file mode 100644 index 000000000..7963bd549 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/DataSourceProxyTest.java @@ -0,0 +1,102 @@ +package com.vladmihalcea.hpjp.hibernate.logging; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.junit.Test; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class DataSourceProxyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("Post it!"); + + entityManager.persist(post); + }); + } + + @Test + public void testBatch() { + doInJPA(entityManager -> { + for (long i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle(String.format("Post no. %d", i)) + ); + } + }); + } + + protected DataSource dataSourceProxy(DataSource dataSource) { + String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + ChainListener listener = new ChainListener(); + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + listener.addListener(loggingListener); + + DataSource proxyDataSource = ProxyDataSourceBuilder + .create(dataSource) + .name(DATA_SOURCE_PROXY_NAME) + .listener(listener) + .build(); + + return proxyDataSource; + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/HibernateLoggingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/HibernateLoggingTest.java new file mode 100644 index 000000000..da789de60 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/HibernateLoggingTest.java @@ -0,0 +1,92 @@ +package com.vladmihalcea.hpjp.hibernate.logging; + +import java.util.Properties; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +import org.junit.Test; + +import com.vladmihalcea.hpjp.util.AbstractTest; + +/** + * @author Vlad Mihalcea + */ +public class HibernateLoggingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + //properties.put("hibernate.format_sql", "true"); + } + + @Override + protected boolean proxyDataSource() { + return false; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + } + + @Test + public void testBatch() { + doInJPA(entityManager -> { + for (long id = 1; id <= 5; id++) { + entityManager.persist( + new Post() + .setId(id) + .setTitle( + String.format( + "High-Performance Java Persistence, part %d", + id + ) + ) + ); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/LoggingStatementInspector.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/LoggingStatementInspector.java new file mode 100644 index 000000000..f8d91ca6c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/LoggingStatementInspector.java @@ -0,0 +1,34 @@ +package com.vladmihalcea.hpjp.hibernate.logging; + +import com.vladmihalcea.hpjp.util.StackTraceUtils; +import org.hibernate.resource.jdbc.spi.StatementInspector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Vlad Mihalcea + */ +public class LoggingStatementInspector implements StatementInspector { + + private static final Logger LOGGER = LoggerFactory.getLogger(LoggingStatementInspector.class); + + private final String packageNamePrefix; + + public LoggingStatementInspector(String packageNamePrefix) { + this.packageNamePrefix = packageNamePrefix; + } + + @Override + public String inspect(String sql) { + LOGGER.info( + "Executing SQL query: {} from {}", + sql, + StackTraceUtils.stackTracePath( + StackTraceUtils.stackTraceElements( + packageNamePrefix + ) + ) + ); + return null; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/MDCLoggingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/MDCLoggingTest.java new file mode 100644 index 000000000..a7b1d650d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/MDCLoggingTest.java @@ -0,0 +1,164 @@ +package com.vladmihalcea.hpjp.hibernate.logging; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import jakarta.persistence.*; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.junit.Test; +import org.slf4j.MDC; + +import java.util.Properties; + +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class MDCLoggingTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + } + + @Override + protected boolean proxyDataSource() { + return true; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("Post it!"); + + entityManager.persist(post); + }); + } + + @Test + public void testWithoutMDC() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .getSingleResult(); + + try { + executeSync(() -> { + doInJPA(_entityManager -> { + Post _post = (Post) _entityManager.createQuery(""" + select p + from Post p + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .unwrap(org.hibernate.query.Query.class) + .setLockOptions( + new LockOptions() + .setLockMode(LockMode.PESSIMISTIC_WRITE) + .setTimeOut(LockOptions.NO_WAIT) + ) + .getSingleResult(); + }); + }); + } catch (Exception expected) { + assertTrue(ExceptionUtil.rootCause(expected).getMessage().contains("could not obtain lock on row in relation")); + } + }); + } + + @Test + public void testWithMDC() { + doInJPA(entityManager -> { + try (MDC.MDCCloseable closable = MDC.putCloseable( + "txId", + String.format(" TxId: [%s]", transactionId(entityManager)) + )) { + + Post post = entityManager.createQuery(""" + select p + from Post p + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .getSingleResult(); + + try { + executeSync(() -> { + doInJPA(_entityManager -> { + try (MDC.MDCCloseable _closable = MDC.putCloseable( + "txId", + String.format(" TxId: [%s]", transactionId(_entityManager)) + )) { + Post _post = (Post) _entityManager.createQuery(""" + select p + from Post p + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .unwrap(org.hibernate.query.Query.class) + .setLockOptions( + new LockOptions() + .setLockMode(LockMode.PESSIMISTIC_WRITE) + .setTimeOut(LockOptions.NO_WAIT) + ) + .getSingleResult(); + } + }); + }); + } catch (Exception expected) { + assertTrue( + ExceptionUtil + .rootCause(expected) + .getMessage() + .contains("could not obtain lock on row in relation") + ); + } + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/MySQLMDCLoggingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/MySQLMDCLoggingTest.java new file mode 100644 index 000000000..57e9bd066 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/MySQLMDCLoggingTest.java @@ -0,0 +1,141 @@ +package com.vladmihalcea.hpjp.hibernate.logging; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import jakarta.persistence.*; +import org.hibernate.LockOptions; +import org.junit.Test; +import org.slf4j.MDC; + +import java.util.Properties; + +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class MySQLMDCLoggingTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "5"); + } + + @Override + protected boolean proxyDataSource() { + return true; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("Post it!"); + + entityManager.persist(post); + }); + } + + @Test + public void testWithMDC() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .getSingleResult(); + + try (MDC.MDCCloseable closable = MDC.putCloseable( + "txId", + String.format(" TxId: [%s]", transactionId(entityManager)) + )) { + executeSync(() -> { + doInJPA(_entityManager -> { + LOGGER.info("Acquire lock so that the TxId is assigned"); + _entityManager.persist( + new Post() + .setId(2L) + .setTitle("New Post!") + ); + _entityManager.flush(); + sleep(100); + + try (MDC.MDCCloseable _closable = MDC.putCloseable( + "txId", + String.format(" TxId: [%s]", transactionId(_entityManager)) + )) { + + try { + Post _post = (Post) _entityManager.createQuery(""" + select p + from Post p + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .unwrap(org.hibernate.query.Query.class) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .setHint( + "jakarta.persistence.lock.timeout", + LockOptions.NO_WAIT + ) + .getSingleResult(); + } catch (Exception expected) { + assertTrue(ExceptionUtil.isLockTimeout(expected)); + } + } + }); + }); + } + }); + } + + protected String threadId(EntityManager entityManager) { + return String.valueOf( + entityManager + .createNativeQuery("SELECT CONNECTION_ID()") + .getSingleResult() + ); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private short version; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/P6spyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/P6spyTest.java new file mode 100644 index 000000000..8c1a2f8fa --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/P6spyTest.java @@ -0,0 +1,103 @@ +package com.vladmihalcea.hpjp.hibernate.logging; + +import java.util.Properties; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * @author Vlad Mihalcea + */ +public class P6spyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( "hibernate.jdbc.batch_size", "5" ); + } + + @Override + protected DataSourceProxyType dataSourceProxyType() { + return DataSourceProxyType.P6SPY; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId( 1L ); + post.setTitle( "Post it!" ); + + entityManager.persist(post); + }); + } + + @Test + public void testBatch() { + doInJPA(entityManager -> { + for ( long i = 0; i < 3; i++ ) { + Post post = new Post(); + post.setId( i ); + post.setTitle( + String.format( + "Post no. %d", + i + ) + ); + entityManager.persist(post); + } + }); + } + + @Test + public void testOutageDetection() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId( 1L ); + post.setTitle( "Post it!" ); + + entityManager.persist(post); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/QueryStackTraceLoggerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/QueryStackTraceLoggerTest.java new file mode 100644 index 000000000..ed05ada87 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/QueryStackTraceLoggerTest.java @@ -0,0 +1,93 @@ +package com.vladmihalcea.hpjp.hibernate.logging; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import io.hypersistence.utils.hibernate.query.QueryStackTraceLogger; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class QueryStackTraceLoggerTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + AvailableSettings.STATEMENT_INSPECTOR, + new QueryStackTraceLogger("com.vladmihalcea.hpjp") + ); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("Post it!"); + + entityManager.persist(post); + }); + } + + @Test + public void testBatch() { + doInJPA(entityManager -> { + for (long id = 1; id <= 5; id++) { + entityManager.persist( + new Post() + .setId(id) + .setTitle( + String.format( + "High-Performance Java Persistence, part %d", + id + ) + ) + ); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private short version; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/StatementInspectorLoggingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/StatementInspectorLoggingTest.java new file mode 100644 index 000000000..22e0093f6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/StatementInspectorLoggingTest.java @@ -0,0 +1,90 @@ +package com.vladmihalcea.hpjp.hibernate.logging; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import org.junit.Test; + +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class StatementInspectorLoggingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.session_factory.statement_inspector", + new LoggingStatementInspector(getClass().getPackage().getName()) + ); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("Post it!"); + + entityManager.persist(post); + }); + } + + @Test + public void testBatch() { + doInJPA(entityManager -> { + for (long id = 1; id <= 5; id++) { + Post post = new Post(); + post.setId(id); + post.setTitle( + String.format( + "High-Performance Java Persistence, part %d", + id + ) + ); + + entityManager.persist(post); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/comment/HibernateCommentLoggingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/comment/HibernateCommentLoggingTest.java new file mode 100644 index 000000000..b7083ee69 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/comment/HibernateCommentLoggingTest.java @@ -0,0 +1,163 @@ +package com.vladmihalcea.hpjp.hibernate.logging.comment; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.LockModeType; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.ParameterExpression; +import jakarta.persistence.criteria.Root; +import org.hibernate.LockOptions; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class HibernateCommentLoggingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected boolean connectionPooling() { + return true; + } + + @Override + protected int connectionPoolSize() { + return 1; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.use_sql_comments", "true"); + } + + @Test + public void testCrud() { + Post post = doInJPA(entityManager -> { + Post newPost = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + entityManager.persist(newPost); + + return newPost; + }); + + post.setTitle("High-Performance Java Persistence 2nd edition"); + + doInJPA(entityManager -> { + entityManager.merge(post); + }); + + doInJPA(entityManager -> { + entityManager.remove( + entityManager.getReference( + Post.class, + post.getId() + ) + ); + }); + } + + @Test + public void testJPQL() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + /* Find post entities matching title pattern */ + select p + from Post p + where p.title like :titlePattern + """, Post.class) + .setParameter("titlePattern", "High-Performance%") + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + @Test + public void testCriteriaAPI() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = builder.createQuery(Post.class); + Root post = query.from(Post.class); + ParameterExpression titlePattern = builder.parameter(String.class); + + query.where( + builder.like(post.get(Post_.TITLE), titlePattern) + ); + + List posts = entityManager.createQuery(query) + .setParameter(titlePattern, "High-Performance%") + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + @Test + public void testCacheMiss() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + entityManager.lock( + post, + LockModeType.PESSIMISTIC_READ + ); + + assertEquals("High-Performance Java Persistence", post.getTitle()); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals("High-Performance Java Persistence", post.getTitle()); + + entityManager.createNativeQuery("select id from post where id=? for share") + .setParameter(1, post.getId()) + .getSingleResult(); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/comment/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/comment/Post.java new file mode 100644 index 000000000..1a36c7ee7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/comment/Post.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.hibernate.logging.comment; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/inspector/Book.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/inspector/Book.java new file mode 100644 index 000000000..86dc1f76b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/inspector/Book.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.hibernate.logging.inspector; + +import org.hibernate.annotations.NaturalId; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Book") +@Table(name = "book") +public class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + private String author; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/inspector/SQLCommentStatementInspector.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/inspector/SQLCommentStatementInspector.java new file mode 100644 index 000000000..0f3e10be4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/inspector/SQLCommentStatementInspector.java @@ -0,0 +1,27 @@ +package com.vladmihalcea.hpjp.hibernate.logging.inspector; + +import org.hibernate.resource.jdbc.spi.StatementInspector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.regex.Pattern; + +/** + * @author Vlad Mihalcea + */ +public class SQLCommentStatementInspector implements StatementInspector { + + private static final Logger LOGGER = LoggerFactory.getLogger(SQLCommentStatementInspector.class); + + private static final Pattern SQL_COMMENT_PATTERN = Pattern.compile("\\/\\*.*?\\*\\/\\s*"); + + @Override + public String inspect(String sql) { + LOGGER.debug( + "Executing SQL query: {}", + sql + ); + + return SQL_COMMENT_PATTERN.matcher(sql).replaceAll(""); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/inspector/SQLCommentTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/inspector/SQLCommentTest.java new file mode 100644 index 000000000..f6fa40963 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/inspector/SQLCommentTest.java @@ -0,0 +1,48 @@ +package com.vladmihalcea.hpjp.hibernate.logging.inspector; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Session; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SQLCommentTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.use_sql_comments", "true"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea") + ); + }); + + doInJPA(entityManager -> { + Book book = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals("High-Performance Java Persistence", book.getTitle()); + }); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/inspector/StatementInspectorSqlCommentTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/inspector/StatementInspectorSqlCommentTest.java new file mode 100644 index 000000000..220e04922 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/inspector/StatementInspectorSqlCommentTest.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hpjp.hibernate.logging.inspector; + +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class StatementInspectorSqlCommentTest extends SQLCommentTest { + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put( + "hibernate.session_factory.statement_inspector", + SQLCommentStatementInspector.class.getName() + ); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/validator/SQLStatementCountValidatorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/validator/SQLStatementCountValidatorTest.java new file mode 100644 index 000000000..1531539ce --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/validator/SQLStatementCountValidatorTest.java @@ -0,0 +1,196 @@ +package com.vladmihalcea.hpjp.hibernate.logging.validator; + +import com.vladmihalcea.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; +import com.vladmihalcea.hpjp.util.AbstractTest; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class SQLStatementCountValidatorTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post1 = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + entityManager.persist(post1); + + entityManager.persist( + new PostComment() + .setId(1L) + .setReview("Good") + .setPost(post1) + ); + + Post post2 = new Post() + .setId(2L) + .setTitle("Hypersistence Optimizer"); + + entityManager.persist(post2); + + entityManager.persist( + new PostComment() + .setId(2L) + .setReview("Excellent") + .setPost(post2) + ); + }); + } + + @Test + public void testNPlusOne() { + doInJPA(entityManager -> { + LOGGER.info("Detect N+1"); + SQLStatementCountValidator.reset(); + + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + """, PostComment.class) + .getResultList(); + + assertEquals(2, comments.size()); + + SQLStatementCountValidator.assertSelectCount(1); + }); + } + + @Test + @Ignore + public void testNPlusOneWithQueryCountHolder() { + doInJPA(entityManager -> { + QueryCountHolder.clear(); + + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + """, PostComment.class) + .getResultList(); + + LOGGER.info("Detect N+1"); + + for(PostComment comment : comments) { + LOGGER.info( + "Comment: [{}] for post: [{}]", + comment.getReview(), + comment.getPost().getTitle() + ); + } + + assertEquals(1, QueryCountHolder.getGrandTotal().getSelect()); + assertEquals(2, comments.size()); + }); + } + + @Test + public void testJoinFetch() { + doInJPA(entityManager -> { + LOGGER.info("Join fetch to prevent N+1"); + SQLStatementCountValidator.reset(); + + List comments = entityManager.createQuery(""" + select pc + from PostComment pc + join fetch pc.post + """, PostComment.class) + .getResultList(); + + for(PostComment comment : comments) { + LOGGER.info( + "Comment: [{}] for post: [{}]", + comment.getReview(), + comment.getPost().getTitle() + ); + } + + SQLStatementCountValidator.assertSelectCount(1); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/validator/sql/SQLStatementCountValidator.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/validator/sql/SQLStatementCountValidator.java similarity index 87% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/validator/sql/SQLStatementCountValidator.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/validator/sql/SQLStatementCountValidator.java index 14db8e4af..d7b76abca 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/logging/validator/sql/SQLStatementCountValidator.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/validator/sql/SQLStatementCountValidator.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package com.vladmihalcea.book.hpjp.hibernate.logging.validator.sql; +package com.vladmihalcea.hpjp.hibernate.logging.validator.sql; -import com.vladmihalcea.book.hpjp.hibernate.logging.validator.sql.exception.SQLStatementCountMismatchException; +import com.vladmihalcea.hpjp.hibernate.logging.validator.sql.exception.SQLStatementCountMismatchException; import net.ttddyy.dsproxy.QueryCount; import net.ttddyy.dsproxy.QueryCountHolder; @@ -46,7 +46,7 @@ public static void reset() { */ public static void assertSelectCount(int expectedSelectCount) { QueryCount queryCount = QueryCountHolder.getGrandTotal(); - int recordedSelectCount = queryCount.getSelect(); + long recordedSelectCount = queryCount.getSelect(); if (expectedSelectCount != recordedSelectCount) { throw new SQLStatementCountMismatchException(expectedSelectCount, recordedSelectCount); } @@ -59,7 +59,7 @@ public static void assertSelectCount(int expectedSelectCount) { */ public static void assertInsertCount(int expectedInsertCount) { QueryCount queryCount = QueryCountHolder.getGrandTotal(); - int recordedInsertCount = queryCount.getInsert(); + long recordedInsertCount = queryCount.getInsert(); if (expectedInsertCount != recordedInsertCount) { throw new SQLStatementCountMismatchException(expectedInsertCount, recordedInsertCount); } @@ -72,7 +72,7 @@ public static void assertInsertCount(int expectedInsertCount) { */ public static void assertUpdateCount(int expectedUpdateCount) { QueryCount queryCount = QueryCountHolder.getGrandTotal(); - int recordedUpdateCount = queryCount.getUpdate(); + long recordedUpdateCount = queryCount.getUpdate(); if (expectedUpdateCount != recordedUpdateCount) { throw new SQLStatementCountMismatchException(expectedUpdateCount, recordedUpdateCount); } @@ -85,7 +85,7 @@ public static void assertUpdateCount(int expectedUpdateCount) { */ public static void assertDeleteCount(int expectedDeleteCount) { QueryCount queryCount = QueryCountHolder.getGrandTotal(); - int recordedDeleteCount = queryCount.getDelete(); + long recordedDeleteCount = queryCount.getDelete(); if (expectedDeleteCount != recordedDeleteCount) { throw new SQLStatementCountMismatchException(expectedDeleteCount, recordedDeleteCount); } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/validator/sql/exception/SQLStatementCountMismatchException.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/validator/sql/exception/SQLStatementCountMismatchException.java new file mode 100644 index 000000000..2ff577a58 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/logging/validator/sql/exception/SQLStatementCountMismatchException.java @@ -0,0 +1,28 @@ +package com.vladmihalcea.hpjp.hibernate.logging.validator.sql.exception; + +/** + * SQLStatementCountMismatchException - Thrown whenever there is a mismatch between expected statements count and + * the ones being executed. + * + * @author Vlad Mihalcea + */ +public class SQLStatementCountMismatchException extends RuntimeException { + + private final long expected; + private final long recorded; + + public SQLStatementCountMismatchException(long expected, long recorded) { + super(String.format("Expected %d statement(s) but recorded %d instead!", + expected, recorded)); + this.expected = expected; + this.recorded = recorded; + } + + public long getExpected() { + return expected; + } + + public long getRecorded() { + return recorded; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/DefaultUpdateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/DefaultUpdateTest.java new file mode 100644 index 000000000..ae81569ce --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/DefaultUpdateTest.java @@ -0,0 +1,146 @@ +package com.vladmihalcea.hpjp.hibernate.mapping; + +import java.sql.Timestamp; +import java.time.format.DateTimeFormatter; +import java.util.Properties; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; + +import com.vladmihalcea.hpjp.util.providers.Database; + +import org.junit.Test; + +import com.vladmihalcea.hpjp.util.AbstractTest; + +/** + * @author Vlad Mihalcea + */ +public class DefaultUpdateTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.jdbc.batch_size", "5"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_versioned_data", "true"); + return properties; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + + entityManager.persist( + new Post() + .setId(2L) + .setTitle("Java Persistence with Hibernate") + ); + }); + + doInJPA(entityManager -> { + Post post1 = entityManager.find(Post.class, 1L); + post1.setTitle("High-Performance Java Persistence 2nd Edition"); + + Post post2 = entityManager.find(Post.class, 2L); + post2.setLikes(12); + + entityManager.flush(); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + private long likes; + + @Column(name = "created_on", nullable = false, updatable = false) + private Timestamp createdOn; + + @Transient + private String creationTimestamp; + + public Post() { + this.createdOn = new Timestamp(System.currentTimeMillis()); + } + + public String getCreationTimestamp() { + if(creationTimestamp == null) { + creationTimestamp = DateTimeFormatter.ISO_DATE_TIME.format( + createdOn.toLocalDateTime() + ); + } + return creationTimestamp; + } + + @Override + public String toString() { + return String.format(""" + Post{ + id=%d + title='%s' + likes=%d + creationTimestamp='%s' + }""" + , id, title, likes, getCreationTimestamp() + ); + } + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public long getLikes() { + return likes; + } + + public Post setLikes(long likes) { + this.likes = likes; + return this; + } + + public Timestamp getCreatedOn() { + return createdOn; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/DynamicUpdateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/DynamicUpdateTest.java new file mode 100644 index 000000000..7f1397b22 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/DynamicUpdateTest.java @@ -0,0 +1,138 @@ +package com.vladmihalcea.hpjp.hibernate.mapping; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.DynamicUpdate; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class DynamicUpdateTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.jdbc.batch_size", "5"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_versioned_data", "true"); + return properties; + } + + @Test + public void test() { + + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + + entityManager.persist( + new Post() + .setId(2L) + .setTitle("Java Persistence with Hibernate") + ); + }); + + doInJPA(entityManager -> { + Post post1 = entityManager.find(Post.class, 1L); + post1.setTitle("High-Performance Java Persistence 2nd Edition"); + + Post post2 = entityManager.find(Post.class, 2L); + post2.setLikes(12); + }); + } + + @Test + public void testBatch() { + + doInJPA(entityManager -> { + for (long i = 1; i <= 5; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle(String.format("Post %s", i)) + ); + } + }); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + """, Post.class) + .getResultList(); + + for(Post post : posts) { + if (post.getId() % 2 == 0) { + post.setTitle(post.getTitle() + " is great!"); + } else { + post.setLikes(post.getId() * 2); + } + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @DynamicUpdate + public static class Post { + + @Id + private Long id; + + private String title; + + private long likes; + + @Column(name = "created_on", nullable = false, updatable = false) + private LocalDateTime createdOn = LocalDateTime.now(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public long getLikes() { + return likes; + } + + public Post setLikes(long likes) { + this.likes = likes; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/FluentSettersTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/FluentSettersTest.java similarity index 97% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/FluentSettersTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/FluentSettersTest.java index be3d58035..25c9e786a 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/FluentSettersTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/FluentSettersTest.java @@ -1,10 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping; +package com.vladmihalcea.hpjp.hibernate.mapping; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.apache.commons.lang3.SerializationUtils; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serializable; import java.sql.Timestamp; import java.time.LocalDateTime; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/GeneratedTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/GeneratedTest.java similarity index 93% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/GeneratedTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/GeneratedTest.java index 6ef0b785c..67e61356e 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/GeneratedTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/GeneratedTest.java @@ -1,13 +1,13 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping; +package com.vladmihalcea.hpjp.hibernate.mapping; -import com.vladmihalcea.book.hpjp.util.AbstractSQLServerIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; import org.hibernate.annotations.Generated; import org.hibernate.annotations.GenerationTime; import org.junit.Test; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; import static org.junit.Assert.assertEquals; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/HibernateImmutableExceptionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/HibernateImmutableExceptionTest.java new file mode 100644 index 000000000..b33a250a7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/HibernateImmutableExceptionTest.java @@ -0,0 +1,156 @@ +package com.vladmihalcea.hpjp.hibernate.mapping; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import jakarta.persistence.*; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.Root; +import org.hibernate.HibernateException; +import org.hibernate.annotations.Immutable; +import org.junit.Test; + +import java.util.Date; +import java.util.Locale; +import java.util.Properties; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class HibernateImmutableExceptionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Event event = new Event(1L, "Temperature", "25"); + + entityManager.persist(event); + }); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.query.immutable_entity_update_query_handling_mode", "exception"); + } + + @Test + public void testFlushChanges() { + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals("25", event.getEventValue()); + + ReflectionUtils.setFieldValue(event, "eventValue", "10"); + assertEquals("10", event.getEventValue()); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals("25", event.getEventValue()); + }); + } + + @Test + public void testJPQL() { + try { + doInJPA(entityManager -> { + entityManager.createQuery( + "update Event " + + "set eventValue = :eventValue " + + "where id = :id") + .setParameter("eventValue", "10") + .setParameter("id", 1L) + .executeUpdate(); + }); + + fail("Should have thrown exception"); + } catch (HibernateException expected) { + assertTrue( + expected.getMessage().toLowerCase(Locale.ROOT).contains( + "attempts to update an immutable entity: [event]" + ) + ); + } + } + + @Test + public void testCriteriaAPI() { + try { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaUpdate update = builder.createCriteriaUpdate(Event.class); + + Root root = update.from(Event.class); + + update + .set(root.get("eventValue"), "100") + .where( + builder.equal(root.get("id"), 1L) + ); + + entityManager.createQuery(update).executeUpdate(); + }); + + fail("Should have thrown exception"); + } catch (HibernateException expected) { + assertTrue( + expected.getMessage().toLowerCase(Locale.ROOT).contains( + "attempts to update an immutable entity: [event]" + ) + ); + } + } + + @Entity(name = "Event") + @Immutable + public static class Event { + + @Id + private Long id; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "event_key") + private String eventKey; + + @Column(name = "event_value") + private String eventValue; + + public Event(Long id, String eventKey, String eventValue) { + this.id = id; + this.eventKey = eventKey; + this.eventValue = eventValue; + } + + private Event() { + } + + public Long getId() { + return id; + } + + public Date getCreatedOn() { + return createdOn; + } + + public String getEventKey() { + return eventKey; + } + + public String getEventValue() { + return eventValue; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/HibernateImmutableWarningTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/HibernateImmutableWarningTest.java new file mode 100644 index 000000000..daf84ac79 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/HibernateImmutableWarningTest.java @@ -0,0 +1,145 @@ +package com.vladmihalcea.hpjp.hibernate.mapping; + +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import org.hibernate.annotations.Immutable; +import org.junit.Test; + +import jakarta.persistence.*; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.Root; +import java.util.Date; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class HibernateImmutableWarningTest extends AbstractSQLServerIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Event event = new Event( + 1L, + "Temperature", + "25" + ); + + entityManager.persist(event); + }); + } + + @Test + public void testFlushChanges() { + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals("25", event.getEventValue()); + + ReflectionUtils.setFieldValue(event, "eventValue", "10"); + assertEquals("10", event.getEventValue()); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals("25", event.getEventValue()); + }); + } + + @Test + public void testJPQL() { + doInJPA(entityManager -> { + entityManager.createQuery( + "update Event " + + "set eventValue = :eventValue " + + "where id = :id") + .setParameter("eventValue", "10") + .setParameter("id", 1L) + .executeUpdate(); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals("10", event.getEventValue()); + }); + } + + @Test + public void testCriteriaAPI() { + + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaUpdate update = builder.createCriteriaUpdate(Event.class); + + Root root = update.from(Event.class); + + update + .set(root.get("eventValue"), "100") + .where( + builder.equal(root.get("id"), 1L) + ); + + entityManager.createQuery(update).executeUpdate(); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals("100", event.getEventValue()); + }); + } + + @Entity(name = "Event") + @Immutable + public static class Event { + + @Id + private Long id; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "event_key") + private String eventKey; + + @Column(name = "event_value") + private String eventValue; + + public Event(Long id, String eventKey, String eventValue) { + this.id = id; + this.eventKey = eventKey; + this.eventValue = eventValue; + } + + private Event() { + } + + public Long getId() { + return id; + } + + public Date getCreatedOn() { + return createdOn; + } + + public String getEventKey() { + return eventKey; + } + + public String getEventValue() { + return eventValue; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/HydratedStateListenerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/HydratedStateListenerTest.java similarity index 93% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/HydratedStateListenerTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/HydratedStateListenerTest.java index fa3954624..e07d3c557 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/HydratedStateListenerTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/HydratedStateListenerTest.java @@ -1,13 +1,13 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping; +package com.vladmihalcea.hpjp.hibernate.mapping; import java.sql.Timestamp; import java.time.format.DateTimeFormatter; import java.util.Properties; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.persistence.Transient; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; import org.hibernate.HibernateException; import org.hibernate.Session; @@ -21,7 +21,7 @@ import org.junit.Test; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/JPAFluentInterfaceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JPAFluentInterfaceTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/JPAFluentInterfaceTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JPAFluentInterfaceTest.java index 52ed1109a..f97682b5d 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/JPAFluentInterfaceTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JPAFluentInterfaceTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping; +package com.vladmihalcea.hpjp.hibernate.mapping; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.sql.Timestamp; import java.time.LocalDateTime; import java.time.ZoneOffset; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JPAImmutableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JPAImmutableTest.java new file mode 100644 index 000000000..37691614d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JPAImmutableTest.java @@ -0,0 +1,130 @@ +package com.vladmihalcea.hpjp.hibernate.mapping; + +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import org.hibernate.HibernateException; +import org.hibernate.annotations.Immutable; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Date; +import java.util.Locale; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class JPAImmutableTest extends AbstractSQLServerIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.IMMUTABLE_ENTITY_UPDATE_QUERY_HANDLING_MODE, ImmutableEntityUpdateQueryHandlingMode.EXCEPTION); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Event event = new Event(1L, "Temperature", "25"); + + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals("25", event.getEventValue()); + + ReflectionUtils.setFieldValue(event, "eventValue", "10"); + assertEquals("10", event.getEventValue()); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals("25", event.getEventValue()); + }); + + try { + doInJPA(entityManager -> { + entityManager.createQuery( + "update Event " + + "set eventValue = :eventValue " + + "where id = :id") + .setParameter("eventValue", "10") + .setParameter("id", 1L) + .executeUpdate(); + }); + + fail("Should have thrown Exception"); + } catch (HibernateException expected) { + assertTrue( + expected.getMessage().toLowerCase(Locale.ROOT).contains( + "attempts to update an immutable entity: [event]" + ) + ); + } + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals("25", event.getEventValue()); + }); + } + + @Entity(name = "Event") + @Immutable + public static class Event { + + @Id + private Long id; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on", updatable = false) + private Date createdOn = new Date(); + + @Column(name = "event_key", updatable = false) + private String eventKey; + + @Column(name = "event_value", updatable = false) + private String eventValue; + + public Event(Long id, String eventKey, String eventValue) { + this.id = id; + this.eventKey = eventKey; + this.eventValue = eventValue; + } + + private Event() { + } + + public Long getId() { + return id; + } + + public Date getCreatedOn() { + return createdOn; + } + + public String getEventKey() { + return eventKey; + } + + public String getEventValue() { + return eventValue; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/JoinFormulaCollationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JoinFormulaCollationTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/JoinFormulaCollationTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JoinFormulaCollationTest.java index 1aff246e1..f958e08f3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/JoinFormulaCollationTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JoinFormulaCollationTest.java @@ -1,13 +1,13 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping; +package com.vladmihalcea.hpjp.hibernate.mapping; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.hibernate.annotations.JoinFormula; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.ManyToOne; -import javax.persistence.Table; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import java.util.Objects; import static org.junit.Assert.assertEquals; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JoinFormulaLastMonthSalaryTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JoinFormulaLastMonthSalaryTest.java new file mode 100644 index 000000000..d56cc5886 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JoinFormulaLastMonthSalaryTest.java @@ -0,0 +1,309 @@ +package com.vladmihalcea.hpjp.hibernate.mapping; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.annotations.JoinFormula; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JoinFormulaLastMonthSalaryTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Employee.class, + Salary.class + }; + } + + @Override + protected void afterInit() { + Employee alice = new Employee(); + alice.setId(1L); + alice.setName("Alice"); + alice.setTitle("CEO"); + + Employee bob = new Employee(); + bob.setId(2L); + bob.setName("Bob"); + bob.setTitle("Developer"); + + doInJPA( entityManager -> { + entityManager.persist(alice); + entityManager.persist(bob); + } ); + + doInJPA( entityManager -> { + Salary aliceSalary201511 = new Salary(); + aliceSalary201511.setId(1L); + aliceSalary201511.setEmployee(alice); + aliceSalary201511.setYear(2015); + aliceSalary201511.setMonth(11); + aliceSalary201511.setAmountCents(10_000); + + entityManager.persist(aliceSalary201511); + + Salary bobSalary201511 = new Salary(); + bobSalary201511.setId(2L); + bobSalary201511.setEmployee(bob); + bobSalary201511.setYear(2015); + bobSalary201511.setMonth(11); + bobSalary201511.setAmountCents(7_000); + + entityManager.persist(bobSalary201511); + + Salary aliceSalary201512 = new Salary(); + aliceSalary201512.setId(3L); + aliceSalary201512.setEmployee(alice); + aliceSalary201512.setYear(2015); + aliceSalary201512.setMonth(12); + aliceSalary201512.setAmountCents(11_000); + + entityManager.persist(aliceSalary201512); + + Salary bobSalary201512 = new Salary(); + bobSalary201512.setId(4L); + bobSalary201512.setEmployee(bob); + bobSalary201512.setYear(2015); + bobSalary201512.setMonth(12); + bobSalary201512.setAmountCents(7_500); + + entityManager.persist(bobSalary201512); + + Salary aliceSalary201601 = new Salary(); + aliceSalary201601.setId(5L); + aliceSalary201601.setEmployee(alice); + aliceSalary201601.setYear(2016); + aliceSalary201601.setMonth(1); + aliceSalary201601.setAmountCents(11_500); + + entityManager.persist(aliceSalary201601); + + Salary bobSalary201601 = new Salary(); + bobSalary201601.setId(6L); + bobSalary201601.setEmployee(bob); + bobSalary201601.setYear(2016); + bobSalary201601.setMonth(1); + bobSalary201601.setAmountCents(7_900); + + entityManager.persist(bobSalary201601); + + Salary aliceSalary201602 = new Salary(); + aliceSalary201602.setId(7L); + aliceSalary201602.setEmployee(alice); + aliceSalary201602.setYear(2016); + aliceSalary201602.setMonth(2); + aliceSalary201602.setAmountCents(11_900); + + entityManager.persist(aliceSalary201602); + + Salary bobSalary201602 = new Salary(); + bobSalary201602.setId(8L); + bobSalary201602.setEmployee(bob); + bobSalary201602.setYear(2016); + bobSalary201602.setMonth(2); + bobSalary201602.setAmountCents(8_500); + + entityManager.persist(bobSalary201602); + } ); + + assertEquals(Long.valueOf(1L), getPreviousSalaryId(3L)); + assertEquals(Long.valueOf(2L), getPreviousSalaryId(4L)); + assertEquals(Long.valueOf(3L), getPreviousSalaryId(5L)); + assertEquals(Long.valueOf(4L), getPreviousSalaryId(6L)); + assertEquals(Long.valueOf(5L), getPreviousSalaryId(7L)); + assertEquals(Long.valueOf(6L), getPreviousSalaryId(8L)); + } + + @Test + @Ignore + public void test() { + doInJPA( entityManager -> { + assertEquals( + Long.valueOf(1L), + entityManager.find(Salary.class, 3L) + .getPreviousMonthSalary().getId() + ); + + assertEquals( + Long.valueOf(2L), + entityManager.find(Salary.class, 4L) + .getPreviousMonthSalary().getId() + ); + + assertEquals( + Long.valueOf(3L), + entityManager.find(Salary.class, 5L) + .getPreviousMonthSalary().getId() + ); + + assertEquals( + Long.valueOf(4L), + entityManager.find(Salary.class, 6L) + .getPreviousMonthSalary().getId() + ); + + assertEquals( + Long.valueOf(5L), + entityManager.find(Salary.class, 7L) + .getPreviousMonthSalary().getId() + ); + + assertEquals( + Long.valueOf(6L), + entityManager.find(Salary.class, 8L) + .getPreviousMonthSalary().getId() + ); + } ); + } + + private Long getPreviousSalaryId(long salaryId) { + return doInJPA( entityManager -> { + Salary salary = entityManager.find(Salary.class, salaryId); + + Number prevSalaryId = (Number) entityManager.createNativeQuery( + "SELECT prev_salary.id " + + "FROM salary prev_salary " + + "WHERE " + + " prev_salary.employee_id = :employeeId AND " + + " ( CASE WHEN :month = 1 " + + " THEN prev_salary.year + 1 = :year AND " + + " prev_salary.month = 12 " + + " ELSE prev_salary.year = :year AND " + + " prev_salary.month + 1 = :month " + + " END ) = true ") + .setParameter("employeeId", salary.getEmployee().getId()) + .setParameter("year", salary.getYear()) + .setParameter("month", salary.getMonth()) + .getSingleResult(); + + return prevSalaryId.longValue(); + } ); + } + + @Entity(name = "Employee") + @Table(name = "employee") + public static class Employee { + + @Id + private Long id; + + private String name; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "Salary") + @Table(name = "salary") + public static class Salary { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Employee employee; + + private int month; + + private int year; + + @Column(name = "amount_cents") + private long amountCents; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinFormula(""" + ( + SELECT prev_salary.id + FROM salary prev_salary + WHERE + prev_salary.employee_id = employee_id AND + ( + CASE WHEN month = 1 + THEN prev_salary.year + 1 = year AND + prev_salary.month = 12 + ELSE prev_salary.year = year AND + prev_salary.month + 1 = month + END + ) = true + ) + """ + ) + private Salary previousMonthSalary; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Employee getEmployee() { + return employee; + } + + public void setEmployee(Employee employee) { + this.employee = employee; + } + + public int getMonth() { + return month; + } + + public void setMonth(int month) { + this.month = month; + } + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } + + public long getAmountCents() { + return amountCents; + } + + public void setAmountCents(long amountCents) { + this.amountCents = amountCents; + } + + public Salary getPreviousMonthSalary() { + return previousMonthSalary; + } + } + + +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/JoinFormulaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JoinFormulaTest.java similarity index 95% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/JoinFormulaTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JoinFormulaTest.java index 352aba400..6dbe744e3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/JoinFormulaTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/JoinFormulaTest.java @@ -1,15 +1,14 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping; +package com.vladmihalcea.hpjp.hibernate.mapping; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.hibernate.annotations.JoinFormula; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.ManyToOne; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; import java.sql.Statement; -import java.util.List; import java.util.Locale; import java.util.Objects; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/LatestChildJoinFormulaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/LatestChildJoinFormulaTest.java new file mode 100644 index 000000000..f5fc86767 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/LatestChildJoinFormulaTest.java @@ -0,0 +1,205 @@ +package com.vladmihalcea.hpjp.hibernate.mapping; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.annotations.JoinFormula; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class LatestChildJoinFormulaTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Test + public void test() { + + doInJPA( entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + + assertNull(post.getLatestComment()); + + entityManager.persist( + new PostComment() + .setId(1L) + .setPost(post) + .setCreatedOn( + Timestamp.valueOf( + LocalDateTime.of(2016, 11, 2, 12, 33, 14) + ) + ) + .setReview("Woohoo!") + ); + + entityManager.persist( + new PostComment() + .setId(2L) + .setPost(post) + .setCreatedOn( + Timestamp.valueOf( + LocalDateTime.of(2016, 11, 2, 15, 45, 58) + ) + ) + .setReview("Finally!") + ); + + entityManager.persist( + new PostComment() + .setId(3L) + .setPost(post) + .setCreatedOn( + Timestamp.valueOf( + LocalDateTime.of(2017, 2, 16, 16, 10, 21) + ) + ) + .setReview("Awesome!") + ); + } ); + + doInJPA( entityManager -> { + Post post = entityManager.find(Post.class, 1L); + PostComment latestComment = post.getLatestComment(); + + assertEquals("Awesome!", latestComment.getReview()); + } ); + + doInJPA( entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + join fetch p.latestComment + """, Post.class) + .getResultList(); + + assertEquals("Awesome!", posts.get(0).getLatestComment().getReview()); + } ); + + doInJPA( entityManager -> { + entityManager.persist( + new Post() + .setId(2L) + .setTitle("High-Performance Java Persistence 2nd edition") + ); + } ); + + doInJPA( entityManager -> { + Post post = entityManager.find(Post.class, 2L); + assertNull(post.getLatestComment()); + } ); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinFormula(""" + (SELECT pc.id + FROM post_comment pc + WHERE pc.post_id = id + ORDER BY pc.created_on DESC + LIMIT 1) + """ + ) + private PostComment latestComment; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostComment getLatestComment() { + return latestComment; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + @Column(name = "created_on") + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/MapsIdWithManyToOneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/MapsIdWithManyToOneTest.java similarity index 85% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/MapsIdWithManyToOneTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/MapsIdWithManyToOneTest.java index d9ee1d6a0..3c27b5a9f 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/MapsIdWithManyToOneTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/MapsIdWithManyToOneTest.java @@ -1,23 +1,19 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping; +package com.vladmihalcea.hpjp.hibernate.mapping; import java.io.Serializable; -import java.util.List; import java.util.Objects; -import javax.persistence.Column; -import javax.persistence.Embeddable; -import javax.persistence.EmbeddedId; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.ManyToOne; -import javax.persistence.MapsId; -import javax.persistence.OneToMany; -import javax.persistence.OneToOne; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; import org.junit.Test; -import com.vladmihalcea.book.hpjp.util.AbstractSQLServerIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/MapsIdWithOneToOneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/MapsIdWithOneToOneTest.java similarity index 95% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/MapsIdWithOneToOneTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/MapsIdWithOneToOneTest.java index 4bdd93cc3..a180c2667 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/MapsIdWithOneToOneTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/MapsIdWithOneToOneTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping; +package com.vladmihalcea.hpjp.hibernate.mapping; -import com.vladmihalcea.book.hpjp.util.AbstractSQLServerIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serializable; import java.util.List; import java.util.Objects; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/MultipleEntitiesOneTableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/MultipleEntitiesOneTableTest.java new file mode 100644 index 000000000..007e59b9d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/MultipleEntitiesOneTableTest.java @@ -0,0 +1,172 @@ +package com.vladmihalcea.hpjp.hibernate.mapping; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; +import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MultipleEntitiesOneTableTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class, + BookSummary.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea") + .setProperties(""" + { + "publisher": "Amazon", + "price": 44.99, + "publication_date": "2016-20-12", + "dimensions": "8.5 x 1.1 x 11 inches", + "weight": "2.5 pounds", + "average_review": "4.7 out of 5 stars", + "url": "https://amzn.com/973022823X" + } + """ + ) + ); + + entityManager.persist( + new BookSummary() + .setIsbn("978-1934356555") + .setTitle("SQL Antipatterns") + .setAuthor("Bill Karwin") + ); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + BookSummary bookSummary = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(BookSummary.class) + .load("978-9730228236"); + + assertEquals("High-Performance Java Persistence", bookSummary.getTitle()); + + bookSummary.setTitle("High-Performance Java Persistence, 2nd edition"); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + + assertEquals("High-Performance Java Persistence, 2nd edition", book.getTitle()); + + ObjectNode jsonProperties = book.getJsonProperties(); + assertEquals("4.7 out of 5 stars", jsonProperties.get("average_review").asText()); + + jsonProperties.put("average_review", "4.8 out of 5 stars"); + book.setProperties(JacksonUtil.toString(jsonProperties)); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + @DynamicUpdate + public static class Book extends BaseBook { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private String properties; + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + + public ObjectNode getJsonProperties() { + return (ObjectNode) JacksonUtil.toJsonNode(properties); + } + } + + @Entity(name = "BookSummary") + @Table(name = "book") + public static class BookSummary extends BaseBook { + + } + + @MappedSuperclass + public static abstract class BaseBook { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + @Column(length = 15) + private String isbn; + + @Column(length = 50) + private String title; + + @Column(length = 50) + private String author; + + public Long getId() { + return id; + } + + public T setId(Long id) { + this.id = id; + return (T) this; + } + + public String getIsbn() { + return isbn; + } + + public T setIsbn(String isbn) { + this.isbn = isbn; + return (T) this; + } + + public String getTitle() { + return title; + } + + public T setTitle(String title) { + this.title = title; + return (T) this; + } + + public String getAuthor() { + return author; + } + + public T setAuthor(String author) { + this.author = author; + return (T) this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/OptionalAttributeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/OptionalAttributeTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/OptionalAttributeTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/OptionalAttributeTest.java index bb6c864db..fd9a27b25 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/OptionalAttributeTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/OptionalAttributeTest.java @@ -1,10 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping; +package com.vladmihalcea.hpjp.hibernate.mapping; -import com.vladmihalcea.book.hpjp.hibernate.forum.MediaType; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.hibernate.forum.MediaType; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.Serializable; import java.util.List; import java.util.Optional; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/calculated/FormulaCurrentDateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/calculated/FormulaCurrentDateTest.java new file mode 100644 index 000000000..f44b8ea86 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/calculated/FormulaCurrentDateTest.java @@ -0,0 +1,68 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.calculated; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.annotations.Formula; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.Date; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class FormulaCurrentDateTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class, + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Event event = new Event(); + event.setId(1L); + + entityManager.persist(event); + entityManager.flush(); + + entityManager.refresh(event); + + assertNotNull(event.getCreatedOn()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + private Long id; + + @Formula("(SELECT current_date)") + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/calculated/FormulaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/calculated/FormulaTest.java similarity index 83% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/calculated/FormulaTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/calculated/FormulaTest.java index 438cba3dd..a8a29aa6f 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/calculated/FormulaTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/calculated/FormulaTest.java @@ -1,10 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping.calculated; +package com.vladmihalcea.hpjp.hibernate.mapping.calculated; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.hibernate.annotations.Formula; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -75,23 +75,25 @@ public static class Account { @Formula("cents::numeric / 100") private double dollars; - @Formula( - "round(" + - " (interestRate::numeric / 100) * " + - " cents * " + - " date_part('month', age(now(), createdOn)" + - ") " + - "/ 12)") + @Formula(""" + round( + (interestRate::numeric / 100) * + cents * + date_part('month', age(now(), createdOn) + ) + / 12) + """) private long interestCents; - @Formula( - "round(" + - " (interestRate::numeric / 100) * " + - " cents * " + - " date_part('month', age(now(), createdOn)" + - ") " + - "/ 12) " + - "/ 100::numeric") + @Formula(""" + round( + (interestRate::numeric / 100) * + cents * + date_part('month', age(now(), createdOn) + ) + / 12) + / 100::numeric + """) private double interestDollars; public Account() { diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/calculated/JPACalculatedPostLoadTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/calculated/JPACalculatedPostLoadTest.java new file mode 100644 index 000000000..580e838e8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/calculated/JPACalculatedPostLoadTest.java @@ -0,0 +1,168 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.calculated; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JPACalculatedPostLoadTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Account.class, + User.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + User user = new User(); + user.setId(1L); + user.setFirstName("John"); + user.setFirstName("Doe"); + + entityManager.persist(user); + + Account account = new Account( + 1L, + user, + "ABC123", + 12345L, + 6.7, + Timestamp.valueOf( + LocalDateTime.now().minusMonths(3) + ) + ); + + entityManager.persist(account); + }); + doInJPA(entityManager -> { + Account account = entityManager.find(Account.class, 1L); + + assertEquals(123.45D, account.getDollars(), 0.001); + assertEquals(207L, account.getInterestCents()); + assertEquals(2.07D, account.getInterestDollars(), 0.001); + }); + } + + @Entity(name = "Account") + @Table(name = "account") + public static class Account { + + @Id + private Long id; + + @ManyToOne + private User owner; + + private String iban; + + private long cents; + + private double interestRate; + + private Timestamp createdOn; + + @Transient + private double dollars; + + @Transient + private long interestCents; + + @Transient + private double interestDollars; + + public Account() { + } + + public Account( + Long id, User owner, String iban, + long cents, double interestRate, Timestamp createdOn) { + this.id = id; + this.owner = owner; + this.iban = iban; + this.cents = cents; + this.interestRate = interestRate; + this.createdOn = createdOn; + } + + @PostLoad + private void postLoad() { + this.dollars = cents / 100D; + + long months = createdOn.toLocalDateTime().until( + LocalDateTime.now(), + ChronoUnit.MONTHS) + ; + + double interestUnrounded = ( + (interestRate / 100D) * cents * months + ) / 12; + + this.interestCents = BigDecimal.valueOf(interestUnrounded) + .setScale(0, BigDecimal.ROUND_HALF_EVEN) + .longValue(); + + this.interestDollars = interestCents / 100D; + } + + public double getDollars() { + return dollars; + } + + public long getInterestCents() { + return interestCents; + } + + public double getInterestDollars() { + return interestDollars; + } + } + + @Entity(name = "User") + @Table(name = "`user`") + public static class User { + + @Id + private Long id; + + private String firstName; + + private String lastName; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/calculated/JpaCalculatedTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/calculated/JpaCalculatedTest.java similarity index 80% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/calculated/JpaCalculatedTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/calculated/JpaCalculatedTest.java index 343234cfa..ee1a4f451 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/mapping/calculated/JpaCalculatedTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/calculated/JpaCalculatedTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.mapping.calculated; +package com.vladmihalcea.hpjp.hibernate.mapping.calculated; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.math.BigDecimal; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -77,7 +77,9 @@ public static class Account { public Account() { } - public Account(Long id, User owner, String iban, long cents, double interestRate, Timestamp createdOn) { + public Account( + Long id, User owner, String iban, + long cents, double interestRate, Timestamp createdOn) { this.id = id; this.owner = owner; this.iban = iban; @@ -86,19 +88,25 @@ public Account(Long id, User owner, String iban, long cents, double interestRate this.createdOn = createdOn; } - @Transient public double getDollars() { return cents / 100D; } - @Transient public long getInterestCents() { - long months = createdOn.toLocalDateTime().until(LocalDateTime.now(), ChronoUnit.MONTHS); - double interestUnrounded = ( ( interestRate / 100D ) * cents * months ) / 12; - return BigDecimal.valueOf(interestUnrounded).setScale(0, BigDecimal.ROUND_HALF_EVEN).longValue(); + long months = createdOn.toLocalDateTime().until( + LocalDateTime.now(), + ChronoUnit.MONTHS + ); + + double interestUnrounded = ( + (interestRate / 100D) * cents * months + ) / 12; + + return BigDecimal.valueOf(interestUnrounded) + .setScale(0, BigDecimal.ROUND_HALF_EVEN) + .longValue(); } - @Transient public double getInterestDollars() { return getInterestCents() / 100D; } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/MySQLCountryByteIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/MySQLCountryByteIdTest.java new file mode 100644 index 000000000..386c33873 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/MySQLCountryByteIdTest.java @@ -0,0 +1,194 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.compact; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; +import jakarta.persistence.*; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Vlad Mihalcea + */ +public class MySQLCountryByteIdTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Country.class, + Customer.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.STATEMENT_BATCH_SIZE, "1000"); + properties.put(AvailableSettings.ORDER_INSERTS, Boolean.TRUE); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new MySQLDataSourceProvider().setRewriteBatchedStatements(true); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Country() + .setId((short) 1) + .setName("Romania") + ); + }); + } + + @Test + public void testOverheadImpact() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + int customersPerCountry = 25_000; + doInJPA(entityManager -> { + AtomicInteger customerId = new AtomicInteger(); + for (short i = 1; i <= 200; i++) { + Country country = new Country() + .setId(i) + .setName(String.format("Country no. %d", i)); + entityManager.persist(country); + for (int j = 1; j <= customersPerCountry; j++) { + entityManager.persist( + new Customer() + .setId(customerId.incrementAndGet()) + .setCountry(country) + .setFirstName("Vlad") + .setFirstName("Mihalcea") + ); + } + } + }); + + executeStatement("CREATE INDEX idx_customer_country_id ON customer (country_id)"); + executeQuery("ANALYZE TABLE customer"); + + doInJPA(entityManager -> { + LOGGER.info( + "Total customer table size: {} MB", + entityManager + .createNativeQuery(""" + select + ROUND(((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024), 2) + from information_schema.TABLES + where TABLE_SCHEMA = 'high_performance_java_persistence' AND TABLE_NAME = 'customer' + """) + .getSingleResult() + ); + }); + + doInJPA(entityManager -> { + LOGGER.info( + "Total customer index size: {} MB", + entityManager + .createNativeQuery(""" + select + ROUND((INDEX_LENGTH / 1024 / 1024), 2) + from information_schema.TABLES + where TABLE_SCHEMA = 'high_performance_java_persistence' AND TABLE_NAME = 'customer' + """) + .getSingleResult() + ); + }); + } + + @Entity(name = "Country") + @Table(name = "country") + public static class Country { + + @Id + @Column(columnDefinition = "tinyint unsigned") + private Short id; + + @Column(columnDefinition = "varchar(100)") + private String name; + + public Short getId() { + return id; + } + + public Country setId(Short id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Country setName(String name) { + this.name = name; + return this; + } + } + + @Entity(name = "Customer") + @Table(name = "customer") + public class Customer { + + @Id + private Integer id; + + @Column(name = "first_name", columnDefinition = "varchar(100)") + private String firstName; + + @Column(name = "last_name", columnDefinition = "varchar(100)") + private String lastName; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "country_id") + private Country country; + + public Integer getId() { + return id; + } + + public Customer setId(Integer id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public Customer setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public Customer setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public Country getCountry() { + return country; + } + + public Customer setCountry(Country country) { + this.country = country; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/MySQLCountryShortIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/MySQLCountryShortIdTest.java new file mode 100644 index 000000000..71dd7cc19 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/MySQLCountryShortIdTest.java @@ -0,0 +1,194 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.compact; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; +import jakarta.persistence.*; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Vlad Mihalcea + */ +public class MySQLCountryShortIdTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Country.class, + Customer.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.STATEMENT_BATCH_SIZE, "1000"); + properties.put(AvailableSettings.ORDER_INSERTS, Boolean.TRUE); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new MySQLDataSourceProvider().setRewriteBatchedStatements(true); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Country() + .setId((short) 1) + .setName("Romania") + ); + }); + } + + @Test + public void testOverheadImpact() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + int customersPerCountry = 25_000; + doInJPA(entityManager -> { + AtomicInteger customerId = new AtomicInteger(); + for (short i = 1; i <= 200; i++) { + Country country = new Country() + .setId(i) + .setName(String.format("Country no. %d", i)); + entityManager.persist(country); + for (int j = 1; j <= customersPerCountry; j++) { + entityManager.persist( + new Customer() + .setId(customerId.incrementAndGet()) + .setCountry(country) + .setFirstName("Vlad") + .setFirstName("Mihalcea") + ); + } + } + }); + + executeStatement("CREATE INDEX idx_customer_country_id ON customer (country_id)"); + executeQuery("ANALYZE TABLE customer"); + + doInJPA(entityManager -> { + LOGGER.info( + "Total customer table size: {} MB", + entityManager + .createNativeQuery(""" + select + ROUND(((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024), 2) + from information_schema.TABLES + where TABLE_SCHEMA = 'high_performance_java_persistence' AND TABLE_NAME = 'customer' + """) + .getSingleResult() + ); + }); + + doInJPA(entityManager -> { + LOGGER.info( + "Total customer index size: {} MB", + entityManager + .createNativeQuery(""" + select + ROUND((INDEX_LENGTH / 1024 / 1024), 2) + from information_schema.TABLES + where TABLE_SCHEMA = 'high_performance_java_persistence' AND TABLE_NAME = 'customer' + """) + .getSingleResult() + ); + }); + } + + @Entity(name = "Country") + @Table(name = "country") + public static class Country { + + @Id + @Column(columnDefinition = "smallint unsigned") + private Short id; + + @Column(columnDefinition = "varchar(100)") + private String name; + + public Short getId() { + return id; + } + + public Country setId(Short id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Country setName(String name) { + this.name = name; + return this; + } + } + + @Entity(name = "Customer") + @Table(name = "customer", indexes = @Index(name ="FK_customer_country_id", columnList = "country_id")) + public class Customer { + + @Id + private Integer id; + + @Column(name = "first_name", columnDefinition = "varchar(100)") + private String firstName; + + @Column(name = "last_name", columnDefinition = "varchar(100)") + private String lastName; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "country_id") + private Country country; + + public Integer getId() { + return id; + } + + public Customer setId(Integer id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public Customer setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public Customer setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public Country getCountry() { + return country; + } + + public Customer setCountry(Country country) { + this.country = country; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/PostgreSQLCountryIntIdAutoPaddingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/PostgreSQLCountryIntIdAutoPaddingTest.java new file mode 100644 index 000000000..c30d1951c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/PostgreSQLCountryIntIdAutoPaddingTest.java @@ -0,0 +1,177 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.compact; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.tool.schema.Action; +import org.junit.Test; + +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLCountryIntIdAutoPaddingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Country.class, + Customer.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.HBM2DDL_AUTO, Action.NONE.getExternalHbm2ddlName()); + properties.put(AvailableSettings.STATEMENT_BATCH_SIZE, "100"); + properties.put(AvailableSettings.ORDER_INSERTS, Boolean.TRUE); + } + + @Override + protected void beforeInit() { + executeStatement("alter table if exists customer drop constraint if exists FK_customer_country_id"); + executeStatement("drop table if exists customer cascade"); + executeStatement("drop table if exists country cascade"); + executeStatement("drop sequence if exists country_SEQ"); + executeStatement("create sequence country_SEQ start with 1 increment by 50"); + executeStatement("create table country (id smallint not null, name varchar(100), primary key (id))"); + executeStatement("create table customer (country_id smallint, id integer not null, first_name varchar(100), last_name varchar(100), primary key (id))"); + executeStatement("alter table if exists customer add constraint FK_customer_country_id foreign key (country_id) references country"); + } + + @Test + public void testOverheadImpact() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + int customersPerCountry = 25_000; + doInJPA(entityManager -> { + AtomicInteger customerId = new AtomicInteger(); + for (short i = 1; i <= 200; i++) { + Country country = new Country() + .setName(String.format("Country no. %d", i)); + entityManager.persist(country); + for (int j = 1; j <= customersPerCountry; j++) { + entityManager.persist( + new Customer() + .setId(customerId.incrementAndGet()) + .setCountry(country) + .setFirstName("Vlad") + .setFirstName("Mihalcea") + ); + } + } + }); + + executeStatement("CREATE INDEX IF NOT EXISTS idx_customer_country_id ON customer (country_id)"); + executeStatement("VACUUM FULL ANALYZE"); + + doInJPA(entityManager -> { + LOGGER.info( + "Total customer table size: {}", + entityManager + .createNativeQuery("select pg_size_pretty(pg_total_relation_size('customer'))") + .getSingleResult() + ); + LOGGER.info( + "Total customer index size: {}", + entityManager + .createNativeQuery("select pg_size_pretty(pg_table_size('idx_customer_country_id'))") + .getSingleResult() + ); + }); + } + + @Entity(name = "Country") + @Table(name = "country") + public static class Country { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(columnDefinition = "int") + private Integer id; + + @Column(columnDefinition = "varchar(100)") + private String name; + + public Integer getId() { + return id; + } + + public Country setId(Integer id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Country setName(String name) { + this.name = name; + return this; + } + } + + @Entity(name = "Customer") + @Table(name = "customer") + public class Customer { + + @Id + private Integer id; + + @Column(name = "first_name", columnDefinition = "varchar(100)") + private String firstName; + + @Column(name = "last_name", columnDefinition = "varchar(100)") + private String lastName; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "country_id") + private Country country; + + public Integer getId() { + return id; + } + + public Customer setId(Integer id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public Customer setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public Customer setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public Country getCountry() { + return country; + } + + public Customer setCountry(Country country) { + this.country = country; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/PostgreSQLCountryIntIdNoPaddingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/PostgreSQLCountryIntIdNoPaddingTest.java new file mode 100644 index 000000000..aa3acd6a9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/PostgreSQLCountryIntIdNoPaddingTest.java @@ -0,0 +1,183 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.compact; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.tool.schema.Action; +import org.junit.Test; + +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLCountryIntIdNoPaddingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Country.class, + Customer.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.HBM2DDL_AUTO, Action.NONE.getExternalHbm2ddlName()); + properties.put(AvailableSettings.STATEMENT_BATCH_SIZE, "100"); + properties.put(AvailableSettings.ORDER_INSERTS, Boolean.TRUE); + } + + @Override + protected void beforeInit() { + executeStatement("alter table if exists customer drop constraint if exists FK_customer_country_id"); + executeStatement("drop table if exists customer cascade"); + executeStatement("drop table if exists country cascade"); + executeStatement("drop sequence if exists country_SEQ"); + executeStatement("create sequence country_SEQ start with 1 increment by 50"); + executeStatement("create table country (name varchar(100), id int not null, primary key (id))"); + executeStatement("create table customer (id integer not null, first_name varchar(100), last_name varchar(100), country_id int, primary key (id))"); + executeStatement("alter table if exists customer add constraint FK_customer_country_id foreign key (country_id) references country"); + } + + @Test + public void testOverheadImpact() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + int customersPerCountry = 25_000; + doInJPA(entityManager -> { + AtomicInteger customerId = new AtomicInteger(); + for (short i = 1; i <= 200; i++) { + Country country = new Country() + .setName(String.format("Country no. %d", i)); + entityManager.persist(country); + for (int j = 1; j <= customersPerCountry; j++) { + entityManager.persist( + new Customer() + .setId(customerId.incrementAndGet()) + .setCountry(country) + .setFirstName("Vlad") + .setFirstName("Mihalcea") + ); + } + } + }); + + executeStatement("CREATE INDEX IF NOT EXISTS idx_customer_country_id ON customer (country_id)"); + executeStatement("VACUUM FULL ANALYZE"); + + doInJPA(entityManager -> { + LOGGER.info( + "Total customer table size: {}", + entityManager + .createNativeQuery("select pg_size_pretty(pg_table_size('customer'))") + .getSingleResult() + ); + LOGGER.info( + "Total customer index size: {}", + entityManager + .createNativeQuery("select pg_size_pretty(pg_table_size('idx_customer_country_id'))") + .getSingleResult() + ); + LOGGER.info( + "Total customer index size in bytes: {}", + entityManager + .createNativeQuery("select pg_table_size('idx_customer_country_id')") + .getSingleResult() + ); + }); + } + + @Entity(name = "Country") + @Table(name = "country") + public static class Country { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(columnDefinition = "int") + private Integer id; + + @Column(columnDefinition = "varchar(100)") + private String name; + + public Integer getId() { + return id; + } + + public Country setId(Integer id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Country setName(String name) { + this.name = name; + return this; + } + } + + @Entity(name = "Customer") + @Table(name = "customer") + public class Customer { + + @Id + private Integer id; + + @Column(name = "first_name", columnDefinition = "varchar(100)") + private String firstName; + + @Column(name = "last_name", columnDefinition = "varchar(100)") + private String lastName; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "country_id") + private Country country; + + public Integer getId() { + return id; + } + + public Customer setId(Integer id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public Customer setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public Customer setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public Country getCountry() { + return country; + } + + public Customer setCountry(Country country) { + this.country = country; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/PostgreSQLCountryShortIdAutoPaddingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/PostgreSQLCountryShortIdAutoPaddingTest.java new file mode 100644 index 000000000..1a20182a2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/PostgreSQLCountryShortIdAutoPaddingTest.java @@ -0,0 +1,177 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.compact; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.tool.schema.Action; +import org.junit.Test; + +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLCountryShortIdAutoPaddingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Country.class, + Customer.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.HBM2DDL_AUTO, Action.NONE.getExternalHbm2ddlName()); + properties.put(AvailableSettings.STATEMENT_BATCH_SIZE, "100"); + properties.put(AvailableSettings.ORDER_INSERTS, Boolean.TRUE); + } + + @Override + protected void beforeInit() { + executeStatement("alter table if exists customer drop constraint if exists FK_customer_country_id"); + executeStatement("drop table if exists customer cascade"); + executeStatement("drop table if exists country cascade"); + executeStatement("drop sequence if exists country_SEQ"); + executeStatement("create sequence country_SEQ start with 1 increment by 50"); + executeStatement("create table country (id smallint not null, name varchar(100), primary key (id))"); + executeStatement("create table customer (country_id smallint, id integer not null, first_name varchar(100), last_name varchar(100), primary key (id))"); + executeStatement("alter table if exists customer add constraint FK_customer_country_id foreign key (country_id) references country"); + } + + @Test + public void testOverheadImpact() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + int customersPerCountry = 25_000; + doInJPA(entityManager -> { + AtomicInteger customerId = new AtomicInteger(); + for (short i = 1; i <= 200; i++) { + Country country = new Country() + .setName(String.format("Country no. %d", i)); + entityManager.persist(country); + for (int j = 1; j <= customersPerCountry; j++) { + entityManager.persist( + new Customer() + .setId(customerId.incrementAndGet()) + .setCountry(country) + .setFirstName("Vlad") + .setFirstName("Mihalcea") + ); + } + } + }); + + executeStatement("CREATE INDEX IF NOT EXISTS idx_customer_country_id ON customer (country_id)"); + executeStatement("VACUUM FULL ANALYZE"); + + doInJPA(entityManager -> { + LOGGER.info( + "Total customer table size: {}", + entityManager + .createNativeQuery("select pg_size_pretty(pg_total_relation_size('customer'))") + .getSingleResult() + ); + LOGGER.info( + "Total customer index size: {}", + entityManager + .createNativeQuery("select pg_size_pretty(pg_table_size('idx_customer_country_id'))") + .getSingleResult() + ); + }); + } + + @Entity(name = "Country") + @Table(name = "country") + public static class Country { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(columnDefinition = "smallint") + private Short id; + + @Column(columnDefinition = "varchar(100)") + private String name; + + public Short getId() { + return id; + } + + public Country setId(Short id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Country setName(String name) { + this.name = name; + return this; + } + } + + @Entity(name = "Customer") + @Table(name = "customer") + public class Customer { + + @Id + private Integer id; + + @Column(name = "first_name", columnDefinition = "varchar(100)") + private String firstName; + + @Column(name = "last_name", columnDefinition = "varchar(100)") + private String lastName; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "country_id") + private Country country; + + public Integer getId() { + return id; + } + + public Customer setId(Integer id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public Customer setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public Customer setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public Country getCountry() { + return country; + } + + public Customer setCountry(Country country) { + this.country = country; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/PostgreSQLCountryShortIdNoPaddingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/PostgreSQLCountryShortIdNoPaddingTest.java new file mode 100644 index 000000000..6ca081a24 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/compact/PostgreSQLCountryShortIdNoPaddingTest.java @@ -0,0 +1,183 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.compact; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.tool.schema.Action; +import org.junit.Test; + +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLCountryShortIdNoPaddingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Country.class, + Customer.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.HBM2DDL_AUTO, Action.NONE.getExternalHbm2ddlName()); + properties.put(AvailableSettings.STATEMENT_BATCH_SIZE, "100"); + properties.put(AvailableSettings.ORDER_INSERTS, Boolean.TRUE); + } + + @Override + protected void beforeInit() { + executeStatement("alter table if exists customer drop constraint if exists FK_customer_country_id"); + executeStatement("drop table if exists customer cascade"); + executeStatement("drop table if exists country cascade"); + executeStatement("drop sequence if exists country_SEQ"); + executeStatement("create sequence country_SEQ start with 1 increment by 50"); + executeStatement("create table country (name varchar(100), id smallint not null, primary key (id))"); + executeStatement("create table customer (id integer not null, first_name varchar(100), last_name varchar(100), country_id smallint, primary key (id))"); + executeStatement("alter table if exists customer add constraint FK_customer_country_id foreign key (country_id) references country"); + } + + @Test + public void testOverheadImpact() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + int customersPerCountry = 25_000; + doInJPA(entityManager -> { + AtomicInteger customerId = new AtomicInteger(); + for (short i = 1; i <= 200; i++) { + Country country = new Country() + .setName(String.format("Country no. %d", i)); + entityManager.persist(country); + for (int j = 1; j <= customersPerCountry; j++) { + entityManager.persist( + new Customer() + .setId(customerId.incrementAndGet()) + .setCountry(country) + .setFirstName("Vlad") + .setFirstName("Mihalcea") + ); + } + } + }); + + executeStatement("CREATE INDEX IF NOT EXISTS idx_customer_country_id ON customer (country_id)"); + executeStatement("VACUUM FULL ANALYZE"); + + doInJPA(entityManager -> { + LOGGER.info( + "Total customer table size: {}", + entityManager + .createNativeQuery("select pg_size_pretty(pg_table_size('customer'))") + .getSingleResult() + ); + LOGGER.info( + "Total customer index size: {}", + entityManager + .createNativeQuery("select pg_size_pretty(pg_table_size('idx_customer_country_id'))") + .getSingleResult() + ); + LOGGER.info( + "Total customer index size in bytes: {}", + entityManager + .createNativeQuery("select pg_table_size('idx_customer_country_id')") + .getSingleResult() + ); + }); + } + + @Entity(name = "Country") + @Table(name = "country") + public static class Country { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(columnDefinition = "smallint") + private Short id; + + @Column(columnDefinition = "varchar(100)") + private String name; + + public Short getId() { + return id; + } + + public Country setId(Short id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Country setName(String name) { + this.name = name; + return this; + } + } + + @Entity(name = "Customer") + @Table(name = "customer") + public class Customer { + + @Id + private Integer id; + + @Column(name = "first_name", columnDefinition = "varchar(100)") + private String firstName; + + @Column(name = "last_name", columnDefinition = "varchar(100)") + private String lastName; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "country_id") + private Country country; + + public Integer getId() { + return id; + } + + public Customer setId(Integer id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public Customer setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public Customer setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public Country getCountry() { + return country; + } + + public Customer setCountry(Country country) { + this.country = country; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/embeddable/EmbeddableEntityListenerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/embeddable/EmbeddableEntityListenerTest.java new file mode 100644 index 000000000..b77d82aa6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/embeddable/EmbeddableEntityListenerTest.java @@ -0,0 +1,214 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.embeddable; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class EmbeddableEntityListenerTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Tag.class + }; + } + + @Test + public void test() { + LoggedUser.logIn("Alice"); + + doInJPA(entityManager -> { + Tag jdbc = new Tag(); + jdbc.setName("JDBC"); + entityManager.persist(jdbc); + + Tag hibernate = new Tag(); + hibernate.setName("Hibernate"); + entityManager.persist(hibernate); + + Tag jOOQ = new Tag(); + jOOQ.setName("jOOQ"); + entityManager.persist(jOOQ); + }); + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence, 1st Edition"); + + post.getTags().add(entityManager.find(Tag.class, "JDBC")); + post.getTags().add(entityManager.find(Tag.class, "Hibernate")); + post.getTags().add(entityManager.find(Tag.class, "jOOQ")); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + post.setTitle("High-Performance Java Persistence, 2nd Edition"); + }); + + LoggedUser.logOut(); + } + + public static class LoggedUser { + + private static final ThreadLocal userHolder = new ThreadLocal<>(); + + public static void logIn(String user) { + userHolder.set(user); + } + + public static void logOut() { + userHolder.remove(); + } + + public static String get() { + return userHolder.get(); + } + } + + @Embeddable + public static class Audit { + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_on") + private LocalDateTime updatedOn; + + @Column(name = "updated_by") + private String updatedBy; + + @PrePersist + public void prePersist() { + createdOn = LocalDateTime.now(); + createdBy = LoggedUser.get(); + } + + @PreUpdate + public void preUpdate() { + updatedOn = LocalDateTime.now(); + updatedBy = LoggedUser.get(); + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @Embedded + private Audit audit = new Audit(); + + private String title; + + @ManyToMany + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Audit getAudit() { + return audit; + } + + public void setAudit(Audit audit) { + this.audit = audit; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getTags() { + return tags; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + private String name; + + @Embedded + private Audit audit = new Audit(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Audit getAudit() { + return audit; + } + + public void setAudit(Audit audit) { + this.audit = audit; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/embeddable/EmbeddableInheritanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/embeddable/EmbeddableInheritanceTest.java new file mode 100644 index 000000000..802ee5452 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/embeddable/EmbeddableInheritanceTest.java @@ -0,0 +1,387 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.embeddable; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.GenericGenerator; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class EmbeddableInheritanceTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostDetails.class, + PostComment.class, + Tag.class + }; + } + + @Test + public void test() { + LoggedUser.logIn("Alice"); + + doInJPA(entityManager -> { + Tag jdbc = new Tag(); + jdbc.setName("JDBC"); + entityManager.persist(jdbc); + + Tag hibernate = new Tag(); + hibernate.setName("Hibernate"); + entityManager.persist(hibernate); + + Tag jOOQ = new Tag(); + jOOQ.setName("jOOQ"); + entityManager.persist(jOOQ); + }); + + byte[] imageBytes = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9}; + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence, 1st Edition"); + + PostDetails details = new PostDetails(); + details.setImage(imageBytes); + + post.setDetails(details); + + post.getTags().add(entityManager.find(Tag.class, "JDBC")); + post.getTags().add(entityManager.find(Tag.class, "Hibernate")); + post.getTags().add(entityManager.find(Tag.class, "jOOQ")); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + post.setTitle("High-Performance Java Persistence, 2nd Edition"); + }); + + LoggedUser.logOut(); + } + + public static class LoggedUser { + + private static final ThreadLocal userHolder = new ThreadLocal<>(); + + public static void logIn(String user) { + userHolder.set(user); + } + + public static void logOut() { + userHolder.remove(); + } + + public static String get() { + return userHolder.get(); + } + } + + public static class AuditListener { + + @PrePersist + public void setCreatedOn(Auditable auditable) { + Audit audit = auditable.getAudit(); + + if(audit == null) { + audit = new Audit(); + auditable.setAudit(audit); + } + + audit.setCreatedOn(LocalDateTime.now()); + audit.setCreatedBy(LoggedUser.get()); + } + + @PreUpdate + public void setUpdatedOn(Auditable auditable) { + Audit audit = auditable.getAudit(); + + audit.setUpdatedOn(LocalDateTime.now()); + audit.setUpdatedBy(LoggedUser.get()); + } + } + + public interface Auditable { + + Audit getAudit(); + + void setAudit(Audit audit); + } + + @MappedSuperclass + public static class BaseAudit { + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + } + + @Embeddable + public static class Audit extends BaseAudit { + + @Column(name = "updated_on") + private LocalDateTime updatedOn; + + @Column(name = "updated_by") + private String updatedBy; + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + } + } + + @Entity(name = "Post") + @Table(name = "post") + @EntityListeners(AuditListener.class) + public static class Post implements Auditable { + + @Id + private Long id; + + @Embedded + private Audit audit; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true, fetch = FetchType.LAZY) + private PostDetails details; + + @ManyToMany + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Audit getAudit() { + return audit; + } + + public void setAudit(Audit audit) { + this.audit = audit; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public PostDetails getDetails() { + return details; + } + + public void setDetails(PostDetails details) { + this.details = details; + details.setPost(this); + } + + public List getTags() { + return tags; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void addDetails(PostDetails details) { + this.details = details; + details.setPost(this); + } + + public void removeDetails() { + this.details.setPost(null); + this.details = null; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + @EntityListeners(AuditListener.class) + public static class PostDetails implements Auditable { + + @Id + private Long id; + + @Embedded + private Audit audit; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + @Lob + private byte[] image; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Audit getAudit() { + return audit; + } + + public void setAudit(Audit audit) { + this.audit = audit; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public byte[] getImage() { + return image; + } + + public void setImage(byte[] image) { + this.image = image; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @EntityListeners(AuditListener.class) + public static class PostComment implements Auditable { + + @Id + @GeneratedValue(generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private Long id; + + @Embedded + private Audit audit; + + @ManyToOne + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Audit getAudit() { + return audit; + } + + public void setAudit(Audit audit) { + this.audit = audit; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + @EntityListeners(AuditListener.class) + public static class Tag implements Auditable { + + @Id + private String name; + + @Embedded + private Audit audit; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Audit getAudit() { + return audit; + } + + public void setAudit(Audit audit) { + this.audit = audit; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/embeddable/EmbeddableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/embeddable/EmbeddableTest.java new file mode 100644 index 000000000..782a46dd5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/embeddable/EmbeddableTest.java @@ -0,0 +1,395 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.embeddable; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.GenericGenerator; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class EmbeddableTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostDetails.class, + PostComment.class, + Tag.class + }; + } + + @Test + public void test() { + LoggedUser.logIn("Alice"); + + doInJPA(entityManager -> { + Tag jdbc = new Tag(); + jdbc.setName("JDBC"); + entityManager.persist(jdbc); + + Tag hibernate = new Tag(); + hibernate.setName("Hibernate"); + entityManager.persist(hibernate); + + Tag jOOQ = new Tag(); + jOOQ.setName("jOOQ"); + entityManager.persist(jOOQ); + }); + + byte[] imageBytes = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9}; + + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence, 1st Edition"); + + PostDetails details = new PostDetails(); + details.setImage(imageBytes); + + post.setDetails(details); + + post.getTags().add(entityManager.find(Tag.class, "JDBC")); + post.getTags().add(entityManager.find(Tag.class, "Hibernate")); + post.getTags().add(entityManager.find(Tag.class, "jOOQ")); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + post.setTitle("High-Performance Java Persistence, 2nd Edition"); + + List postAudits = entityManager.createQuery(""" + select p.audit + from Post p + order by p.id + """, Audit.class) + .getResultList(); + + assertEquals(1, postAudits.size()); + }); + + LoggedUser.logOut(); + } + + public static class LoggedUser { + + private static final ThreadLocal userHolder = new ThreadLocal<>(); + + public static void logIn(String user) { + userHolder.set(user); + } + + public static void logOut() { + userHolder.remove(); + } + + public static String get() { + return userHolder.get(); + } + } + + public static class AuditListener { + + @PrePersist + public void setCreatedOn(Auditable auditable) { + Audit audit = auditable.getAudit(); + + if(audit == null) { + audit = new Audit(); + auditable.setAudit(audit); + } + + audit.setCreatedOn(LocalDateTime.now()); + audit.setCreatedBy(LoggedUser.get()); + } + + @PreUpdate + public void setUpdatedOn(Auditable auditable) { + Audit audit = auditable.getAudit(); + + audit.setUpdatedOn(LocalDateTime.now()); + audit.setUpdatedBy(LoggedUser.get()); + } + } + + public interface Auditable { + + Audit getAudit(); + + void setAudit(Audit audit); + } + + @Embeddable + public static class Audit { + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_on") + private LocalDateTime updatedOn; + + @Column(name = "updated_by") + private String updatedBy; + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + } + } + + @Entity(name = "Post") + @Table(name = "post") + @EntityListeners(AuditListener.class) + public static class Post implements Auditable { + + @Id + private Long id; + + @Embedded + private Audit audit; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true, fetch = FetchType.LAZY) + private PostDetails details; + + @ManyToMany + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Audit getAudit() { + return audit; + } + + public void setAudit(Audit audit) { + this.audit = audit; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public PostDetails getDetails() { + return details; + } + + public void setDetails(PostDetails details) { + this.details = details; + details.setPost(this); + } + + public List getTags() { + return tags; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void addDetails(PostDetails details) { + this.details = details; + details.setPost(this); + } + + public void removeDetails() { + this.details.setPost(null); + this.details = null; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + @EntityListeners(AuditListener.class) + public static class PostDetails implements Auditable { + + @Id + private Long id; + + @Embedded + private Audit audit; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + @Lob + private byte[] image; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Audit getAudit() { + return audit; + } + + public void setAudit(Audit audit) { + this.audit = audit; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public byte[] getImage() { + return image; + } + + public void setImage(byte[] image) { + this.image = image; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @EntityListeners(AuditListener.class) + public static class PostComment implements Auditable { + + @Id + @GeneratedValue(generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private Long id; + + @Embedded + private Audit audit; + + @ManyToOne + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Audit getAudit() { + return audit; + } + + public void setAudit(Audit audit) { + this.audit = audit; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + @EntityListeners(AuditListener.class) + public static class Tag implements Auditable { + + @Id + private String name; + + @Embedded + private Audit audit; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Audit getAudit() { + return audit; + } + + public void setAudit(Audit audit) { + this.audit = audit; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/encrypt/MySQLEncryptTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/encrypt/MySQLEncryptTest.java new file mode 100644 index 000000000..1cac3167d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/encrypt/MySQLEncryptTest.java @@ -0,0 +1,194 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.encrypt; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import org.hibernate.Session; +import org.hibernate.annotations.ColumnTransformer; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLEncryptTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + User.class, + UserDetails.class, + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + setEncryptionKey(entityManager); + + User user = new User() + .setId(1L) + .setUsername("vladmihalcea"); + + entityManager.persist(user); + + entityManager.persist( + new UserDetails() + .setUser(user) + .setFirstName("Vlad") + .setLastName("Mihalcea") + .setEmailAddress("vlad@vladmihalcea.com") + ); + }); + + doInJPA(entityManager -> { + setEncryptionKey(entityManager); + + UserDetails userDetails = entityManager.find( + UserDetails.class, + 1L + ); + + assertEquals("Vlad", userDetails.getFirstName()); + assertEquals("Mihalcea", userDetails.getLastName()); + assertEquals("vlad@vladmihalcea.com", userDetails.getEmailAddress()); + }); + } + + private void setEncryptionKey(EntityManager entityManager) { + Session session = entityManager.unwrap(Session.class); + Dialect dialect = session.getSessionFactory().unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect(); + String encryptionKey = ReflectionUtils.invokeMethod( + dialect, + "inlineLiteral", + "encryptionKey" + ); + + session.doWork(connection -> { + update( + connection, + String.format( + "SET @encryption_key = %s", encryptionKey + ) + ); + }); + } + + @Entity + @Table(name = "users") + public static class User { + + @Id + private Long id; + + private String username; + + public Long getId() { + return id; + } + + public User setId(Long id) { + this.id = id; + return this; + } + + public String getUsername() { + return username; + } + + public User setUsername(String username) { + this.username = username; + return this; + } + } + + @Entity + @Table(name = "user_details") + public static class UserDetails { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private User user; + + @ColumnTransformer( + read = "AES_DECRYPT(first_name, @encryption_key)", + write = "AES_ENCRYPT(?, @encryption_key)" + ) + @Column(name = "first_name", columnDefinition = "VARBINARY(100)") + private String firstName; + + @ColumnTransformer( + read = "AES_DECRYPT(last_name, @encryption_key)", + write = "AES_ENCRYPT(?, @encryption_key)" + ) + @Column(name = "last_name", columnDefinition = "VARBINARY(100)") + private String lastName; + + @ColumnTransformer( + read = "AES_DECRYPT(email_address, @encryption_key)", + write = "AES_ENCRYPT(?, @encryption_key)" + ) + @Column(name = "email_address", columnDefinition = "VARBINARY(100)") + private String emailAddress; + + public Long getId() { + return id; + } + + public UserDetails setId(Long id) { + this.id = id; + return this; + } + + public User getUser() { + return user; + } + + public UserDetails setUser(User user) { + this.user = user; + this.id = user.getId(); + return this; + } + + public String getFirstName() { + return firstName; + } + + public UserDetails setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public UserDetails setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public String getEmailAddress() { + return emailAddress; + } + + public UserDetails setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/encrypt/MySQLJsonEncryptTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/encrypt/MySQLJsonEncryptTest.java new file mode 100644 index 000000000..4ef797588 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/encrypt/MySQLJsonEncryptTest.java @@ -0,0 +1,188 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.encrypt; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.CryptoUtils; +import com.vladmihalcea.hpjp.util.providers.Database; +import io.hypersistence.utils.hibernate.type.json.JsonStringType; +import jakarta.persistence.*; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLJsonEncryptTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + User.class, + UserDetails.class, + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + User user = new User() + .setId(1L) + .setUsername("vladmihalcea") + .setDetails( + new UserDetails() + .setFirstName("Vlad") + .setLastName("Mihalcea") + .setEmailAddress("info@vladmihalcea.com") + ); + + entityManager.persist(user); + }); + + doInJPA(entityManager -> { + User user = entityManager.find( + User.class, + 1L + ); + + UserDetails userDetails = user.getDetails(); + + assertEquals("Vlad", userDetails.getFirstName()); + assertEquals("Mihalcea", userDetails.getLastName()); + assertEquals("info@vladmihalcea.com", userDetails.getEmailAddress()); + }); + + doInJPA(entityManager -> { + User user = entityManager.find(User.class, 1L); + + user.getDetails().setEmailAddress("noreply@vladmihalcea.com"); + }); + } + + @Entity + @Table(name = "users") + @DynamicUpdate + public static class User { + + @Id + private Long id; + + private String username; + + @Type(JsonStringType.class) + @Column(columnDefinition = "json") + private UserDetails details; + + public Long getId() { + return id; + } + + public User setId(Long id) { + this.id = id; + return this; + } + + public String getUsername() { + return username; + } + + public User setUsername(String username) { + this.username = username; + return this; + } + + public UserDetails getDetails() { + return details; + } + + public User setDetails(UserDetails details) { + this.details = details; + return this; + } + + @PrePersist + @PreUpdate + private void encryptFields() { + if (details != null) { + if (details.getFirstName() != null) { + details.setFirstName( + CryptoUtils.encrypt(details.getFirstName()) + ); + } + if (details.getLastName() != null) { + details.setLastName( + CryptoUtils.encrypt(details.getLastName()) + ); + } + if (details.getEmailAddress() != null) { + details.setEmailAddress( + CryptoUtils.encrypt(details.getEmailAddress()) + ); + } + } + } + + @PostLoad + private void decryptFields() { + if (details != null) { + if (details.getFirstName() != null) { + details.setFirstName( + CryptoUtils.decrypt(details.getFirstName()) + ); + } + if (details.getLastName() != null) { + details.setLastName( + CryptoUtils.decrypt(details.getLastName()) + ); + } + if (details.getEmailAddress() != null) { + details.setEmailAddress( + CryptoUtils.decrypt(details.getEmailAddress()) + ); + } + } + } + } + + public static class UserDetails { + + private String firstName; + + private String lastName; + + private String emailAddress; + + public String getFirstName() { + return firstName; + } + + public UserDetails setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public UserDetails setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public String getEmailAddress() { + return emailAddress; + } + + public UserDetails setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/encrypt/PostgreSQLEncryptTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/encrypt/PostgreSQLEncryptTest.java new file mode 100644 index 000000000..83e0807ca --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/encrypt/PostgreSQLEncryptTest.java @@ -0,0 +1,108 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.encrypt; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.annotations.ColumnTransformer; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLEncryptTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Vault.class + }; + } + + @Override + protected void afterInit() { + executeStatement("CREATE EXTENSION IF NOT EXISTS \"pgcrypto\""); + } + + @Override + public void afterDestroy() { + executeStatement("DROP EXTENSION \"pgcrypto\" CASCADE"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Vault user = new Vault(); + user.setId(1L); + user.setStorage("my_secret_key"); + + entityManager.persist(user); + + }); + doInJPA(entityManager -> { + String encryptedStorage = (String) entityManager.createNativeQuery(""" + select encode(storage, 'base64') + from Vault + where id = :id + """) + .setParameter("id", 1L) + .getSingleResult(); + + LOGGER.info("Encoded storage:\n{}", encryptedStorage); + }); + doInJPA(entityManager -> { + Vault vault = entityManager.find(Vault.class, 1L); + assertEquals("my_secret_key", vault.getStorage()); + + vault.setStorage("another_secret_key"); + }); + + doInJPA(entityManager -> { + Vault vault = entityManager.find(Vault.class, 1L); + assertEquals("another_secret_key", vault.getStorage()); + }); + } + + @Entity(name = "Vault") + public static class Vault { + + @Id + private Long id; + + @ColumnTransformer( + read = """ + pgp_sym_decrypt( + storage, + current_setting('encrypt.key') + ) + """, + write = """ + pgp_sym_encrypt( + ?, + current_setting('encrypt.key') + ) + """ + ) + @Column(columnDefinition = "bytea") + private String storage; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getStorage() { + return storage; + } + + public void setStorage(String storage) { + this.storage = storage; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/CustomOrdinalEnumConverterTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/CustomOrdinalEnumConverterTest.java new file mode 100644 index 000000000..804f76bde --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/CustomOrdinalEnumConverterTest.java @@ -0,0 +1,155 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.enums; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import io.hypersistence.utils.hibernate.type.basic.CustomOrdinalEnumConverter; +import jakarta.persistence.*; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class CustomOrdinalEnumConverterTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + executeStatement("ALTER TABLE post DROP COLUMN status"); + executeStatement("ALTER TABLE post ADD COLUMN status NUMERIC(3)"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1) + .setTitle("To be moderated") + .setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION) + ); + entityManager.persist( + new Post() + .setId(2) + .setTitle("Pending") + .setStatus(PostStatus.PENDING) + ); + entityManager.persist( + new Post() + .setId(3) + .setTitle("Approved") + .setStatus(PostStatus.APPROVED) + ); + entityManager.persist( + new Post() + .setId(4) + .setTitle("Spam post") + .setStatus(PostStatus.SPAM) + ); + }); + + doInJPA(entityManager -> { + assertEquals( + PostStatus.REQUIRES_MODERATOR_INTERVENTION, + entityManager.find(Post.class, 1).getStatus() + ); + assertEquals( + PostStatus.PENDING, + entityManager.find(Post.class, 2).getStatus() + ); + assertEquals( + PostStatus.APPROVED, + entityManager.find(Post.class, 3).getStatus() + ); + assertEquals( + PostStatus.SPAM, + entityManager.find(Post.class, 4).getStatus() + ); + }); + } + + public enum PostStatus { + PENDING(100), + APPROVED(10), + SPAM(50), + REQUIRES_MODERATOR_INTERVENTION(1); + + private final int statusCode; + + PostStatus(int statusCode) { + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Integer id; + + @Column(length = 250) + private String title; + + @Column(columnDefinition = "NUMERIC(3)") + @Convert(converter = PostStatusConverter.class) + private PostStatus status; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + } + + @Converter(autoApply = true) + public static class PostStatusConverter extends CustomOrdinalEnumConverter { + + public PostStatusConverter() { + super(PostStatus.class); + } + + @Override + public Integer convertToDatabaseColumn(PostStatus enumValue) { + return enumValue.getStatusCode(); + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumCustomOrdinalTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumCustomOrdinalTest.java new file mode 100644 index 000000000..ab865cd3d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumCustomOrdinalTest.java @@ -0,0 +1,211 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.enums; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.JavaType; +import org.hibernate.type.descriptor.java.EnumJavaType; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class EnumCustomOrdinalTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void afterInit() { + executeStatement("ALTER TABLE post DROP COLUMN status"); + executeStatement("ALTER TABLE post ADD COLUMN status NUMERIC(3)"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1) + .setTitle("To be moderated") + .setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION) + ); + entityManager.persist( + new Post() + .setId(2) + .setTitle("Pending") + .setStatus(PostStatus.PENDING) + ); + entityManager.persist( + new Post() + .setId(3) + .setTitle("Approved") + .setStatus(PostStatus.APPROVED) + ); + entityManager.persist( + new Post() + .setId(4) + .setTitle("Spam post") + .setStatus(PostStatus.SPAM) + ); + }); + + doInJPA(entityManager -> { + assertEquals( + PostStatus.REQUIRES_MODERATOR_INTERVENTION, + entityManager.find(Post.class, 1).getStatus() + ); + assertEquals( + PostStatus.PENDING, + entityManager.find(Post.class, 2).getStatus() + ); + assertEquals( + PostStatus.APPROVED, + entityManager.find(Post.class, 3).getStatus() + ); + assertEquals( + PostStatus.SPAM, + entityManager.find(Post.class, 4).getStatus() + ); + }); + } + + public enum PostStatus { + PENDING(100), + APPROVED(10), + SPAM(50), + REQUIRES_MODERATOR_INTERVENTION(1); + + private final int statusCode; + + PostStatus(int statusCode) { + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Integer id; + + @Column(length = 250) + private String title; + + @Column(columnDefinition = "NUMERIC(3)") + @JavaType(PostStatusJavaType.class) + private PostStatus status; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + } + + public static abstract class CustomOrdinalValueEnumJavaType> extends EnumJavaType { + + private Map enumToCustomOrdinalValueMap = new HashMap<>(); + private Map customOrdinalValueToEnumMap = new HashMap<>(); + + public CustomOrdinalValueEnumJavaType(Class type) { + super(type); + T[] enumValues = ReflectionUtils.invokeStaticMethod( + ReflectionUtils.getMethod(type, "values") + ); + for(T enumValue : enumValues) { + Integer customOrdinalValue = getCustomOrdinalValue(enumValue); + enumToCustomOrdinalValueMap.put(enumValue, customOrdinalValue); + customOrdinalValueToEnumMap.put(customOrdinalValue, enumValue); + } + } + + protected abstract Integer getCustomOrdinalValue(T enumValue); + + public Byte toByte(T domainForm) { + return domainForm != null ? + enumToCustomOrdinalValueMap.get(domainForm).byteValue() : null + ; + } + + public Short toShort(T domainForm) { + return domainForm != null ? + enumToCustomOrdinalValueMap.get(domainForm).shortValue() : null + ; + } + + public Integer toInteger(T domainForm) { + return domainForm != null ? + enumToCustomOrdinalValueMap.get(domainForm) : null + ; + } + + public Long toLong(T domainForm) { + return domainForm != null ? + enumToCustomOrdinalValueMap.get(domainForm).longValue() : null + ; + } + + public T fromByte(Byte byteValue) { + return byteValue != null ? + customOrdinalValueToEnumMap.get(byteValue.intValue()) : null + ; + } + } + + public static class PostStatusJavaType extends CustomOrdinalValueEnumJavaType { + public PostStatusJavaType() { + super(PostStatus.class); + } + + @Override + protected Integer getCustomOrdinalValue(PostStatus postStatus) { + return postStatus.getStatusCode(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalDescriptionMySQLTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalDescriptionMySQLTest.java new file mode 100644 index 000000000..accb38974 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalDescriptionMySQLTest.java @@ -0,0 +1,160 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.enums; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class EnumOrdinalDescriptionMySQLTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + public void beforeInit() { + executeStatement("drop table if exists post"); + executeStatement("drop table if exists post_status_info"); + executeStatement("create table post (id integer not null auto_increment, title varchar(100), status tinyint, primary key (id))"); + executeStatement("create table post_status_info (id tinyint not null, name varchar(50), description varchar(255), primary key (id))"); + executeStatement("alter table post add constraint status_id foreign key (status) references post_status_info (id)"); + executeStatement("insert into post_status_info (id, name, description) values (0, 'PENDING', 'Post waiting to be approved')"); + executeStatement("insert into post_status_info (id, name, description) values (1, 'APPROVED', 'Post approved')"); + executeStatement("insert into post_status_info (id, name, description) values (2, 'SPAM', 'Post rejected as spam')"); + executeStatement("insert into post_status_info (id, name, description) values (3, 'REQUIRES_MODERATOR_INTERVENTION', 'Post requires moderator intervention')"); + } + + @Test + public void testPendingPost() { + Post _post = doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + post.setStatus(PostStatus.PENDING); + entityManager.persist(post); + + return post; + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, _post.getId()); + + assertEquals(PostStatus.PENDING, post.getStatus()); + assertEquals("PENDING", post.getStatus().name()); + + Tuple tuple = (Tuple) entityManager.createNativeQuery(""" + SELECT + p.id, p.title, p.status, + psi.name, psi.description + FROM post p + INNER JOIN post_status_info psi ON p.status = psi.id + WHERE p.id = :postId + """, Tuple.class) + .setParameter("postId", _post.getId()) + .getSingleResult(); + + assertEquals("PENDING", tuple.get("name")); + assertEquals("Posts waiting to be approved by the admin", tuple.get("description")); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("Check out my website") + .setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION) + ); + }); + + doInJPA(entityManager -> { + int postId = 50; + + try { + entityManager.createNativeQuery(""" + INSERT INTO post (status, title, id) + VALUES (:status, :title, :id) + """) + .setParameter("status", 99) + .setParameter("title", "Illegal Enum value") + .setParameter("id", postId) + .executeUpdate(); + + fail("Should not allow us to insert an Enum value of 100!"); + } catch (ConstraintViolationException e) { + assertTrue(e.getMessage().contains("a foreign key constraint fails")); + } + }); + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM, + REQUIRES_MODERATOR_INTERVENTION + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + private String title; + + @Enumerated(EnumType.ORDINAL) + @Column(columnDefinition = "tinyint") + private PostStatus status; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalDescriptionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalDescriptionTest.java new file mode 100644 index 000000000..b0aa53198 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalDescriptionTest.java @@ -0,0 +1,230 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.enums; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class EnumOrdinalDescriptionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostStatusInfo.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + public void beforeInit() { + executeStatement("DROP TYPE IF EXISTS post_status_info CASCADE"); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + PostStatusInfo pending = new PostStatusInfo(); + pending.setId(PostStatus.PENDING.ordinal()); + pending.setName(PostStatus.PENDING.name()); + pending.setDescription("Post waiting to be approved by the admin"); + entityManager.persist(pending); + + PostStatusInfo approved = new PostStatusInfo(); + approved.setId(PostStatus.APPROVED.ordinal()); + approved.setName(PostStatus.APPROVED.name()); + approved.setDescription("Post approved by the admin"); + entityManager.persist(approved); + + PostStatusInfo spam = new PostStatusInfo(); + spam.setId(PostStatus.SPAM.ordinal()); + spam.setName(PostStatus.SPAM.name()); + spam.setDescription("Post rejected as spam"); + entityManager.persist(spam); + + PostStatusInfo moderated = new PostStatusInfo(); + moderated.setId(PostStatus.REQUIRES_MODERATOR_INTERVENTION.ordinal()); + moderated.setName(PostStatus.REQUIRES_MODERATOR_INTERVENTION.name()); + moderated.setDescription("Post requires moderator intervention"); + entityManager.persist(moderated); + }); + } + + @Test + public void testPendingPost() { + Post _post = doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + post.setStatus(PostStatus.PENDING); + entityManager.persist(post); + + return post; + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, _post.getId()); + + assertEquals(PostStatus.PENDING, post.getStatus()); + assertEquals("PENDING", post.getStatusInfo().getName()); + + Tuple tuple = (Tuple) entityManager.createNativeQuery(""" + SELECT + p.id, + p.title, + p.status, + psi.name, + psi.description + FROM post p + INNER JOIN post_status_info psi ON p.status = psi.id + WHERE p.id = :postId + """, Tuple.class) + .setParameter("postId", _post.getId()) + .getSingleResult(); + + assertEquals("PENDING", tuple.get("name")); + assertEquals("Post waiting to be approved by the admin", tuple.get("description")); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("Check out my website") + .setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION) + ); + }); + + doInJPA(entityManager -> { + int postId = 50; + + try { + entityManager.createNativeQuery(""" + INSERT INTO post (status, title, id) + VALUES (:status, :title, :id) + """) + .setParameter("status", 99) + .setParameter("title", "Illegal Enum value") + .setParameter("id", postId) + .executeUpdate(); + + fail("Should not allow us to insert an Enum value of 100!"); + } catch (ConstraintViolationException e) { + assertEquals("post_status_check", e.getConstraintName()); + } + }); + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM, + REQUIRES_MODERATOR_INTERVENTION + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Integer id; + + private String title; + + @Enumerated(EnumType.ORDINAL) + @Column(columnDefinition = "NUMERIC(2)") + private PostStatus status; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn( + name = "status", + insertable = false, + updatable = false, + foreignKey = @ForeignKey( + name = "status_id" + ) + ) + private PostStatusInfo statusInfo; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + + public PostStatusInfo getStatusInfo() { + return statusInfo; + } + } + + @Entity(name = "PostStatusInfo") + @Table(name = "post_status_info") + public static class PostStatusInfo { + + @Id + @Column(columnDefinition = "NUMERIC(2)") + private Integer id; + + @Column(length = 50) + private String name; + + private String description; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalMySQLWithCheckTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalMySQLWithCheckTest.java new file mode 100644 index 000000000..394da2e5d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalMySQLWithCheckTest.java @@ -0,0 +1,213 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.enums; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.JDBCException; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.util.Properties; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class EnumOrdinalMySQLWithCheckTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected void beforeInit() { + executeStatement("DROP TABLE IF EXISTS post"); + executeStatement("CREATE TABLE post (id integer not null auto_increment, title varchar(100), status tinyint unsigned, PRIMARY KEY (id)) engine=InnoDB"); + } + + @Test + public void testEnumName() { + String enumName = PostStatus.APPROVED.name(); + PostStatus enumValue = PostStatus.valueOf(enumName); + + assertSame(PostStatus.APPROVED, enumValue); + } + + @Test + public void testEnumOrdinal() { + int enumOrdinal = PostStatus.APPROVED.ordinal(); + PostStatus enumValue = PostStatus.values()[enumOrdinal]; + + assertSame(PostStatus.APPROVED, enumValue); + } + + @Test + public void test() { + Integer postId = doInJPA(entityManager -> { + Post post = new Post() + .setTitle("Tuning Spring applications with Hypersistence Optimizer") + .setStatus(PostStatus.PENDING); + + entityManager.persist(post); + + return post.getId(); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, postId); + post.setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION); + }); + } + + @Test + public void testCheckConstraint() { + executeStatement(""" + ALTER TABLE post + ADD CONSTRAINT CHK_status_enum_value + CHECK (status between 0 and 3) + """); + + try { + doInJPA(entityManager -> { + entityManager.createNativeQuery(""" + INSERT INTO post (title, status) + VALUES (:title, :status) + """) + .setParameter("title", "Illegal Enum value") + .setParameter("status", 99) + .executeUpdate(); + + fail("Should not store the ordinal value of 99!"); + }); + } catch (JDBCException e) { + assertTrue(e.getMessage().contains("Check constraint 'post_status_enum' is violated")); + } + } + + @Test + public void testAlterColumnPerformance() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + executeStatement(""" + ALTER TABLE post + ADD CONSTRAINT CHK_status_enum_value + CHECK (status between 0 and 3) + """); + + int postCount = 1_000_000; + int batchSize = 1_000; + ThreadLocalRandom random = ThreadLocalRandom.current(); + PostStatus[] postStatuses = PostStatus.values(); + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(PreparedStatement preparedStatement = connection.prepareStatement(""" + INSERT INTO post (title, status) + VALUES (?, ?) + """)) { + + boolean flushed = false; + for (int i = 1; i < postCount; i++) { + preparedStatement.setString(1, String.format("Post nr %d", i)); + preparedStatement.setInt(2, random.nextInt(postStatuses.length)); + + flushed = false; + preparedStatement.addBatch(); + if(i % batchSize == 0) { + preparedStatement.executeBatch(); + flushed = true; + } + } + if (!flushed) { + preparedStatement.executeBatch(); + } + } + }); + }); + long startNanos = System.nanoTime(); + executeStatement(""" + ALTER TABLE post + DROP CONSTRAINT CHK_status_enum_value; + """, + """ + ALTER TABLE post + ADD CONSTRAINT CHK_status_enum_value + CHECK (status between 0 and 4) + """ + ); + LOGGER.info("Add CHECK constraint took [{}] ms", TimeUnit.NANOSECONDS.toMillis( + System.nanoTime() - startNanos + )); + //Add CHECK constraint took [6221] ms + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM, + REQUIRES_MODERATOR_INTERVENTION + } + + @Entity(name = "Post") + @Table(name = "post") + @DynamicUpdate + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(length = 100) + private String title; + + @Enumerated + @Column(columnDefinition = "tinyint unsigned") + private PostStatus status; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalMySQLWithoutCheckTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalMySQLWithoutCheckTest.java new file mode 100644 index 000000000..0ba22ef30 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalMySQLWithoutCheckTest.java @@ -0,0 +1,126 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.enums; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class EnumOrdinalMySQLWithoutCheckTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected void beforeInit() { + executeStatement("drop table if exists post"); + executeStatement("create table post (id integer not null auto_increment, title varchar(100), status tinyint unsigned, primary key (id)) engine=InnoDB"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("Tuning Spring applications with Hypersistence Optimizer") + .setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION) + ); + }); + + try { + doInJPA(entityManager -> { + int postId = 50; + + int rowCount = entityManager.createNativeQuery(""" + INSERT INTO post (id, title, status) + VALUES (:id, :title, :status) + """) + .setParameter("id", postId) + .setParameter("title", "Illegal Enum value") + .setParameter("status", 99) + .executeUpdate(); + + assertEquals(1, rowCount); + + Post post = entityManager.find(Post.class, postId); + + fail("Should not map the Enum value of 99!"); + }); + } catch (ArrayIndexOutOfBoundsException e) { + LOGGER.info("Expected", e); + assertEquals("Index 99 out of bounds for length 4", e.getMessage()); + } + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM, + REQUIRES_MODERATOR_INTERVENTION + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(length = 100) + private String title; + + @Enumerated + @Column(columnDefinition = "tinyint unsigned") + private PostStatus status; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalOracleTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalOracleTest.java new file mode 100644 index 000000000..be8d8a30c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalOracleTest.java @@ -0,0 +1,111 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.enums; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class EnumOrdinalOracleTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.ORACLE; + } + + @Override + protected void afterInit() { + executeStatement("ALTER TABLE post DROP COLUMN status"); + executeStatement("ALTER TABLE post ADD status NUMBER(3)"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("Check out my website") + .setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION) + ); + }); + + doInJPA(entityManager -> { + int postId = 50; + + for (int i = 0; i < 200; i++) { + int rowCount = entityManager.createNativeQuery(""" + INSERT INTO post (status, title, id) + VALUES (:status, :title, :id) + """) + .setParameter("status", i) + .setParameter("title", "Illegal Enum value") + .setParameter("id", postId++) + .executeUpdate(); + + assertEquals(1, rowCount); + } + }); + + LOGGER.info(""); + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM, + REQUIRES_MODERATOR_INTERVENTION + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Integer id; + + private String title; + + @Enumerated(EnumType.ORDINAL) + private PostStatus status; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalTest.java new file mode 100644 index 000000000..18e3f9780 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumOrdinalTest.java @@ -0,0 +1,109 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.enums; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class EnumOrdinalTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("Check out my website") + .setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION) + ); + }); + + try { + doInJPA(entityManager -> { + int postId = 50; + + int rowCount = entityManager.createNativeQuery(""" + INSERT INTO post (status, title, id) + VALUES (:status, :title, :id) + """) + .setParameter("status", 99) + .setParameter("title", "Illegal Enum value") + .setParameter("id", postId) + .executeUpdate(); + + assertEquals(1, rowCount); + + Post post = entityManager.find(Post.class, postId); + + fail("Should not map the Enum value of 100!"); + }); + } catch (ConstraintViolationException e) { + assertTrue(e.getMessage().contains("violates check constraint \"post_status_check\"")); + } + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM, + REQUIRES_MODERATOR_INTERVENTION + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Integer id; + + private String title; + + @Enumerated(EnumType.ORDINAL) + private PostStatus status; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumPostgreSQLTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumPostgreSQLTest.java new file mode 100644 index 000000000..77b474304 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumPostgreSQLTest.java @@ -0,0 +1,130 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.enums; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.JdbcType; +import org.hibernate.annotations.Type; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class EnumPostgreSQLTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + + @Override + protected void beforeInit() { + executeStatement("drop table if exists post cascade"); + executeStatement("drop type if exists PostStatus cascade"); + executeStatement("create type poststatus as enum ('PENDING','APPROVED','REQUIRES_MODERATOR_INTERVENTION','SPAM')"); + //executeStatement("create cast (varchar as PostStatus) with inout as implicit"); + //executeStatement("create cast (PostStatus as varchar) with inout as implicit"); + executeStatement("create table post (id integer not null, title varchar(100), status PostStatus, primary key (id))"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1) + .setTitle("High-Performance Java Persistence") + .setStatus(PostStatus.PENDING) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertEquals(PostStatus.PENDING, post.getStatus()); + }); + } + + @Test + public void testUpdate() { + Integer postId = doInJPA(entityManager -> { + Post post = new Post() + .setId(1) + .setTitle("Tuning Spring applications with Hypersistence Optimizer") + .setStatus(PostStatus.PENDING); + + entityManager.persist(post); + + return post.getId(); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, postId); + + post.setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION); + }); + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM, + REQUIRES_MODERATOR_INTERVENTION + } + + @Entity(name = "Post") + @Table(name = "post") + @DynamicUpdate + public static class Post { + + @Id + private Integer id; + + private String title; + + @Enumerated + @JdbcType(PostgreSQLEnumJdbcType.class) + private PostStatus status; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumStringPostgreSQLWithCheckTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumStringPostgreSQLWithCheckTest.java new file mode 100644 index 000000000..0a5d991b4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumStringPostgreSQLWithCheckTest.java @@ -0,0 +1,183 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.enums; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.JDBCException; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.util.Properties; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class EnumStringPostgreSQLWithCheckTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void beforeInit() { + executeStatement("drop table if exists post cascade"); + executeStatement("drop sequence if exists post_SEQ"); + executeStatement("create sequence post_SEQ start with 1 increment by 50"); + executeStatement("create table post (id integer not null, title varchar(100), status varchar(31), primary key (id))"); + } + + @Test + public void testCheckConstraint() { + executeStatement(""" + ALTER TABLE post + ADD CONSTRAINT CHK_status_enum_value + CHECK (status in ('PENDING','APPROVED','SPAM','REQUIRES_MODERATOR_INTERVENTION')) + """); + + try { + doInJPA(entityManager -> { + entityManager.createNativeQuery(""" + INSERT INTO post (id, title, status) + VALUES (:id, :title, :status) + """) + .setParameter("id", 100) + .setParameter("title", "Illegal Enum value") + .setParameter("status", "UNSUPPORTED") + .executeUpdate(); + + fail("Should not store the string value of UNSUPPORTED!"); + }); + } catch (ConstraintViolationException e) { + assertTrue(e.getMessage().contains("new row for relation \"post\" violates check constraint \"chk_status_enum_value\"")); + } + } + + @Test + public void testAlterColumnPerformance() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + executeStatement(""" + ALTER TABLE post + ADD CONSTRAINT CHK_status_enum_value + CHECK (status in ('PENDING','APPROVED','SPAM','REQUIRES_MODERATOR_INTERVENTION')) + """); + + int postCount = 1_000_000; + int batchSize = 1_000; + ThreadLocalRandom random = ThreadLocalRandom.current(); + PostStatus[] postStatuses = PostStatus.values(); + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(PreparedStatement preparedStatement = connection.prepareStatement(""" + INSERT INTO post (id, title, status) + VALUES (?, ?, ?) + """)) { + + boolean flushed = false; + for (int i = 1; i < postCount; i++) { + preparedStatement.setInt(1, i); + preparedStatement.setString(2, String.format("Post nr %d", i)); + preparedStatement.setString(3, postStatuses[random.nextInt(postStatuses.length)].name()); + + flushed = false; + preparedStatement.addBatch(); + if(i % batchSize == 0) { + preparedStatement.executeBatch(); + flushed = true; + } + } + if (!flushed) { + preparedStatement.executeBatch(); + } + } + }); + }); + long startNanos = System.nanoTime(); + executeStatement(""" + ALTER TABLE post + DROP CONSTRAINT CHK_status_enum_value; + """, + """ + ALTER TABLE post + ADD CONSTRAINT CHK_status_enum_value + CHECK (status in ('PENDING','APPROVED','SPAM','REQUIRES_MODERATOR_INTERVENTION', 'PROMOTED')) + """ + ); + LOGGER.info("Add CHECK constraint took [{}] ms", TimeUnit.NANOSECONDS.toMillis( + System.nanoTime() - startNanos + )); + //Add CHECK constraint took [6221] ms + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM, + REQUIRES_MODERATOR_INTERVENTION + } + + @Entity(name = "Post") + @Table(name = "post") + @DynamicUpdate + public static class Post { + + @Id + @GeneratedValue + private Integer id; + + private String title; + + @Enumerated(EnumType.STRING) + @Column(length = 31) + private PostStatus status; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumStringPostgreSQLWithoutCheckTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumStringPostgreSQLWithoutCheckTest.java new file mode 100644 index 000000000..77613c24b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumStringPostgreSQLWithoutCheckTest.java @@ -0,0 +1,153 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.enums; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class EnumStringPostgreSQLWithoutCheckTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void beforeInit() { + executeStatement("drop table if exists post cascade"); + executeStatement("drop sequence if exists post_SEQ"); + executeStatement("create sequence post_SEQ start with 1 increment by 50"); + executeStatement("create table post (id integer not null, title varchar(100), status varchar(31), primary key (id))"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("Check out my website") + .setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION) + ); + }); + + try { + doInJPA(entityManager -> { + int postId = 50; + + int rowCount = entityManager.createNativeQuery(""" + INSERT INTO post (id, title, status) + VALUES (:id, :title, :status) + """) + .setParameter("id", postId) + .setParameter("title", "Illegal Enum value") + .setParameter("status", "UNSUPPORTED") + .executeUpdate(); + + assertEquals(1, rowCount); + + Post post = entityManager.find(Post.class, postId); + + fail("Should not map the Enum value of UNSUPPORTED!"); + }); + } catch (Exception e) { + LOGGER.info("Expected", e); + assertEquals( + String.format( + "No enum constant %s.UNSUPPORTED", + PostStatus.class.getName() + ).replaceAll("\\$", "."), + e.getMessage() + ); + } + } + + @Test + public void testUpdate() { + Integer postId = doInJPA(entityManager -> { + Post post = new Post() + .setTitle("Tuning Spring applications with Hypersistence Optimizer") + .setStatus(PostStatus.PENDING); + + entityManager.persist(post); + + return post.getId(); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, postId); + post.setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION); + }); + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM, + REQUIRES_MODERATOR_INTERVENTION + } + + @Entity(name = "Post") + @Table(name = "post") + @DynamicUpdate + public static class Post { + + @Id + @GeneratedValue + private Integer id; + + private String title; + + @Enumerated(EnumType.STRING) + @Column(length = 31) + private PostStatus status; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumStringTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumStringTest.java new file mode 100644 index 000000000..dab4b28fc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/enums/EnumStringTest.java @@ -0,0 +1,84 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.enums; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.junit.Test; + +/** + * @author Vlad Mihalcea + */ +public class EnumStringTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("Check out my website") + .setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION) + ); + }); + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM, + REQUIRES_MODERATOR_INTERVENTION + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Integer id; + + private String title; + + @Enumerated(EnumType.STRING) + @Column(length = 31) + private PostStatus status; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/generated/LoggedUserTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/generated/LoggedUserTest.java new file mode 100644 index 000000000..e858fbfc7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/generated/LoggedUserTest.java @@ -0,0 +1,188 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.generated; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.GenerationTime; +import org.hibernate.annotations.GeneratorType; +import org.hibernate.tuple.ValueGenerator; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import jakarta.persistence.Table; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * @author Vlad Mihalcea + */ +public class LoggedUserTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Sensor.class + }; + } + + @Override + protected void afterInit() { + LoggedUser.logIn("Alice"); + + doInJPA(entityManager -> { + Sensor ip = new Sensor(); + ip.setName("ip"); + ip.setValue("192.168.0.101"); + + entityManager.persist(ip); + + executeSync(() -> { + LoggedUser.logIn("Bob"); + + doInJPA(_entityManager -> { + Sensor temperature = new Sensor(); + temperature.setName("temperature"); + temperature.setValue("32"); + + _entityManager.persist(temperature); + }); + + LoggedUser.logOut(); + }); + }); + + LoggedUser.logOut(); + } + + @Test + public void test() { + LoggedUser.logIn("Alice"); + + doInJPA(entityManager -> { + Sensor temperature = entityManager.find(Sensor.class, "temperature"); + + temperature.setValue("36"); + + executeSync(() -> { + LoggedUser.logIn("Bob"); + + doInJPA(_entityManager -> { + Sensor ip = _entityManager.find(Sensor.class, "ip"); + + ip.setValue("192.168.0.102"); + }); + + LoggedUser.logOut(); + }); + }); + + LoggedUser.logOut(); + } + + @Entity(name = "Sensor") + @Table(name = "sensor") + public static class Sensor { + + @Id + @Column(name = "sensor_name") + private String name; + + @Column(name = "sensor_value") + private String value; + + @Column(name = "created_by") + @GeneratorType( + type = LoggedUserGenerator.class, + when = GenerationTime.INSERT + ) + private String createdBy; + + @Column(name = "updated_by") + @GeneratorType( + type = LoggedUserGenerator.class, + when = GenerationTime.ALWAYS + ) + private String updatedBy; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getCreatedBy() { + return createdBy; + } + + public String getUpdatedBy() { + return updatedBy; + } + } + + public static class LoggedUser { + + private static final ThreadLocal userHolder = new ThreadLocal<>(); + + public static void logIn(String user) { + userHolder.set(user); + } + + public static void logOut() { + userHolder.remove(); + } + + public static String get() { + return userHolder.get(); + } + } + + public static class LoggedUserGenerator + implements ValueGenerator { + + @Override + public String generateValue( + Session session, Object owner) { + return LoggedUser.get(); + } + } + + public static class LoggedUserFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain filterChain) + throws IOException, ServletException { + + try { + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + LoggedUser.logIn(httpServletRequest.getRemoteUser()); + + filterChain.doFilter(request, response); + } + finally { + LoggedUser.logOut(); + } + } + + @Override + public void destroy() { + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/generated/SequenceDefaultColumnValueTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/generated/SequenceDefaultColumnValueTest.java new file mode 100644 index 000000000..9a878b0e6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/generated/SequenceDefaultColumnValueTest.java @@ -0,0 +1,91 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.generated; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SequenceDefaultColumnValueTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + public void init() { + executeStatement("DROP SEQUENCE sensor_seq"); + executeStatement(""" + CREATE SEQUENCE + sensor_seq + START 100 + """ + ); + super.init(); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.createQuery("select p from Post p", Post.class).getSingleResult(); + + assertEquals(Long.valueOf(100), post.getSequenceId()); + }); + } + + @Entity(name = "Post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @Column( + columnDefinition = "int8 DEFAULT nextval('sensor_seq')", + insertable = false + ) + private Long sequenceId; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Long getSequenceId() { + return sequenceId; + } + + public void setSequenceId(Long sequenceId) { + this.sequenceId = sequenceId; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/softdelete/SoftDeleteAnnotationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/softdelete/SoftDeleteAnnotationTest.java new file mode 100644 index 000000000..1da10e845 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/softdelete/SoftDeleteAnnotationTest.java @@ -0,0 +1,510 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.softdelete; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.LazyInitializationException; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; +import org.hibernate.annotations.SoftDelete; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class SoftDeleteAnnotationTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + PostComment.class, + Tag.class + }; + } + + @Override + public void afterInit() { + doInJPA( entityManager -> { + entityManager.persist( + new Tag().setName("Java") + ); + + entityManager.persist( + new Tag().setName("JPA") + ); + + entityManager.persist( + new Tag().setName("Hibernate") + ); + + entityManager.persist( + new Tag().setName("Misc") + ); + } ); + } + + @Test + public void testRemoveTag() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Java")); + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Hibernate")); + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Misc")); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(3, post.getTags().size()); + }); + + Tag _miscTag = doInJPA(entityManager -> { + Tag miscTag = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Tag.class) + .getReference("Misc"); + + entityManager.remove(miscTag); + + return miscTag; + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getTags().size()); + }); + + doInJPA(entityManager -> { + //That would not work without @Loader(namedQuery = "findTagById") + assertNull(entityManager.find(Tag.class, _miscTag.getId())); + }); + + doInJPA(entityManager -> { + Boolean exists = entityManager.createQuery(""" + select count(t) = 1 + from Tag t + where t.name = :name + """, Boolean.class) + .setParameter("name", "Misc") + .getSingleResult(); + + assertFalse(exists); + }); + + doInJPA(entityManager -> { + List tags = entityManager.createQuery("select t from Tag t", Tag.class).getResultList(); + //That would not work without @Where(clause = "deleted = false") + assertEquals(3, tags.size()); + }); + } + + @Test + public void testRemovePostDetails() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + post.addDetails( + new PostDetails() + .setCreatedOn( + Timestamp.valueOf(LocalDateTime.of(2023, 7, 20, 12, 0, 0)) + ) + ); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNotNull(post.getDetails()); + + post.removeDetails(); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNull(post.getDetails()); + }); + + doInJPA(entityManager -> { + assertNull(entityManager.find(PostDetails.class, 1L)); + }); + } + + @Test + public void testRemovePostComment() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("Great!") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("To read") + ) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + + assertNotNull(entityManager.find(PostComment.class, 2L)); + + post.removeComment(post.getComments().get(1)); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, post.getComments().size()); + assertNull(entityManager.find(PostComment.class, 2L)); + }); + } + + @Test + public void testRemovePost() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addDetails( + new PostDetails() + .setCreatedOn( + Timestamp.valueOf( + LocalDateTime.of(2023, 7, 20, 12, 0, 0) + ) + ) + ) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Java")) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Misc")) + .addComment( + new PostComment() + .setId(1L) + .setReview("Great!") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("To read") + ) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + join fetch p.details + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + entityManager.remove(post); + }); + } + + @Test + public void testRemoveAndFindPostComment() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + post.addComment( + new PostComment() + .setId(1L) + .setReview("Great!") + ); + + post.addComment( + new PostComment() + .setId(2L) + .setReview("Excellent!") + ); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + post.removeComment(post.getComments().get(0)); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, post.getComments().size()); + }); + } + + public void testEagerLoading() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + post.addComment( + new PostComment() + .setId(1L) + .setReview("Great!") + ); + }); + + PostComment comment = doInJPA(entityManager -> { + return entityManager.find(PostComment.class, 1L); + }); + + LOGGER.info("Post [{}] was loaded eagrly: ", comment.getPost().getTitle()); + } + + @Entity(name = "Post") + @Table(name = "post") + @SoftDelete + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY + ) + private PostDetails details; + + @ManyToMany + @JoinTable( + name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + @SoftDelete + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public PostDetails getDetails() { + return details; + } + + public List getTags() { + return tags; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } + + public Post addDetails(PostDetails details) { + this.details = details; + details.setPost(this); + return this; + } + + public Post removeDetails() { + this.details.setPost(null); + this.details = null; + return this; + } + + public Post addTag(Tag tag) { + tags.add(tag); + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + @SoftDelete + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + public PostDetails() { + createdOn = new Date(); + } + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "id") + @MapsId + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @SoftDelete + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @NotFound(action = NotFoundAction.EXCEPTION) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + @SoftDelete + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/softdelete/SoftDeleteTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/softdelete/SoftDeleteTest.java new file mode 100644 index 000000000..381ce5276 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/softdelete/SoftDeleteTest.java @@ -0,0 +1,506 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.softdelete; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.Loader; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class SoftDeleteTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + PostComment.class, + Tag.class + }; + } + + @Override + public void afterInit() { + doInJPA( entityManager -> { + entityManager.persist( + new Tag().setName("Java") + ); + + entityManager.persist( + new Tag().setName("JPA") + ); + + entityManager.persist( + new Tag().setName("Hibernate") + ); + + entityManager.persist( + new Tag().setName("Misc") + ); + } ); + } + + @Test + public void testRemoveTag() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Java")); + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Hibernate")); + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Misc")); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(3, post.getTags().size()); + }); + + Tag miscTag = doInJPA(entityManager -> { + return entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Misc"); + }); + + doInJPA(entityManager -> { + entityManager.remove(miscTag); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getTags().size()); + }); + + doInJPA(entityManager -> { + //That would not work without @Loader(namedQuery = "findTagById") + assertNull(entityManager.find(Tag.class, miscTag.getId())); + }); + + doInJPA(entityManager -> { + List tags = entityManager.createQuery("select t from Tag t", Tag.class).getResultList(); + //That would not work without @Where(clause = "deleted = false") + assertEquals(3, tags.size()); + }); + } + + @Test + public void testRemovePostDetails() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + PostDetails postDetails = new PostDetails() + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 7, 20, 12, 0, 0))); + post.addDetails(postDetails); + + entityManager.persist(post); + + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Java")); + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Hibernate")); + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Misc")); + + post.addComment( + new PostComment() + .setId(1L) + .setReview("Great!") + ); + + post.addComment( + new PostComment() + .setId(2L) + .setReview("To read") + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNotNull(post.getDetails()); + post.removeDetails(); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNull(post.getDetails()); + }); + + doInJPA(entityManager -> { + assertNull(entityManager.find(PostDetails.class, 1L)); + }); + } + + @Test + public void testRemovePostComment() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + PostDetails postDetails = new PostDetails() + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 7, 20, 12, 0, 0))); + post.addDetails(postDetails); + + entityManager.persist(post); + + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Java")); + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Hibernate")); + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Misc")); + + post.addComment( + new PostComment() + .setId(1L) + .setReview("Great!") + ); + + post.addComment( + new PostComment() + .setId(2L) + .setReview("To read") + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + assertNotNull(entityManager.find(PostComment.class, 2L)); + post.removeComment(post.getComments().get(1)); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, post.getComments().size()); + assertNull(entityManager.find(PostComment.class, 2L)); + }); + } + + @Test + public void testRemoveAndFindPostComment() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + post.addComment( + new PostComment() + .setId(1L) + .setReview("Great!") + ); + + post.addComment( + new PostComment() + .setId(2L) + .setReview("Excellent!") + ); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + post.removeComment(post.getComments().get(0)); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, post.getComments().size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @SQLDelete(sql = """ + UPDATE post + SET deleted = true + WHERE id = ?1 + """) + @Loader(namedQuery = "findPostById") + @NamedQuery(name = "findPostById", query = """ + select p + from Post p + where + p.id = ?1 and + p.deleted = false + """) + @Where(clause = "deleted = false") + public static class Post extends SoftDeletable { + + @Id + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY + ) + private PostDetails details; + + @ManyToMany + @JoinTable( + name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public PostDetails getDetails() { + return details; + } + + public List getTags() { + return tags; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } + + public Post addDetails(PostDetails details) { + this.details = details; + details.setPost(this); + return this; + } + + public Post removeDetails() { + this.details.setPost(null); + this.details = null; + return this; + } + + public Post addTag(Tag tag) { + tags.add(tag); + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + @SQLDelete(sql = """ + UPDATE post_details + SET deleted = true + WHERE id = ? + """) + @Loader(namedQuery = "findPostDetailsById") + @NamedQuery(name = "findPostDetailsById", query = """ + select pd + from PostDetails pd + where + pd.id = ?1 and + pd.deleted = false + """) + @Where(clause = "deleted = false") + public static class PostDetails extends SoftDeletable { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + public PostDetails() { + createdOn = new Date(); + } + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "id") + @MapsId + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @SQLDelete(sql = """ + UPDATE post_comment + SET deleted = true + WHERE id = ? + """) + @Loader(namedQuery = "findPostCommentById") + @NamedQuery(name = "findPostCommentById", query = """ + select pc + from PostComment pc + where + pc.id = ?1 and + pc.deleted = false + """) + @Where(clause = "deleted = false") + public static class PostComment extends SoftDeletable { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + @SQLDelete(sql = """ + UPDATE tag + SET deleted = true + WHERE id = ? + """) + @Loader(namedQuery = "findTagById") + @NamedQuery(name = "findTagById", query = """ + select t + from Tag t + where + t.id = ?1 and + t.deleted = false + """) + @Where(clause = "deleted = false") + public static class Tag extends SoftDeletable { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } + + @MappedSuperclass + public static abstract class SoftDeletable { + + private boolean deleted; + + public boolean isDeleted() { + return deleted; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/softdelete/SoftDeleteVersionAnnotationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/softdelete/SoftDeleteVersionAnnotationTest.java new file mode 100644 index 000000000..342b608e3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/softdelete/SoftDeleteVersionAnnotationTest.java @@ -0,0 +1,501 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.softdelete; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; +import org.hibernate.annotations.SoftDelete; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class SoftDeleteVersionAnnotationTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + PostComment.class, + Tag.class + }; + } + + @Override + public void afterInit() { + doInJPA( entityManager -> { + entityManager.persist( + new Tag().setName("Java") + ); + + entityManager.persist( + new Tag().setName("JPA") + ); + + entityManager.persist( + new Tag().setName("Hibernate") + ); + + entityManager.persist( + new Tag().setName("Misc") + ); + } ); + } + + @Test + public void testRemoveTag() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Java")); + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Hibernate")); + post.addTag(entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Misc")); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(3, post.getTags().size()); + }); + + Tag _miscTag = doInJPA(entityManager -> { + Tag miscTag = entityManager.unwrap(Session.class) + .bySimpleNaturalId(Tag.class) + .getReference("Misc"); + + entityManager.remove(miscTag); + + return miscTag; + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getTags().size()); + }); + + doInJPA(entityManager -> { + //That would not work without @Loader(namedQuery = "findTagById") + assertNull(entityManager.find(Tag.class, _miscTag.getId())); + }); + + doInJPA(entityManager -> { + Boolean exists = entityManager.createQuery(""" + select count(t) = 1 + from Tag t + where t.name = :name + """, Boolean.class) + .setParameter("name", "Misc") + .getSingleResult(); + + assertFalse(exists); + }); + + doInJPA(entityManager -> { + List tags = entityManager.createQuery("select t from Tag t", Tag.class).getResultList(); + //That would not work without @Where(clause = "deleted = false") + assertEquals(3, tags.size()); + }); + } + + @Test + public void testRemovePostDetails() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + + post.addDetails( + new PostDetails() + .setCreatedOn( + Timestamp.valueOf(LocalDateTime.of(2023, 7, 20, 12, 0, 0)) + ) + ); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNotNull(post.getDetails()); + + post.removeDetails(); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNull(post.getDetails()); + }); + + doInJPA(entityManager -> { + assertNull(entityManager.find(PostDetails.class, 1L)); + }); + } + + @Test + public void testRemovePostComment() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("Great!") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("To read") + ) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + + assertNotNull(entityManager.find(PostComment.class, 2L)); + + post.removeComment(post.getComments().get(1)); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, post.getComments().size()); + assertNull(entityManager.find(PostComment.class, 2L)); + }); + } + + @Test + public void testRemovePost() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addDetails( + new PostDetails() + .setCreatedOn( + Timestamp.valueOf( + LocalDateTime.of(2023, 7, 20, 12, 0, 0) + ) + ) + ) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Java")) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Misc")) + .addComment( + new PostComment() + .setId(1L) + .setReview("Great!") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("To read") + ) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments + join fetch p.details + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + entityManager.remove(post); + }); + } + + @Test + public void testRemoveAndFindPostComment() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + post.addComment( + new PostComment() + .setId(1L) + .setReview("Great!") + ); + + post.addComment( + new PostComment() + .setId(2L) + .setReview("Excellent!") + ); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + post.removeComment(post.getComments().get(0)); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, post.getComments().size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @SoftDelete + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY + ) + private PostDetails details; + + @ManyToMany + @JoinTable( + name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + @SoftDelete + private List tags = new ArrayList<>(); + + @Version + private short version; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public PostDetails getDetails() { + return details; + } + + public List getTags() { + return tags; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } + + public Post addDetails(PostDetails details) { + this.details = details; + details.setPost(this); + return this; + } + + public Post removeDetails() { + this.details.setPost(null); + this.details = null; + return this; + } + + public Post addTag(Tag tag) { + tags.add(tag); + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + @SoftDelete + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + public PostDetails() { + createdOn = new Date(); + } + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "id") + @MapsId + @NotFound(action = NotFoundAction.EXCEPTION) + private Post post; + + @Version + private short version; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @SoftDelete + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @NotFound(action = NotFoundAction.EXCEPTION) + private Post post; + + private String review; + + @Version + private short version; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + @SoftDelete + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @Version + private short version; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/softdelete/SoftDeleteVersionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/softdelete/SoftDeleteVersionTest.java new file mode 100644 index 000000000..3a0357c6b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/softdelete/SoftDeleteVersionTest.java @@ -0,0 +1,580 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.softdelete; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.StaleStateException; +import org.hibernate.annotations.Loader; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class SoftDeleteVersionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + PostComment.class, + Tag.class + }; + } + + @Override + public void afterInit() { + doInJPA( entityManager -> { + entityManager.persist(new Tag().setName("Java")); + entityManager.persist(new Tag().setName("JPA")); + entityManager.persist(new Tag().setName("Hibernate")); + entityManager.persist(new Tag().setName("Misc")); + } ); + } + + @Test + public void testRemoveTag() { + doInJPA(entityManager -> { + Post post = new Post() + .setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + + Session session = entityManager.unwrap(Session.class); + + post.addTag(session.bySimpleNaturalId(Tag.class).getReference("Java")); + post.addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")); + post.addTag(session.bySimpleNaturalId(Tag.class).getReference("Misc")); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(3, post.getTags().size()); + }); + + Tag miscTag = doInJPA(entityManager -> { + return entityManager.unwrap(Session.class).bySimpleNaturalId(Tag.class).getReference("Misc"); + }); + + doInJPA(entityManager -> { + entityManager.remove(miscTag); + }); + + doInJPA(entityManager -> { + LOGGER.info("Load post entity"); + Post post = entityManager.find(Post.class, 1L); + LOGGER.info("Load associated Tag entities"); + assertEquals(2, post.getTags().size()); + }); + + doInJPA(entityManager -> { + //That would not work without @Loader(namedQuery = "findTagById") + assertNull(entityManager.find(Tag.class, miscTag.getId())); + + assertEquals( + miscTag.getName(), + entityManager.createNativeQuery(""" + SELECT + name + FROM + tag + WHERE + id = :id + """) + .setParameter("id", miscTag.getId()) + .getSingleResult() + ); + }); + + doInJPA(entityManager -> { + List tags = entityManager.createQuery(""" + select t + from Tag t + """, Tag.class) + .getResultList(); + //That would not work without @Where(clause = "deleted = false") + assertEquals(3, tags.size()); + }); + } + + @Test + public void testRemovePostDetails() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + .addDetails(new PostDetails().setCreatedBy("Vlad Mihalcea")) + .addComment(new PostComment().setReview("Excellent!")) + .addComment(new PostComment().setReview("Great!")) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.details + where p.title = :title + """, Post.class) + .setParameter("title", "High-Performance Java Persistence") + .getSingleResult(); + + assertNotNull(post.getDetails()); + + post.removeDetails(); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNull(post.getDetails()); + }); + + doInJPA(entityManager -> { + assertNull(entityManager.find(PostDetails.class, 1L)); + }); + } + + @Test + public void testRemovePostComment() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + .addDetails(new PostDetails().setCreatedBy("Vlad Mihalcea")) + .addComment(new PostComment().setReview("Excellent!")) + .addComment(new PostComment().setReview("Great!")) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(2, post.getComments().size()); + assertNotNull(entityManager.find(PostComment.class, 2L)); + post.removeComment(post.getComments().get(1)); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, post.getComments().size()); + assertNull(entityManager.find(PostComment.class, 2L)); + }); + } + + @Test + public void testRemoveAndFindPostComment() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + .addDetails(new PostDetails().setCreatedBy("Vlad Mihalcea")) + .addComment(new PostComment().setReview("Excellent!")) + .addComment(new PostComment().setReview("Great!")) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + where p.title = :title + """, Post.class) + .setParameter("title", "High-Performance Java Persistence") + .getSingleResult(); + + post.removeComment(post.getComments().get(0)); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertEquals(1, post.getComments().size()); + }); + } + + @Test + public void testRemovePost() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + .addDetails(new PostDetails().setCreatedBy("Vlad Mihalcea")) + .addComment(new PostComment().setReview("Excellent!")) + .addComment(new PostComment().setReview("Great!")) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + left join fetch p.details + left join fetch p.comments + where p.title = :title + """, Post.class) + .setParameter("title", "High-Performance Java Persistence") + .getSingleResult(); + + entityManager.remove(post); + }); + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNull(post); + }); + } + + @Test + public void testConcurrentUpdate() { + Post post_ = new Post().setTitle("High-Performance Java Persistence"); + doInJPA(entityManager -> { + entityManager.persist(post_); + }); + + try { + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, post_.id); + + executeSync(() -> { + doInJPA(_entityManager -> { + _entityManager.remove(_entityManager.find(Post.class, post_.id)); + }); + }); + + post.setTitle("High-Performance Java Persistence, 2nd edition"); + }); + } catch (Exception e) { + assertTrue(StaleStateException.class.isAssignableFrom(ExceptionUtil.rootCause(e).getClass())); + } + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + assertNull(post); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @SQLDelete(sql = """ + UPDATE post + SET + deleted = true, + version = version + 1 + WHERE + id = ? AND + version = ? + """) + @Loader(namedQuery = "findPostById") + @NamedQuery(name = "findPostById", query = """ + select p + from Post p + where + p.id = ?1 and + p.deleted = false + """) + @Where(clause = "deleted = false") + public static class Post extends SoftDeletable { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + @OneToOne( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY + ) + private PostDetails details; + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable( + name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + @Version + private short version; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public PostDetails getDetails() { + return details; + } + + public List getTags() { + return tags; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } + + public Post addDetails(PostDetails details) { + this.details = details; + details.setPost(this); + return this; + } + + public Post removeDetails() { + this.details.setPost(null); + this.details = null; + return this; + } + + public Post addTag(Tag tag) { + tags.add(tag); + return this; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + @SQLDelete(sql = """ + UPDATE post_details + SET + deleted = true, + version = version + 1 + WHERE + id = ? AND + version = ? + """) + @Loader(namedQuery = "findPostDetailsById") + @NamedQuery(name = "findPostDetailsById", query = """ + select pd + from PostDetails pd + where + pd.id = ?1 and + pd.deleted = false + """) + @Where(clause = "deleted = false") + public static class PostDetails extends SoftDeletable { + + @Id + @GeneratedValue + private Long id; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "id") + @MapsId + private Post post; + + @Version + private short version; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @SQLDelete(sql = """ + UPDATE post_comment + SET + deleted = true, + version = version + 1 + WHERE + id = ? AND + version = ? + """) + @Loader(namedQuery = "findPostCommentById") + @NamedQuery(name = "findPostCommentById", query = """ + select pc + from PostComment pc + where + pc.id = ?1 and + pc.deleted = false + """) + @Where(clause = "deleted = false") + public static class PostComment extends SoftDeletable { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + @Version + private short version; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + @SQLDelete(sql = """ + UPDATE tag + SET + deleted = true, + version = version + 1 + WHERE + id = ? AND + version = ? + """) + @Loader(namedQuery = "findTagById") + @NamedQuery(name = "findTagById", query = """ + select t + from Tag t + where + t.id = ?1 and + t.deleted = false + """) + @Where(clause = "deleted = false") + public static class Tag extends SoftDeletable { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @Version + private short version; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } + + @MappedSuperclass + public static abstract class SoftDeletable { + + private boolean deleted; + + public boolean isDeleted() { + return deleted; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/types/CreationDetails.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/types/CreationDetails.java new file mode 100644 index 000000000..53b72a519 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/types/CreationDetails.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.types; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Embeddable +public class CreationDetails { + + @Column(name = "created_on") + private LocalDateTime createdOn = LocalDateTime.now(); + + @Column(name = "created_by") + private String createdBy; + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public CreationDetails setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public CreationDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/types/EmbeddedTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/types/EmbeddedTest.java new file mode 100644 index 000000000..5461100a6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/types/EmbeddedTest.java @@ -0,0 +1,61 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.types; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class EmbeddedTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + + @Override + protected void beforeInit() { + executeStatement("drop table if exists post cascade"); + executeStatement("create table post (id integer not null, title varchar(100), status tinyint check (status between 0 and 2), created_on datetime(6), created_by varchar(100), primary key (id))"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1) + .setTitle("High-Performance Java Persistence") + .setStatus(PostStatus.PENDING) + .setCreationDetails( + new CreationDetails() + .setCreatedBy("Vlad Mihalcea") + ) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, 1L); + + assertEquals("Vlad Mihalcea", post.getCreationDetails().getCreatedBy()); + }); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/types/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/types/Post.java new file mode 100644 index 000000000..ff7819390 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/types/Post.java @@ -0,0 +1,58 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.types; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post") +public class Post { + + @Id + private Integer id; + + private String title; + + @Enumerated(EnumType.ORDINAL) + private PostStatus status; + + @Embedded + private CreationDetails creationDetails; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + + public CreationDetails getCreationDetails() { + return creationDetails; + } + + public Post setCreationDetails(CreationDetails creation) { + this.creationDetails = creation; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/types/PostStatus.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/types/PostStatus.java new file mode 100644 index 000000000..c44fc290a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/mapping/types/PostStatus.java @@ -0,0 +1,10 @@ +package com.vladmihalcea.hpjp.hibernate.mapping.types; + +/** + * @author Vlad Mihalcea + */ +public enum PostStatus { + PENDING, + APPROVED, + SPAM +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/metadata/MetadataExtractorIntegrator.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/metadata/MetadataExtractorIntegrator.java new file mode 100644 index 000000000..067c6b70d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/metadata/MetadataExtractorIntegrator.java @@ -0,0 +1,44 @@ +package com.vladmihalcea.hpjp.hibernate.metadata; + +import org.hibernate.boot.Metadata; +import org.hibernate.boot.model.relational.Database; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; + +/** + * @author Vlad Mihalcea + */ +public class MetadataExtractorIntegrator implements org.hibernate.integrator.spi.Integrator { + + public static final MetadataExtractorIntegrator INSTANCE = new MetadataExtractorIntegrator(); + + private Database database; + + private Metadata metadata; + + public Database getDatabase() { + return database; + } + + public Metadata getMetadata() { + return metadata; + } + + @Override + public void integrate( + Metadata metadata, + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + this.database = metadata.getDatabase(); + this.metadata = metadata; + + } + + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/metadata/MetadataTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/metadata/MetadataTest.java new file mode 100644 index 000000000..078f410c9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/metadata/MetadataTest.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.hibernate.metadata; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.model.relational.Namespace; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.mapping.Table; +import org.junit.Test; + +/** + * @author Vlad Mihalcea + */ +public class MetadataTest extends AbstractMySQLIntegrationTest { + + @Override + protected Integrator integrator() { + return MetadataExtractorIntegrator.INSTANCE; + } + + @Override + protected Class[] entities() { + return new BlogEntityProvider().entities(); + } + + @Test + public void testDatabaseMetadata() { + for (Namespace namespace : MetadataExtractorIntegrator.INSTANCE.getDatabase().getNamespaces()) { + for (Table table : namespace.getTables()) { + LOGGER.info("Table {} has the following columns: {}", + table, + table.getColumns().stream().map(Column::getName).toList() + ); + } + } + } + + @Test + public void testEntityToDatabaseBindingMetadata() { + Metadata metadata = MetadataExtractorIntegrator.INSTANCE.getMetadata(); + + for (PersistentClass persistentClass : metadata.getEntityBindings()) { + Table table = persistentClass.getTable(); + LOGGER.info("Entity: {} is mapped to table: {}", + persistentClass.getClassName(), + table.getName() + ); + + for (Property property : persistentClass.getProperties()) { + for (Column column : property.getColumns()) { + LOGGER.info("Property: {} is mapped on table column: {} of type: {}", + property.getName(), + column.getName(), + column.getSqlType() + ); + } + } + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/CatalogMultitenancyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/CatalogMultitenancyTest.java new file mode 100644 index 000000000..709bd1969 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/CatalogMultitenancyTest.java @@ -0,0 +1,233 @@ +package com.vladmihalcea.hpjp.hibernate.multitenancy; + +import com.mysql.cj.jdbc.MysqlDataSource; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Environment; +import org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl; +import org.junit.Test; + +import javax.sql.DataSource; +import java.sql.Statement; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class CatalogMultitenancyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + User.class, + Post.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + properties.setProperty(AvailableSettings.SHOW_SQL, "true"); + properties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, MultiTenantConnectionProvider.INSTANCE); + properties.setProperty(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, TenantContext.TenantIdentifierResolver.class.getName()); + } + + @Override + public void afterInit() { + MysqlDataSource defaultDataSource = (MysqlDataSource) database().dataSourceProvider().dataSource(); + addTenantConnectionProvider(TenantContext.DEFAULT_TENANT_IDENTIFIER, defaultDataSource, propertiesMap()); + + createCatalog("europe"); + createCatalog("asia"); + } + + private void createCatalog(String catalogName) { + executeStatement( + String.format("drop database if exists %s", catalogName), + String.format("create database %s", catalogName), + String.format("USE %s", catalogName), + "create table posts (id bigint not null auto_increment, created_on datetime(6), title varchar(255), user_id bigint, primary key (id)) engine=InnoDB", + "create table users (id bigint not null auto_increment, registered_on datetime(6), firstName varchar(255), lastName varchar(255), primary key (id)) engine=InnoDB", + "alter table posts add constraint fk_user_id foreign key (user_id) references users (id)" + ); + + addTenantConnectionProvider(catalogName); + } + + private void addTenantConnectionProvider(String tenantId) { + DataSourceProvider dataSourceProvider = database().dataSourceProvider(); + + Map properties = propertiesMap(); + + MysqlDataSource tenantDataSource = new MysqlDataSource(); + tenantDataSource.setDatabaseName(tenantId); + tenantDataSource.setUser(dataSourceProvider.username()); + tenantDataSource.setPassword(dataSourceProvider.password()); + + properties.put( + Environment.DATASOURCE, + dataSourceProxyType().dataSource(tenantDataSource) + ); + + addTenantConnectionProvider(tenantId, tenantDataSource, properties); + } + + private void addTenantConnectionProvider(String tenantId, DataSource tenantDataSource, Map properties) { + DatasourceConnectionProviderImpl connectionProvider = new DatasourceConnectionProviderImpl(); + connectionProvider.setDataSource(tenantDataSource); + connectionProvider.configure(properties); + MultiTenantConnectionProvider.INSTANCE.getConnectionProviderMap().put( + tenantId, connectionProvider + ); + } + + @Test + public void test() { + TenantContext.setTenant("europe"); + + User vlad = doInJPA(entityManager -> { + + User user = new User(); + user.setFirstName("Vlad"); + user.setLastName("Mihalcea"); + + entityManager.persist(user); + + return user; + }); + + TenantContext.setTenant("asia"); + + doInJPA(entityManager -> { + + User user = new User(); + user.setFirstName("John"); + user.setLastName("Doe"); + + entityManager.persist(user); + }); + + TenantContext.setTenant("europe"); + + doInJPA(entityManager -> { + + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + post.setUser(vlad); + entityManager.persist(post); + }); + } + + @Entity(name = "User") + @Table(name = "users") + public static class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String firstName; + + private String lastName; + + @Column(name = "registered_on") + @CreationTimestamp + private LocalDateTime createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + } + + @Entity(name = "Post") + @Table(name = "posts") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + @Column(name = "created_on") + @CreationTimestamp + private LocalDateTime createdOn; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/MultiTenantConnectionProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/MultiTenantConnectionProvider.java new file mode 100644 index 000000000..ea5b795d1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/MultiTenantConnectionProvider.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.hpjp.hibernate.multitenancy; + +import org.hibernate.engine.jdbc.connections.spi.AbstractMultiTenantConnectionProvider; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class MultiTenantConnectionProvider + extends AbstractMultiTenantConnectionProvider { + + public static final MultiTenantConnectionProvider INSTANCE = + new MultiTenantConnectionProvider(); + + private final Map connectionProviderMap = new HashMap<>(); + + Map getConnectionProviderMap() { + return connectionProviderMap; + } + + @Override + protected ConnectionProvider getAnyConnectionProvider() { + return connectionProviderMap.get(TenantContext.DEFAULT_TENANT_IDENTIFIER); + } + + @Override + protected ConnectionProvider selectConnectionProvider(Object tenantIdentifier) { + return connectionProviderMap.get(tenantIdentifier); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/SchemaMultitenancyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/SchemaMultitenancyTest.java new file mode 100644 index 000000000..fb0d6dcf9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/SchemaMultitenancyTest.java @@ -0,0 +1,257 @@ +package com.vladmihalcea.hpjp.hibernate.multitenancy; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Environment; +import org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl; +import org.junit.Test; +import org.postgresql.ds.PGSimpleDataSource; + +import javax.sql.DataSource; +import java.sql.Statement; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class SchemaMultitenancyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + User.class, + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + properties.setProperty(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, TenantContext.TenantIdentifierResolver.class.getName()); + properties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, MultiTenantConnectionProvider.INSTANCE); + } + + @Override + public void afterInit() { + PGSimpleDataSource defaultDataSource = (PGSimpleDataSource) database().dataSourceProvider().dataSource(); + addTenantConnectionProvider(TenantContext.DEFAULT_TENANT_IDENTIFIER, defaultDataSource, propertiesMap()); + + createSchema("europe"); + createSchema("asia"); + } + + private void createSchema(String schemaName) { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + statement.executeUpdate(String.format("drop schema if exists %s cascade", schemaName)); + statement.executeUpdate(String.format("create schema %s", schemaName)); + statement.executeUpdate(String.format("SET search_path TO %s,public", schemaName)); + + statement.executeUpdate("create sequence if not exists users_seq start 1 increment 1"); + statement.executeUpdate("create sequence if not exists posts_seq start 1 increment 1"); + statement.executeUpdate("create table posts (id int8 not null, created_on timestamp, title varchar(255), user_id int8, primary key (id))"); + statement.executeUpdate("create table users (id int8 not null, registered_on timestamp, firstName varchar(255), lastName varchar(255), primary key (id))"); + statement.executeUpdate("alter table if exists posts add constraint fk_user_id foreign key (user_id) references users"); + + statement.executeUpdate("SET search_path TO public"); + } + }); + }); + + addTenantConnectionProvider(schemaName); + } + + private void addTenantConnectionProvider(String tenantId) { + PGSimpleDataSource defaultDataSource = (PGSimpleDataSource) database().dataSourceProvider().dataSource(); + + Map properties = propertiesMap(); + + PGSimpleDataSource tenantDataSource = new PGSimpleDataSource(); + tenantDataSource.setDatabaseName(defaultDataSource.getDatabaseName()); + tenantDataSource.setCurrentSchema(tenantId); + tenantDataSource.setServerName(defaultDataSource.getServerName()); + tenantDataSource.setUser(defaultDataSource.getUser()); + tenantDataSource.setPassword(defaultDataSource.getPassword()); + + properties.put( + Environment.DATASOURCE, + dataSourceProxyType().dataSource(tenantDataSource) + ); + + addTenantConnectionProvider(tenantId, tenantDataSource, properties); + } + + private void addTenantConnectionProvider(String tenantId, DataSource tenantDataSource, Map properties) { + DatasourceConnectionProviderImpl connectionProvider = new DatasourceConnectionProviderImpl(); + connectionProvider.setDataSource(tenantDataSource); + connectionProvider.configure(properties); + MultiTenantConnectionProvider.INSTANCE.getConnectionProviderMap().put( + tenantId, connectionProvider + ); + } + + @Test + public void test() { + TenantContext.setTenant("europe"); + + User vlad = doInJPA(entityManager -> { + + LOGGER.info( + "Current schema: {}", + entityManager.createNativeQuery("select current_schema()").getSingleResult() + ); + + User user = new User(); + user.setFirstName("Vlad"); + user.setLastName("Mihalcea"); + + entityManager.persist(user); + + return user; + }); + + TenantContext.setTenant("asia"); + + doInJPA(entityManager -> { + + LOGGER.info( + "Current schema: {}", + entityManager.createNativeQuery("select current_schema()").getSingleResult() + ); + + User user = new User(); + user.setFirstName("John"); + user.setLastName("Doe"); + + entityManager.persist(user); + }); + + TenantContext.setTenant("europe"); + + doInJPA(entityManager -> { + + LOGGER.info( + "Current schema: {}", + entityManager.createNativeQuery("select current_schema()").getSingleResult() + ); + + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + post.setUser(vlad); + entityManager.persist(post); + }); + } + + @Entity(name = "User") + @Table(name = "users") + public static class User { + + @Id + @GeneratedValue + private Long id; + + private String firstName; + + private String lastName; + + @Column(name = "registered_on") + @CreationTimestamp + private LocalDateTime createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + } + + @Entity(name = "Post") + @Table(name = "posts") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @Column(name = "created_on") + @CreationTimestamp + private LocalDateTime createdOn; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/TenantContext.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/TenantContext.java new file mode 100644 index 000000000..020dd1823 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/TenantContext.java @@ -0,0 +1,35 @@ +package com.vladmihalcea.hpjp.hibernate.multitenancy; + +import org.hibernate.context.spi.CurrentTenantIdentifierResolver; + +/** + * @author Vlad Mihalcea + */ +public class TenantContext { + + public static final String DEFAULT_TENANT_IDENTIFIER = "public"; + + private static final ThreadLocal TENANT_IDENTIFIER = new ThreadLocal<>(); + + public static void setTenant(String tenantIdentifier) { + TENANT_IDENTIFIER.set(tenantIdentifier); + } + + public static void reset() { + TENANT_IDENTIFIER.remove(); + } + + public static class TenantIdentifierResolver implements CurrentTenantIdentifierResolver { + + @Override + public String resolveCurrentTenantIdentifier() { + String currentTenantId = TENANT_IDENTIFIER.get(); + return currentTenantId != null ? currentTenantId : DEFAULT_TENANT_IDENTIFIER; + } + + @Override + public boolean validateExistingCurrentSessions() { + return false; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/PartitionContext.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/PartitionContext.java new file mode 100644 index 000000000..8226cfca8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/PartitionContext.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.hibernate.multitenancy.partition; + +/** + * @author Vlad Mihalcea + */ +public class PartitionContext { + + public static final String DEFAULT_PARTITION = "default"; + + private static final ThreadLocal CURRENT_PARTITION = new ThreadLocal<>(); + + public static String get() { + String currentTenantId = CURRENT_PARTITION.get(); + return currentTenantId != null ? currentTenantId : DEFAULT_PARTITION; + } + + public static void set(String tenantId) { + CURRENT_PARTITION.set(tenantId); + } + + public static void reset() { + CURRENT_PARTITION.remove(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/PostgreSQLTablePartitionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/PostgreSQLTablePartitionTest.java new file mode 100644 index 000000000..24d7469b9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/PostgreSQLTablePartitionTest.java @@ -0,0 +1,163 @@ +package com.vladmihalcea.hpjp.hibernate.multitenancy.partition; + +import com.vladmihalcea.hpjp.hibernate.multitenancy.partition.model.Post; +import com.vladmihalcea.hpjp.hibernate.multitenancy.partition.model.User; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLTablePartitionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + User.class, + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + + @Override + protected void beforeInit() { + executeStatement("DROP TABLE IF EXISTS posts cascade"); + executeStatement("DROP TABLE IF EXISTS users cascade"); + executeStatement("DROP SEQUENCE IF EXISTS posts_SEQ"); + executeStatement("DROP SEQUENCE IF EXISTS users_SEQ"); + + executeStatement("CREATE SEQUENCE posts_SEQ START WITH 1 INCREMENT BY 50"); + executeStatement("CREATE SEQUENCE users_SEQ START WITH 1 INCREMENT BY 50"); + + executeStatement(""" + CREATE TABLE users ( + id bigint NOT NULL, + first_name varchar(255), + last_name varchar(255), + registered_on timestamp(6), + partition_key varchar(255), + PRIMARY KEY (id, partition_key) + ) PARTITION BY LIST (partition_key) + """); + executeStatement("CREATE TABLE users_asia PARTITION OF users FOR VALUES IN ('Asia')"); + executeStatement("CREATE TABLE users_africa PARTITION OF users FOR VALUES IN ('Africa')"); + executeStatement("CREATE TABLE users_north_america PARTITION OF users FOR VALUES IN ('North America')"); + executeStatement("CREATE TABLE users_south_america PARTITION OF users FOR VALUES IN ('South America')"); + executeStatement("CREATE TABLE users_europe PARTITION OF users FOR VALUES IN ('Europe')"); + executeStatement("CREATE TABLE users_australia PARTITION OF users FOR VALUES IN ('Australia')"); + + executeStatement(""" + CREATE TABLE posts ( + id bigint NOT NULL, + title varchar(255), + created_on timestamp(6), + user_id bigint, + partition_key varchar(255), + PRIMARY KEY (id, partition_key) + ) PARTITION BY LIST (partition_key) + """); + executeStatement("CREATE TABLE posts_asia PARTITION OF posts FOR VALUES IN ('Asia')"); + executeStatement("CREATE TABLE posts_africa PARTITION OF posts FOR VALUES IN ('Africa')"); + executeStatement("CREATE TABLE posts_north_america PARTITION OF posts FOR VALUES IN ('North America')"); + executeStatement("CREATE TABLE posts_south_america PARTITION OF posts FOR VALUES IN ('South America')"); + executeStatement("CREATE TABLE posts_europe PARTITION OF posts FOR VALUES IN ('Europe')"); + executeStatement("CREATE TABLE posts_australia PARTITION OF posts FOR VALUES IN ('Australia')"); + + executeStatement(""" + ALTER TABLE IF EXISTS posts + ADD CONSTRAINT fk_posts_user_id FOREIGN KEY (user_id, partition_key) REFERENCES users + """); + } + + @Test + public void test() { + PartitionContext.set("Europe"); + + User vlad = doInJPA(entityManager -> { + User user = new User() + .setFirstName("Vlad") + .setLastName("Mihalcea"); + + entityManager.persist(user); + return user; + }); + + PartitionContext.set("North America"); + + doInJPA(entityManager -> { + entityManager.persist( + new User() + .setFirstName("John") + .setLastName("Doe") + ); + + entityManager.persist( + new User() + .setFirstName("Jane") + .setLastName("Doe") + ); + }); + + PartitionContext.set("Europe"); + + Post _post = doInJPA(entityManager -> { + Post post = new Post() + .setTitle("High-Performance Java Persistence") + .setUser(vlad); + + entityManager.persist(post); + + return post; + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, _post.getId()); + + entityManager.remove(post); + }); + + PartitionContext.set("North America"); + + doInJPA(entityManager -> { + List allUsers = entityManager.createQuery(""" + select u + from User u + order by u.id + """, User.class) + .getResultList(); + + assertEquals(3, allUsers.size()); + + entityManager + .unwrap(Session.class) + .enableFilter("partitionKey") + .setParameter("partitionKey", PartitionContext.get()); + + List northAmericanUsers = entityManager.createQuery(""" + select u + from User u + order by u.id + """, User.class) + .getResultList(); + + assertEquals(2, northAmericanUsers.size()); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/TablePartitionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/TablePartitionTest.java new file mode 100644 index 000000000..571eef075 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/TablePartitionTest.java @@ -0,0 +1,100 @@ +package com.vladmihalcea.hpjp.hibernate.multitenancy.partition; + +import com.vladmihalcea.hpjp.hibernate.multitenancy.partition.model.Post; +import com.vladmihalcea.hpjp.hibernate.multitenancy.partition.model.User; +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Session; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class TablePartitionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + User.class, + Post.class + }; + } + + @Test + public void test() { + PartitionContext.set("Europe"); + + User vlad = doInJPA(entityManager -> { + User user = new User() + .setFirstName("Vlad") + .setLastName("Mihalcea"); + + entityManager.persist(user); + return user; + }); + + PartitionContext.set("North America"); + + doInJPA(entityManager -> { + entityManager.persist( + new User() + .setFirstName("John") + .setLastName("Doe") + ); + + entityManager.persist( + new User() + .setFirstName("Jane") + .setLastName("Doe") + ); + }); + + PartitionContext.set("Europe"); + + Post _post = doInJPA(entityManager -> { + Post post = new Post() + .setTitle("High-Performance Java Persistence") + .setUser(vlad); + + entityManager.persist(post); + + return post; + }); + + doInJPA(entityManager -> { + Post post = entityManager.find(Post.class, _post.getId()); + + entityManager.remove(post); + }); + + PartitionContext.set("North America"); + + doInJPA(entityManager -> { + List allUsers = entityManager.createQuery(""" + select u + from User u + order by u.id + """, User.class) + .getResultList(); + + assertEquals(3, allUsers.size()); + + entityManager + .unwrap(Session.class) + .enableFilter("partitionKey") + .setParameter("partitionKey", PartitionContext.get()); + + List northAmericanUsers = entityManager.createQuery(""" + select u + from User u + order by u.id + """, User.class) + .getResultList(); + + assertEquals(2, northAmericanUsers.size()); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/model/PartitionAware.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/model/PartitionAware.java new file mode 100644 index 000000000..adbffe661 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/model/PartitionAware.java @@ -0,0 +1,40 @@ +package com.vladmihalcea.hpjp.hibernate.multitenancy.partition.model; + +import com.vladmihalcea.hpjp.hibernate.multitenancy.partition.PartitionContext; +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import org.hibernate.annotations.Filter; +import org.hibernate.annotations.FilterDef; +import org.hibernate.annotations.ParamDef; +import org.hibernate.annotations.PartitionKey; + +/** + * @author Vlad Mihalcea + */ +@MappedSuperclass +@FilterDef( + name = "partitionKey", + parameters = @ParamDef( + name = "partitionKey", + type = String.class + ) +) +@Filter( + name = "partitionKey", + condition = "partition_key = :partitionKey" +) +public abstract class PartitionAware { + + @Column(name = "partition_key") + @PartitionKey + private String partitionKey = PartitionContext.get(); + + public String getPartitionKey() { + return partitionKey; + } + + public T setPartitionKey(String partitionKey) { + this.partitionKey = partitionKey; + return (T) this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/model/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/model/Post.java new file mode 100644 index 000000000..49c955d4f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/model/Post.java @@ -0,0 +1,64 @@ +package com.vladmihalcea.hpjp.hibernate.multitenancy.partition.model; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "posts") +public class Post extends PartitionAware { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @Column(name = "created_on") + @CreationTimestamp + private LocalDateTime createdOn; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public User getUser() { + return user; + } + + public Post setUser(User user) { + this.user = user; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/model/User.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/model/User.java new file mode 100644 index 000000000..08d10f052 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/multitenancy/partition/model/User.java @@ -0,0 +1,64 @@ +package com.vladmihalcea.hpjp.hibernate.multitenancy.partition.model; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "users") +public class User extends PartitionAware { + + @Id + @GeneratedValue + private Long id; + + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + @Column(name = "registered_on") + @CreationTimestamp + private LocalDateTime createdOn; + + public Long getId() { + return id; + } + + public User setId(Long id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public User setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public User setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public User setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/CamelCaseToSnakeCaseNamingStrategy.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/CamelCaseToSnakeCaseNamingStrategy.java new file mode 100644 index 000000000..82164c12c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/CamelCaseToSnakeCaseNamingStrategy.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hpjp.hibernate.naming; + +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; + +/** + * @author Vlad Mihalcea + */ +public class CamelCaseToSnakeCaseNamingStrategy extends PhysicalNamingStrategyStandardImpl { + + @Override + public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) { + return formatIdentifier(super.toPhysicalTableName(name, context)); + } + + @Override + public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment context) { + return formatIdentifier(super.toPhysicalColumnName(name, context)); + } + + private Identifier formatIdentifier(Identifier identifier) { + String name = identifier.getText(); + + String formattedName = name.replaceAll("([a-z]+)([A-Z]+)", "$1\\_$2").toLowerCase(); + + return !formattedName.equals(name) ? + Identifier.toIdentifier(formattedName, identifier.isQuoted()) : + identifier; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/CamelCaseToSnakeCaseNamingStrategyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/CamelCaseToSnakeCaseNamingStrategyTest.java new file mode 100644 index 000000000..8ac9fa8a7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/CamelCaseToSnakeCaseNamingStrategyTest.java @@ -0,0 +1,153 @@ +package com.vladmihalcea.hpjp.hibernate.naming; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import java.time.LocalDate; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class CamelCaseToSnakeCaseNamingStrategyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + BookAuthor.class, + PaperbackBook.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.physical_naming_strategy", + CamelCaseToSnakeCaseNamingStrategy.class.getName() + ); + } + + @Test + public void test() { + doInJPA(entityManager -> { + BookAuthor author = new BookAuthor(); + author.setId(1L); + author.setFirstName("Vlad"); + author.setLastName("Mihalcea"); + + entityManager.persist(author); + + PaperbackBook book = new PaperbackBook(); + book.setISBN("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishedOn(LocalDate.of(2016, 10, 12)); + book.setPublishedBy(author); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + PaperbackBook book = entityManager.find(PaperbackBook.class, "978-9730228236"); + assertEquals("High-Performance Java Persistence", book.getTitle()); + + assertEquals("Vlad Mihalcea", book.getPublishedBy().getFullName()); + }); + } + + @Entity(name = "BookAuthor") + public static class BookAuthor { + + @Id + private Long id; + + private String firstName; + + private String lastName; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getFullName() { + return firstName + " " + lastName; + } + } + + @Entity(name = "PaperbackBook") + public static class PaperbackBook { + + @Id + private String ISBN; + + private String title; + + private LocalDate publishedOn; + + @ManyToOne(fetch = FetchType.LAZY) + private BookAuthor publishedBy; + + public String getISBN() { + return ISBN; + } + + public void setISBN(String ISBN) { + this.ISBN = ISBN; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getPublishedOn() { + return publishedOn; + } + + public void setPublishedOn(LocalDate publishedOn) { + this.publishedOn = publishedOn; + } + + public BookAuthor getPublishedBy() { + return publishedBy; + } + + public void setPublishedBy(BookAuthor publishedBy) { + this.publishedBy = publishedBy; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/naming/DefaultNamingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/DefaultNamingTest.java similarity index 85% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/naming/DefaultNamingTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/DefaultNamingTest.java index 2a9af7cad..057a70ff2 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/naming/DefaultNamingTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/DefaultNamingTest.java @@ -1,18 +1,18 @@ -package com.vladmihalcea.book.hpjp.hibernate.naming; +package com.vladmihalcea.hpjp.hibernate.naming; -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.ManyToOne; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; import static org.junit.Assert.assertEquals; /** * @author Vlad Mihalcea */ -public class DefaultNamingTest extends AbstractOracleXEIntegrationTest { +public class DefaultNamingTest extends AbstractTest { @Override protected Class[] entities() { diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/ExtendedNamingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/ExtendedNamingTest.java new file mode 100644 index 000000000..c3cc56e38 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/ExtendedNamingTest.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.hibernate.naming; + +import com.vladmihalcea.hpjp.util.providers.Database; + +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class ExtendedNamingTest extends DefaultNamingTest { + + @Override + protected Database database() { + return Database.ORACLE; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.physical_naming_strategy", + OracleNamingStrategy.class.getName() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/naming/OracleNamingStrategy.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/OracleNamingStrategy.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/naming/OracleNamingStrategy.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/OracleNamingStrategy.java index cc9b42bdb..7955e2715 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/naming/OracleNamingStrategy.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/naming/OracleNamingStrategy.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.hibernate.naming; +package com.vladmihalcea.hpjp.hibernate.naming; import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/pc/CloneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/pc/CloneTest.java new file mode 100644 index 000000000..e04897ef3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/pc/CloneTest.java @@ -0,0 +1,289 @@ +package com.vladmihalcea.hpjp.hibernate.pc; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.junit.Test; + +import java.util.*; + +public class CloneTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostDetails.class, + PostComment.class, + Tag.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Tag java = new Tag(); + java.setName("Java"); + + entityManager.persist(java); + + Tag jdbc = new Tag(); + jdbc.setName("JDBC"); + + entityManager.persist(jdbc); + + Tag jpa = new Tag(); + jpa.setName("JPA"); + + entityManager.persist(jpa); + + Tag jooq = new Tag(); + jooq.setName("jOOQ"); + + entityManager.persist(jooq); + }); + + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java Persistence, 1st edition"); + + PostDetails details = new PostDetails(); + details.setCreatedBy("Vlad Mihalcea"); + post.addDetails(details); + + post.getTags().add(entityManager.getReference(Tag.class, "Java")); + post.getTags().add(entityManager.getReference(Tag.class, "JDBC")); + post.getTags().add(entityManager.getReference(Tag.class, "JPA")); + post.getTags().add(entityManager.getReference(Tag.class, "jOOQ")); + + PostComment comment1 = new PostComment(); + comment1.setReview("This book is a big one"); + post.addComment(comment1); + + PostComment comment2 = new PostComment(); + comment2.setReview("5 stars"); + post.addComment(comment2); + + entityManager.persist(post); + }); + } + + @Test + public void testClone() { + doInJPA(entityManager -> { + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.details + join fetch p.tags + where p.title = :title + """, Post.class) + .setParameter("title", "High-Performance Java Persistence, 1st edition") + .getSingleResult(); + + Post postClone = new Post(post); + postClone.setTitle(postClone.getTitle().replace("1st", "2nd")); + entityManager.persist(postClone); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true, fetch = FetchType.LAZY) + private PostDetails details; + + @ManyToMany + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private Set tags = new HashSet<>(); + + /** + * Needed by Hibernate when hydrating the entity + * from the JDBC ResultSet + */ + private Post() {} + + public Post(Post post) { + this.title = post.getTitle(); + + addDetails(new PostDetails(post.getDetails())); + + tags.addAll(post.getTags()); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public PostDetails getDetails() { + return details; + } + + public Set getTags() { + return tags; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void addDetails(PostDetails details) { + this.details = details; + details.setPost(this); + } + + public void removeDetails() { + this.details.setPost(null); + this.details = null; + } + } + + @Entity(name = "PostDetails") + @Table(name = "post_details") + public static class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + @CreationTimestamp + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + /** + * Needed by Hibernate when hydrating the entity + * from the JDBC ResultSet + */ + private PostDetails() { + } + + public PostDetails(PostDetails details) { + this.createdBy = details.getCreatedBy(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/pc/SaveVariantsSelectBeforeUpdateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/pc/SaveVariantsSelectBeforeUpdateTest.java new file mode 100644 index 000000000..7f34a4aa8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/pc/SaveVariantsSelectBeforeUpdateTest.java @@ -0,0 +1,93 @@ +package com.vladmihalcea.hpjp.hibernate.pc; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.Session; +import org.hibernate.annotations.SelectBeforeUpdate; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +public class SaveVariantsSelectBeforeUpdateTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void testUpdate() { + Book _book = doInJPA(entityManager -> { + Book book = new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea"); + + entityManager.persist(book); + + return book; + }); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + session.update(_book); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + @SelectBeforeUpdate + public static class Book { + + @Id + @GeneratedValue + private Long id; + + private String isbn; + + private String title; + + private String author; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/pc/SaveVariantsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/pc/SaveVariantsTest.java new file mode 100644 index 000000000..687aa5bb3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/pc/SaveVariantsTest.java @@ -0,0 +1,278 @@ +package com.vladmihalcea.hpjp.hibernate.pc; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.NonUniqueObjectException; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.junit.Assert.assertFalse; + +public class SaveVariantsTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void testPersist() { + doInJPA(entityManager -> { + Book book = new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea"); + + entityManager.persist(book); + + LOGGER.info("Persisting the Book entity with the id: {}", book.getId()); + }); + } + + @Test + public void testSave() { + doInJPA(entityManager -> { + Book book = new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea"); + + Session session = entityManager.unwrap(Session.class); + + Long id = (Long) session.save(book); + + LOGGER.info("Saving the Book entity with the id: {}", id); + }); + } + + @Test + public void testUpdate() { + Book _book = doInJPA(entityManager -> { + Book book = new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea"); + + entityManager.persist(book); + + return book; + }); + + LOGGER.info("Modifying the Book entity"); + + _book.setTitle("High-Performance Java Persistence, 2nd edition"); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + session.update(_book); + + LOGGER.info("Updating the Book entity"); + }); + } + + @Test + public void testUpdateWithoutModification() { + Book _book = doInJPA(entityManager -> { + Book book = new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea"); + + entityManager.persist(book); + + return book; + }); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + session.update(_book); + + LOGGER.info("Updating the Book entity"); + }); + } + + @Test(expected = NonUniqueObjectException.class) + public void testUpdateFailAlreadyManaged() { + Book _book = doInJPA(entityManager -> { + Book book = new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea"); + + entityManager.persist(book); + + return book; + }); + + _book.setTitle("High-Performance Java Persistence, 2nd edition"); + + doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, _book.getId()); + + Session session = entityManager.unwrap(Session.class); + + session.update(_book); + }); + } + + @Test + public void testSaveUpdate() { + Book _book = doInJPA(entityManager -> { + Book book = new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea"); + + Session session = entityManager.unwrap(Session.class); + session.saveOrUpdate(book); + + return book; + }); + + _book.setTitle("High-Performance Java Persistence, 2nd edition"); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + session.saveOrUpdate(_book); + }); + } + + @Test + public void testSaveOrUpdateFailAlreadyManaged() { + Book _book = doInJPA(entityManager -> { + Book book = new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea"); + + Session session = entityManager.unwrap(Session.class); + session.saveOrUpdate(book); + + return book; + }); + + _book.setTitle("High-Performance Java Persistence, 2nd edition"); + + try { + doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, _book.getId()); + + Session session = entityManager.unwrap(Session.class); + session.saveOrUpdate(_book); + }); + } catch (NonUniqueObjectException e) { + LOGGER.error( + "The Persistence Context cannot hold " + + "two representations of the same entity", + e + ); + } + } + + @Test + public void testMerge() { + Book _book = doInJPA(entityManager -> { + Book book = new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea"); + + entityManager.persist(book); + + return book; + }); + + LOGGER.info("Modifying the Book entity"); + + _book.setTitle("High-Performance Java Persistence, 2nd edition"); + + doInJPA(entityManager -> { + Book book = entityManager.merge(_book); + + LOGGER.info("Merging the Book entity"); + + assertFalse(book == _book); + }); + } + + @Test + public void testMergeAlreadyManaged() { + Book _book = doInJPA(entityManager -> { + Book book = new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea"); + + entityManager.persist(book); + + return book; + }); + + _book.setTitle("High-Performance Java Persistence, 2nd edition"); + + doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, _book.getId()); + + entityManager.merge(_book); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + private String isbn; + + private String title; + + private String author; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/EscapeLikeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/EscapeLikeTest.java similarity index 88% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/EscapeLikeTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/EscapeLikeTest.java index 46421d7bb..ed8bbd2bd 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/EscapeLikeTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/EscapeLikeTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.query; +package com.vladmihalcea.hpjp.hibernate.query; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.Session; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.Id; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; import java.util.List; import static org.junit.Assert.assertEquals; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/InQueryTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/InQueryTest.java new file mode 100644 index 000000000..4bbea98f7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/InQueryTest.java @@ -0,0 +1,97 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class InQueryTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.query.in_clause_parameter_padding", "true"); + } + + @Test + public void testPadding() { + doInJPA(entityManager -> { + for (int i = 1; i <= 15; i++) { + Post post = new Post(); + post.setId(i); + post.setTitle(String.format("Post no. %d", i)); + + entityManager.persist(post); + } + }); + + doInJPA(entityManager -> { + assertEquals(3, getPostByIds(entityManager, 1, 2, 3).size()); + assertEquals(4, getPostByIds(entityManager, 1, 2, 3, 4).size()); + assertEquals(5, getPostByIds(entityManager, 1, 2, 3, 4, 5).size()); + assertEquals(6, getPostByIds(entityManager, 1, 2, 3, 4, 5, 6).size()); + assertEquals(7, getPostByIds(entityManager, 1, 2, 3, 4, 5, 6, 7).size()); + assertEquals(8, getPostByIds(entityManager, 1, 2, 3, 4, 5, 6, 7, 8).size()); + }); + } + + private List getPostByIds(EntityManager entityManager, Integer... ids) { + return entityManager.createQuery(""" + select p + from Post p + where p.id in :ids + """, Post.class) + .setParameter("ids", Arrays.asList(ids)) + .getResultList(); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @Column(name = "user_id") + private Integer id; + + private String title; + + public Post() {} + + public Post(String title) { + this.title = title; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/JoinUnrelatedEntitiesTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/JoinUnrelatedEntitiesTest.java new file mode 100644 index 000000000..159f27551 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/JoinUnrelatedEntitiesTest.java @@ -0,0 +1,211 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class JoinUnrelatedEntitiesTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PageView.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setSlug("/books/high-performance-java-persistence"); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + }); + doInJPA(entityManager -> { + Post post = new Post(); + post.setSlug("/presentations"); + post.setTitle("Presentations"); + + entityManager.persist(post); + }); + doInJPA(entityManager -> { + PageView pageView = new PageView(); + pageView.setSlug("/books/high-performance-java-persistence"); + pageView.setIpAddress("127.0.0.1"); + + entityManager.persist(pageView); + }); + doInJPA(entityManager -> { + PageView pageView = new PageView(); + pageView.setSlug("/books/high-performance-java-persistence"); + pageView.setIpAddress("192.168.0.1"); + + entityManager.persist(pageView); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Tuple postViewCount = entityManager.createQuery( + "select p as post, count(pv) as page_views " + + "from Post p " + + "left join PageView pv on p.slug = pv.slug " + + "where p.title = :title " + + "group by p", Tuple.class) + .setParameter("title", "High-Performance Java Persistence") + .getSingleResult(); + + Post post = (Post) postViewCount.get("post"); + assertEquals("/books/high-performance-java-persistence", post.getSlug()); + + int pageViews = ((Number) postViewCount.get("page_views")).intValue(); + assertEquals(2, pageViews); + }); + + doInJPA(entityManager -> { + Tuple postViewCount = entityManager.createQuery( + "select p as post, count(pv) as page_views " + + "from Post p " + + "left join PageView pv on p.slug = pv.slug " + + "where p.title = :title " + + "group by p", Tuple.class) + .setParameter("title", "Presentations") + .getSingleResult(); + + Post post = (Post) postViewCount.get("post"); + assertEquals("/presentations", post.getSlug()); + + int pageViews = ((Number) postViewCount.get("page_views")).intValue(); + assertEquals(0, pageViews); + }); + + doInJPA(entityManager -> { + Tuple postViewCount = entityManager.createQuery( + "select p as post, count(pv) as page_views " + + "from Post p, PageView pv " + + "where p.title = :title and " + + " ( pv is null or p.slug = pv.slug ) " + + "group by p", Tuple.class) + .setParameter("title", "High-Performance Java Persistence") + .getSingleResult(); + + Post post = (Post) postViewCount.get("post"); + assertEquals("/books/high-performance-java-persistence", post.getSlug()); + + int pageViews = ((Number) postViewCount.get("page_views")).intValue(); + assertEquals(2, pageViews); + }); + + doInJPA(entityManager -> { + List postViewCount = entityManager.createQuery( + "select p as post, count(pv) as page_views " + + "from Post p, PageView pv " + + "where p.title = :title and " + + " ( p.slug = pv.slug ) " + + "group by p", Tuple.class) + .setParameter("title", "Presentations") + .getResultList(); + + assertEquals(0, postViewCount.size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String slug; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + } + + @Entity(name = "PageView") + @Table(name = "page_view") + public static class PageView { + + @Id + @GeneratedValue + private Long id; + + private String slug; + + @CreationTimestamp + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "ip_address") + private String ipAddress; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public Date getCreatedOn() { + return createdOn; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/ManyToManyFetchParentWithChildMatchAllFilteringCriteriaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/ManyToManyFetchParentWithChildMatchAllFilteringCriteriaTest.java new file mode 100644 index 000000000..6c1ac34a0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/ManyToManyFetchParentWithChildMatchAllFilteringCriteriaTest.java @@ -0,0 +1,375 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.util.*; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ManyToManyFetchParentWithChildMatchAllFilteringCriteriaTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Cluster.class, + Tag.class, + ClusterTag.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Cluster cluster1 = new Cluster(); + cluster1.id = 1L; + cluster1.name = "Cluster 1"; + + entityManager.persist(cluster1); + + Cluster cluster2 = new Cluster(); + cluster2.id = 2L; + cluster2.name = "Cluster 2"; + + entityManager.persist(cluster2); + + Cluster cluster3 = new Cluster(); + cluster3.id = 3L; + cluster3.name = "Cluster 3"; + + entityManager.persist(cluster3); + + Tag tag1 = new Tag(); + tag1.id = 1L; + tag1.name = "Spark"; + tag1.value = "2.2"; + + entityManager.persist(tag1); + + Tag tag2 = new Tag(); + tag2.id = 2L; + tag2.name = "Hadoop"; + tag2.value = "2.7"; + + entityManager.persist(tag2); + + Tag tag3 = new Tag(); + tag3.id = 3L; + tag3.name = "Spark"; + tag3.value = "2.3"; + + entityManager.persist(tag3); + + Tag tag4 = new Tag(); + tag4.id = 4L; + tag4.name = "Hadoop"; + tag4.value = "2.6"; + + entityManager.persist(tag4); + + cluster1.addTag(tag1); + cluster1.addTag(tag2); + + cluster2.addTag(tag1); + cluster2.addTag(tag4); + + cluster3.addTag(tag3); + cluster3.addTag(tag4); + }); + } + + @Test + @Ignore + public void testJPQLBroken() { + doInJPA(entityManager -> { + List clusters = entityManager.createQuery( + "select distinct c " + + "from ClusterTag ct " + + "join ct.cluster c " + + "join ct.tag t " + + "where " + + " (t.name = :tagName1 and t.value = :tagValue1) or " + + " (t.name = :tagName2 and t.value = :tagValue2) " + , Cluster.class) + .setParameter("tagName1", "Spark") + .setParameter("tagValue1", "2.2") + .setParameter("tagName2", "Hadoop") + .setParameter("tagValue2", "2.7") + .getResultList(); + + assertEquals(1, clusters.size()); + }); + } + + @Test + public void testNativeQueryJoin() { + doInJPA(entityManager -> { + List clusters = entityManager.createNativeQuery( + "SELECT * " + + "FROM cluster c " + + "JOIN (" + + " SELECT ct.cluster_id AS c_id " + + " FROM cluster_tag ct " + + " JOIN tag t ON ct.tag_id = t.id " + + " WHERE " + + " (t.tag_name = :tagName1 AND t.tag_value = :tagValue1) OR " + + " (t.tag_name = :tagName2 AND t.tag_value = :tagValue2) " + + " GROUP BY ct.cluster_id " + + " HAVING COUNT(*) = 2" + + ") ct1 on c.id = ct1.c_id ", Cluster.class) + .setParameter("tagName1", "Spark") + .setParameter("tagValue1", "2.2") + .setParameter("tagName2", "Hadoop") + .setParameter("tagValue2", "2.7") + .getResultList(); + + assertEquals(1, clusters.size()); + }); + } + + @Test + public void testNativeQueryExists() { + doInJPA(entityManager -> { + List clusters = entityManager.createNativeQuery( + "SELECT * " + + "FROM cluster c " + + "WHERE EXISTS (" + + " SELECT ct.cluster_id as c_id " + + " FROM cluster_tag ct " + + " JOIN tag t ON ct.tag_id = t.id " + + " WHERE " + + " c.id = ct.cluster_id AND ( " + + " (t.tag_name = :tagName1 AND t.tag_value = :tagValue1) OR " + + " (t.tag_name = :tagName2 AND t.tag_value = :tagValue2) " + + " )" + + " GROUP BY ct.cluster_id " + + " HAVING COUNT(*) = 2 " + + ") ", Cluster.class) + .setParameter("tagName1", "Spark") + .setParameter("tagValue1", "2.2") + .setParameter("tagName2", "Hadoop") + .setParameter("tagValue2", "2.7") + .getResultList(); + + assertEquals(1, clusters.size()); + }); + } + + @Test + public void testJPQLExists() { + doInJPA(entityManager -> { + List clusters = entityManager.createQuery( + "select c " + + "from Cluster c " + + "where exists (" + + " select ctc.id " + + " from ClusterTag ct " + + " join ct.cluster ctc " + + " join ct.tag ctt " + + " where " + + " c.id = ctc.id and ( " + + " (ctt.name = :tagName1 and ctt.value = :tagValue1) or " + + " (ctt.name = :tagName2 and ctt.value = :tagValue2) " + + " )" + + " group by ctc.id " + + " having count(*) = 2" + + ") ", Cluster.class) + .setParameter("tagName1", "Spark") + .setParameter("tagValue1", "2.2") + .setParameter("tagName2", "Hadoop") + .setParameter("tagValue2", "2.7") + .getResultList(); + + assertEquals(1, clusters.size()); + }); + } + + @Test + public void testJPQLExistsImplicitJoin() { + doInJPA(entityManager -> { + List clusters = entityManager.createQuery( + "select c " + + "from Cluster c " + + "where exists (" + + " select ct.cluster.id " + + " from ClusterTag ct " + + " join ct.tag ctt " + + " where " + + " c.id = ct.cluster.id and ( " + + " (ctt.name = :tagName1 and ctt.value = :tagValue1) or " + + " (ctt.name = :tagName2 and ctt.value = :tagValue2) " + + " )" + + " group by ct.cluster.id " + + " having count(*) = 2" + + ") ", Cluster.class) + .setParameter("tagName1", "Spark") + .setParameter("tagValue1", "2.2") + .setParameter("tagName2", "Hadoop") + .setParameter("tagValue2", "2.7") + .getResultList(); + + assertEquals(1, clusters.size()); + }); + } + + @Entity(name = "Cluster") + @Table(name = "cluster") + public static class Cluster { + + @Id + private Long id; + + private String name; + + @OneToMany( + mappedBy = "cluster", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public List getTags() { + return tags; + } + + public void addTag(Tag tag) { + tags.add(new ClusterTag(this, tag)); + } + } + + @Embeddable + public static class ClusterTagId implements Serializable { + + @Column(name = "cluster_id") + private Long clusterId; + + @Column(name = "tag_id") + private Long tagId; + + public ClusterTagId() {} + + public ClusterTagId(Long clusterId, Long tagId) { + this.clusterId = clusterId; + this.tagId = tagId; + } + + public Long getClusterId() { + return clusterId; + } + + public Long getTagId() { + return tagId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ClusterTagId that = (ClusterTagId) o; + return Objects.equals(clusterId, that.getClusterId()) && + Objects.equals(tagId, that.getTagId()); + } + + @Override + public int hashCode() { + return Objects.hash(clusterId, tagId); + } + } + + @Entity(name = "ClusterTag") + @Table(name = "cluster_tag") + public static class ClusterTag { + + @EmbeddedId + private ClusterTagId id; + + @ManyToOne + @MapsId("clusterId") + private Cluster cluster; + + @ManyToOne + @MapsId("tagId") + private Tag tag; + + private ClusterTag() {} + + public ClusterTag(Cluster cluster, Tag tag) { + this.cluster = cluster; + this.tag = tag; + this.id = new ClusterTagId(cluster.getId(), tag.getId()); + } + + public ClusterTagId getId() { + return id; + } + + public Cluster getCluster() { + return cluster; + } + + public void setCluster(Cluster cluster) { + this.cluster = cluster; + } + + public Tag getTag() { + return tag; + } + + public void setTag(Tag tag) { + this.tag = tag; + } + } + + @Entity(name = "Tag") + @Table( + name = "tag", + uniqueConstraints = @UniqueConstraint(columnNames = { + "tag_name", "tag_value" + }) + ) + public static class Tag { + + @Id + private Long id; + + @Column(name = "tag_name") + private String name; + + @Column(name = "tag_value") + private String value; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/MySQLExecutionPlanTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/MySQLExecutionPlanTest.java new file mode 100644 index 000000000..9494fd329 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/MySQLExecutionPlanTest.java @@ -0,0 +1,186 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class MySQLExecutionPlanTest extends AbstractTest { + + private static final int ENTITY_COUNT = 500; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + } + + @Test + public void testQueryExecutionPlan() { + doInJPA(entityManager -> { + for (int i = 0; i < ENTITY_COUNT; i++) { + Post post = new Post(String.format("Post no. %d", i)); + + post.addComment(new PostComment(String.format("Comment %d-1", i))); + if (Math.random() < 0.1) { + post.addComment(new PostComment("Bingo")); + } + + entityManager.persist(post); + } + }); + + /* + SELECT + p.id + FROM + post p + WHERE EXISTS ( + SELECT 1 + FROM + post_comment pc + WHERE + pc.post_id = p.id AND + pc.review = 'Bingo' + ) + ORDER BY + p.title + LIMIT 10 + */ + + String executionPlan = doInJPA(entityManager -> (String) entityManager.createNativeQuery( + "EXPLAIN FORMAT=JSON " + + "SELECT " + + " p.id " + + "FROM " + + " post p " + + "WHERE EXISTS ( " + + " SELECT 1 " + + " FROM " + + " post_comment pc " + + " WHERE " + + " pc.post_id = p.id AND " + + " pc.review = 'Bingo' " + + ") " + + "ORDER BY " + + " p.title " + + "LIMIT 10") + .getSingleResult()); + + LOGGER.info("Execution plan: \n{}", executionPlan); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + public Post() {} + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(foreignKey = @ForeignKey(name = "fk_post_id")) + private Post post; + + private String review; + + public PostComment() {} + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/NativeQueryEntityMappingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/NativeQueryEntityMappingTest.java new file mode 100644 index 000000000..43dc1dbf8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/NativeQueryEntityMappingTest.java @@ -0,0 +1,228 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class NativeQueryEntityMappingTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + Tag.class, + }; + } + + @Test + public void test() { + final Long postId = doInJPA(entityManager -> { + Post post1 = new Post("JPA with Hibernate"); + Post post2 = new Post("Native Hibernate"); + + Tag tag1 = new Tag("Java"); + Tag tag2 = new Tag("Hibernate"); + + post1.addTag(tag1); + post1.addTag(tag2); + + post2.addTag(tag1); + + entityManager.persist(post1); + entityManager.persist(post2); + + return post1.id; + }); + doInJPA(entityManager -> { + List tuples = entityManager.createNativeQuery(""" + SELECT + {p.*}, {t.*} + FROM post p + LEFT JOIN post_tag pt ON p.id = pt.post_id + LEFT JOIN tag t ON t.id = pt.tag_id + """) + .unwrap(NativeQuery.class) + .addEntity("p", Post.class) + .addEntity("t", Tag.class) + .getResultList(); + + assertEquals(3, tuples.size()); + }); + + doInJPA(entityManager -> { + List tuples = entityManager + .createNamedQuery("find_posts_with_tags") + .getResultList(); + + assertEquals(3, tuples.size()); + }); + } + + @NamedNativeQuery( + name = "find_posts_with_tags", + query = """ + SELECT + p.id as "p.id", + p.title as "p.title", + t.id as "t.id", + t.name as "t.name" + FROM post p + LEFT JOIN post_tag pt ON p.id = pt.post_id + LEFT JOIN tag t ON t.id = pt.tag_id + """, + resultSetMapping = "posts_with_tags" + ) + @SqlResultSetMapping( + name = "posts_with_tags", + entities = { + @EntityResult( + entityClass = Post.class, + fields = { + @FieldResult(name = "id", column = "p.id"), + @FieldResult(name = "title", column = "p.title"), + } + ), + @EntityResult( + entityClass = Tag.class, + fields = { + @FieldResult(name = "id", column = "t.id"), + @FieldResult(name = "name", column = "t.name"), + } + ) + } + ) + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + public Post() {} + + public Post(String title) { + this.title = title; + } + + @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private Set tags = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public void addTag(Tag tag) { + tags.add(tag); + tag.getPosts().add(this); + } + + public void removeTag(Tag tag) { + tags.remove(tag); + tag.getPosts().remove(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Post post = (Post) o; + return Objects.equals( title, post.title); + } + + @Override + public int hashCode() { + return Objects.hash(title); + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + private String name; + + @ManyToMany(mappedBy = "tags") + private Set posts = new HashSet<>(); + + public Tag() {} + + public Tag(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getPosts() { + return posts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tag tag = (Tag) o; + return Objects.equals(name, tag.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/NativeQueryWithCustomSchemaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/NativeQueryWithCustomSchemaTest.java similarity index 93% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/NativeQueryWithCustomSchemaTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/NativeQueryWithCustomSchemaTest.java index 0cc58f258..3f5943245 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/NativeQueryWithCustomSchemaTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/NativeQueryWithCustomSchemaTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.query; +package com.vladmihalcea.hpjp.hibernate.query; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.hibernate.cfg.AvailableSettings; +import org.junit.Ignore; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.sql.Timestamp; import java.time.LocalDateTime; import java.time.ZoneOffset; @@ -34,6 +34,7 @@ protected Properties properties() { } @Test + @Ignore public void test() { doInJPA(entityManager -> { Event firstPartRelease = new Event(); diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/OracleExecutionPlanNativeQueryTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/OracleExecutionPlanNativeQueryTest.java new file mode 100644 index 000000000..f8ba942f5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/OracleExecutionPlanNativeQueryTest.java @@ -0,0 +1,209 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class OracleExecutionPlanNativeQueryTest extends AbstractOracleIntegrationTest { + + private static final int ENTITY_COUNT = 500; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.use_sql_comments", "true"); + } + + @Test + public void testQueryExecutionPlan() { + doInJPA(entityManager -> { + for (int i = 0; i < ENTITY_COUNT; i++) { + Post post = new Post(String.format("Post no. %d", i)); + + post.addComment(new PostComment(String.format("Comment %d-1", i))); + if (Math.random() < 0.1) { + post.addComment(new PostComment("Bingo")); + } + + entityManager.persist(post); + } + }); + + int pageStart = 20; + int pageSize = 10; + + doInJPA(entityManager -> { + + List postIds = entityManager.createNativeQuery( + "SELECT " + + " p.id " + + "FROM " + + " post p " + + "WHERE EXISTS ( " + + " SELECT 1 " + + " FROM " + + " post_comment pc " + + " WHERE " + + " pc.post_id = p.id AND " + + " pc.review = 'Bingo' " + + ") " + + "ORDER BY " + + " p.title ") + .setFirstResult(pageStart) + .setMaxResults(pageSize) + .getResultList(); + + assertEquals(pageSize, postIds.size()); + }); + + doInJPA(entityManager -> { + + List summaries = entityManager.createNativeQuery( + "SELECT " + + " p.id " + + "FROM " + + " post p " + + "WHERE EXISTS ( " + + " SELECT 1 " + + " FROM " + + " post_comment pc " + + " WHERE " + + " pc.post_id = p.id AND " + + " pc.review = 'Bingo' " + + ") " + + "ORDER BY " + + " p.title ") + .setFirstResult(pageStart) + .setMaxResults(pageSize) + .unwrap(org.hibernate.query.Query.class) + .addQueryHint("GATHER_PLAN_STATISTICS") + .setComment("POST_WITH_BINGO_COMMENTS") + .getResultList(); + + List executionPlanLines = entityManager.createNativeQuery( + "SELECT p.* " + + "FROM v$sql s, TABLE ( " + + " dbms_xplan.display_cursor ( " + + " s.sql_id, s.child_number, 'ALLSTATS LAST' " + + " ) " + + ") p " + + "WHERE s.sql_text LIKE '%POST_WITH_BINGO_COMMENTS%'") + .getResultList(); + + LOGGER.info("Execution plan: \n{}", executionPlanLines.stream().collect(Collectors.joining("\n"))); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + private String title; + + public Post() {} + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(foreignKey = @ForeignKey(name = "fk_post_id")) + private Post post; + + private String review; + + public PostComment() {} + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/PostgreSQLCastTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/PostgreSQLCastTest.java new file mode 100644 index 000000000..6cf108761 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/PostgreSQLCastTest.java @@ -0,0 +1,168 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.time.temporal.TemporalAdjusters; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLCastTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post part1 = new Post(); + part1.setTitle("High-Performance Java Persistence, Part 1"); + part1.setCreatedOn( + LocalDateTime.now().with(TemporalAdjusters.previous(DayOfWeek.MONDAY)) + ); + entityManager.persist(part1); + + Post part2 = new Post(); + part2.setTitle("High-Performance Java Persistence, Part 2"); + part2.setCreatedOn( + LocalDateTime.now().with(TemporalAdjusters.previous(DayOfWeek.TUESDAY)) + ); + entityManager.persist(part2); + + Post part3 = new Post(); + part3.setTitle("High-Performance Java Persistence, Part 3"); + part3.setCreatedOn( + LocalDateTime.now().with(TemporalAdjusters.previous(DayOfWeek.THURSDAY)) + ); + entityManager.persist(part3); + }); + } + + @Test + @Ignore + public void testCastOperator() { + doInJPA(entityManager -> { + List posts = entityManager.createNativeQuery( + "SELECT * " + + "FROM post " + + "WHERE " + + " date_part('dow', created_on) = " + + " date_part('dow', :datetime::date)", Post.class) + .setParameter("datetime", Timestamp.valueOf( + LocalDateTime.now().with( + TemporalAdjusters.next(DayOfWeek.MONDAY))) + ) + .getResultList(); + }); + } + + @Test + public void testCastEscapeOperator() { + doInJPA(entityManager -> { + List posts = entityManager.createNativeQuery( + "SELECT * " + + "FROM post " + + "WHERE " + + " date_part('dow', created_on) = " + + " date_part('dow', :datetime\\:\\:date)", Post.class) + .setParameter("datetime", Timestamp.valueOf( + LocalDateTime.now().with( + TemporalAdjusters.next(DayOfWeek.MONDAY))) + ) + .getResultList(); + + assertEquals(1, posts.size()); + assertEquals("High-Performance Java Persistence, Part 1", posts.get(0).getTitle()); + }); + } + + @Test + public void testCastFunction() { + doInJPA(entityManager -> { + List posts = entityManager.createNativeQuery( + "SELECT * " + + "FROM post " + + "WHERE " + + " date_part('dow', created_on) = " + + " date_part('dow', cast(:datetime AS date))", Post.class) + .setParameter("datetime", Timestamp.valueOf( + LocalDateTime.now().with( + TemporalAdjusters.next(DayOfWeek.MONDAY))) + ) + .getResultList(); + + assertEquals(1, posts.size()); + assertEquals("High-Performance Java Persistence, Part 1", posts.get(0).getTitle()); + }); + } + + @Test + public void testTimestampFunction() { + doInJPA(entityManager -> { + List posts = entityManager.createNativeQuery( + "SELECT * " + + "FROM post " + + "WHERE " + + " date_part('dow', created_on) = " + + " date_part('dow', to_timestamp(:datetime, 'YYYY-MM-dd H24:MI:SS'))", Post.class) + .setParameter("datetime", Timestamp.valueOf( + LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY))).toString()) + .getResultList(); + + assertEquals(1, posts.size()); + assertEquals("High-Performance Java Persistence, Part 1", posts.get(0).getTitle()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/PostgreSQLExecutionPlanTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/PostgreSQLExecutionPlanTest.java new file mode 100644 index 000000000..0b49abe78 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/PostgreSQLExecutionPlanTest.java @@ -0,0 +1,190 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLExecutionPlanTest extends AbstractTest { + + private static final int ENTITY_COUNT = 500; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + } + + @Test + public void testQueryExecutionPlan() { + doInJPA(entityManager -> { + for (int i = 0; i < ENTITY_COUNT; i++) { + Post post = new Post(String.format("Post no. %d", i)); + + post.addComment(new PostComment(String.format("Comment %d-1", i))); + if (Math.random() < 0.1) { + post.addComment(new PostComment("Bingo")); + } + + entityManager.persist(post); + } + }); + + /* + SELECT + p.id + FROM + post p + WHERE EXISTS ( + SELECT 1 + FROM + post_comment pc + WHERE + pc.post_id = p.id AND + pc.review = 'Bingo' + ) + ORDER BY + p.title + LIMIT 10 + */ + + List executionPlanLines = doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + EXPLAIN ANALYZE + SELECT + p.id + FROM + post p + WHERE EXISTS ( + SELECT 1 + FROM + post_comment pc + WHERE + pc.post_id = p.id AND + pc.review = 'Bingo' + ) + ORDER BY + p.title + LIMIT 10 + """) + .getResultList(); + }); + + LOGGER.info("Execution plan: \n{}", String.join("\n", executionPlanLines)); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + private String title; + + public Post() {} + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(foreignKey = @ForeignKey(name = "fk_post_id")) + private Post post; + + private String review; + + public PostComment() {} + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/PostgreSQLNativeQueryNullParameterTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/PostgreSQLNativeQueryNullParameterTest.java new file mode 100644 index 000000000..8e659a6d7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/PostgreSQLNativeQueryNullParameterTest.java @@ -0,0 +1,86 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLNativeQueryNullParameterTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Event firstPartRelease = new Event(); + firstPartRelease.setName("High-Performance Java Persistence Part I"); + firstPartRelease.setCreatedOn( + Timestamp.from(LocalDateTime.of(2015, 11, 2, 9, 0, 0).toInstant(ZoneOffset.UTC)) + ); + entityManager.persist(firstPartRelease); + }); + + //Explicit cast needed as a workaround for HHH-13155 + doInJPA(entityManager -> { + List events = entityManager + .createNativeQuery( + "SELECT * " + + "FROM Event " + + "WHERE (:name is null or name = :name)", Event.class) + .unwrap(NativeQuery.class) + .setParameter("name", null, String.class) + .getResultList(); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + @GeneratedValue + private long id; + + private String name; + + @Column(name = "created_on") + private Timestamp createdOn; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Timestamp getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Timestamp createdOn) { + this.createdOn = createdOn; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/PostgreSQLUpdateNullTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/PostgreSQLUpdateNullTest.java new file mode 100644 index 000000000..c226f229f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/PostgreSQLUpdateNullTest.java @@ -0,0 +1,82 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLUpdateNullTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.id = 1L; + post.externalId = 123L; + post.title = "High-Performance Java Persistence"; + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + int count = entityManager.createQuery( + "update Post p " + + "set p.externalId = :externalId, p.title = :title " + + "where p.id = :id") + .setParameter("externalId", null) + .setParameter("title", null) + .setParameter("id", 1L) + .executeUpdate(); + assertEquals(1, count); + + Post post = entityManager.find(Post.class, 1L); + assertNull(post.externalId); + assertNull(post.title); + }); + + doInJPA(entityManager -> { + int count = entityManager.createNativeQuery( + "UPDATE post " + + "SET externalId = :externalId, title = :title " + + "WHERE id = :id") + .unwrap(org.hibernate.query.NativeQuery.class) + .setParameter("externalId", null, Long.class) + .setParameter("title", null, String.class) + .setParameter("id", 1L) + .executeUpdate(); + assertEquals(1, count); + + Post post = entityManager.find(Post.class, 1L); + assertNull(post.externalId); + assertNull(post.title); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private Long externalId; + + @Column(columnDefinition = "text") + private String title; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/RowValueExpressionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/RowValueExpressionTest.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/RowValueExpressionTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/RowValueExpressionTest.java index 604ba86b4..000dc358f 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/RowValueExpressionTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/RowValueExpressionTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.query; +package com.vladmihalcea.hpjp.hibernate.query; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.Session; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.Id; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; import java.util.List; /** diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/RunningTotalTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/RunningTotalTest.java similarity index 98% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/RunningTotalTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/RunningTotalTest.java index 3a11515dc..dc1e98da3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/RunningTotalTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/RunningTotalTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.query; +package com.vladmihalcea.hpjp.hibernate.query; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.time.LocalDate; import java.time.ZoneOffset; import java.util.Date; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/SQLInjectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/SQLInjectionTest.java new file mode 100644 index 000000000..4ecb43057 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/SQLInjectionTest.java @@ -0,0 +1,289 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.Session; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.*; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class SQLInjectionTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + PostComment comment1 = new PostComment(); + comment1.setId(1L); + comment1.setReview("Good"); + + PostComment comment2 = new PostComment(); + comment2.setId(2L); + comment2.setReview("Excellent"); + + post.addComment(comment1); + post.addComment(comment2); + + entityManager.persist(post); + }); + } + + @Test + @Ignore + public void testStatementUpdateDropTable() { + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + assertEquals("Good", comment.getReview()); + }); + + updatePostCommentReviewUsingStatement(1L, "Awesome"); + + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + assertEquals("Awesome", comment.getReview()); + }); + + try { + updatePostCommentReviewUsingStatement(1L, "'; DROP TABLE post_comment; -- '"); + } catch (Exception e) { + LOGGER.error("Failure", e); + } + + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + assertNotNull(comment); + }); + } + + @Test + @Ignore + public void testPreparedStatementUpdateDropTable() { + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + assertEquals("Good", comment.getReview()); + }); + + updatePostCommentReviewUsingPreparedStatement(1L, "Awesome"); + + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + assertEquals("Awesome", comment.getReview()); + }); + + try { + updatePostCommentReviewUsingPreparedStatement(1L, "'; DROP TABLE post_comment; -- '"); + } catch (Exception e) { + LOGGER.error("Failure", e); + } + + doInJPA(entityManager -> { + PostComment comment = entityManager.find(PostComment.class, 1L); + assertNotNull(comment); + }); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.LOG_SLOW_QUERY, "1"); + } + + @Test + public void testPreparedStatementSelectAndWait() { + assertEquals("Good", getPostCommentReviewUsingPreparedStatement("1")); + + getPostCommentReviewUsingPreparedStatement("1 AND 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(2) )"); + + assertEquals("Good", getPostCommentReviewUsingPreparedStatement("1")); + } + + @Test + public void testJPQLSelectAndWait() { + doInJPA(entityManager -> { + List posts = getPostsByTitle( + "High-Performance Java Persistence' and " + + "(SELECT pg_sleep(2)||'') = '" + ); + assertEquals(1, posts.size()); + }); + } + + private void updatePostCommentReviewUsingStatement(Long id, String review) { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + session.doWork(connection -> { + try(Statement statement = connection.createStatement()) { + statement.executeUpdate( + "UPDATE post_comment " + + "SET review = '" + review + "' " + + "WHERE id = " + id + ); + } + }); + }); + } + + private void updatePostCommentReviewUsingPreparedStatement(Long id, String review) { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + session.doWork(connection -> { + try(PreparedStatement statement = connection.prepareStatement( + "UPDATE post_comment " + + "SET review = '" + review + "' " + + "WHERE id = " + id + )) { + statement.executeUpdate(); + } + }); + }); + } + + private String getPostCommentReviewUsingPreparedStatement(String id) { + return doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + return session.doReturningWork(connection -> { + try(PreparedStatement statement = connection.prepareStatement( + "SELECT review " + + "FROM post_comment " + + "WHERE id = " + id + )) { + try(ResultSet resultSet = statement.executeQuery()) { + return resultSet.next() ? resultSet.getString(1) : null; + } + } + }); + }); + } + + private List getPostsByTitle(String title) { + return doInJPA(entityManager -> { + return entityManager.createQuery( + "select p " + + "from Post p " + + "where" + + " p.title = '" + title + "'", Post.class) + .getResultList(); + }); + } + + private List getPostsByTitle(Class entityClass, String title) { + return doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = builder.createQuery(entityClass); + Root root = query.from(entityClass); + query.where( + builder.equal( + root.get("title"), + title + ) + ); + + return entityManager + .createQuery(query) + .getResultList(); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/UpdateSubQueryTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/UpdateSubQueryTest.java similarity index 95% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/UpdateSubQueryTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/UpdateSubQueryTest.java index 475278de8..781ef0769 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/UpdateSubQueryTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/UpdateSubQueryTest.java @@ -1,10 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.query; +package com.vladmihalcea.hpjp.hibernate.query; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; import org.hibernate.Session; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.HashSet; import java.util.Set; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/WindowFunctionGroupingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/WindowFunctionGroupingTest.java similarity index 76% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/WindowFunctionGroupingTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/WindowFunctionGroupingTest.java index 6244c4b1a..7087e20b7 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/WindowFunctionGroupingTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/WindowFunctionGroupingTest.java @@ -1,11 +1,12 @@ -package com.vladmihalcea.book.hpjp.hibernate.query; +package com.vladmihalcea.hpjp.hibernate.query; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.sql.Timestamp; -import java.time.LocalDateTime; +import java.time.LocalDate; +import java.time.ZoneId; import java.util.Date; import java.util.List; @@ -30,37 +31,37 @@ public void test() { event1.setCategory("Living room"); event1.setKpi("Temperature"); event1.setValue(21.5d); - event1.setCreatedOn(Timestamp.valueOf(LocalDateTime.now().minusDays(1))); + event1.setCreatedOn(Timestamp.valueOf(LocalDate.now().minusDays(1).atStartOfDay(ZoneId.systemDefault()).toLocalDateTime())); DataEvent event2 = new DataEvent(); event2.setCategory("Living room"); event2.setKpi("Temperature"); event2.setValue(22.5d); - event2.setCreatedOn(Timestamp.valueOf(LocalDateTime.now().minusDays(2))); + event2.setCreatedOn(Timestamp.valueOf(LocalDate.now().minusDays(2).atStartOfDay(ZoneId.systemDefault()).toLocalDateTime())); DataEvent event3 = new DataEvent(); event3.setCategory("Living room"); event3.setKpi("Temperature"); event3.setValue(20.5d); - event3.setCreatedOn(Timestamp.valueOf(LocalDateTime.now().minusDays(2))); + event3.setCreatedOn(Timestamp.valueOf(LocalDate.now().minusDays(2).atStartOfDay(ZoneId.systemDefault()).toLocalDateTime())); DataEvent event4 = new DataEvent(); event4.setCategory("Bedroom"); event4.setKpi("Temperature"); event4.setValue(23.5d); - event4.setCreatedOn(Timestamp.valueOf(LocalDateTime.now().minusDays(1))); + event4.setCreatedOn(Timestamp.valueOf(LocalDate.now().minusDays(1).atStartOfDay(ZoneId.systemDefault()).toLocalDateTime())); DataEvent event5 = new DataEvent(); event5.setCategory("Bedroom"); event5.setKpi("Pressure"); event5.setValue(750.5d); - event5.setCreatedOn(Timestamp.valueOf(LocalDateTime.now().minusDays(1))); + event5.setCreatedOn(Timestamp.valueOf(LocalDate.now().minusDays(1).atStartOfDay(ZoneId.systemDefault()).toLocalDateTime())); DataEvent event6 = new DataEvent(); event6.setCategory("Living room"); event6.setKpi("Temperature"); event6.setValue(22.5d); - event6.setCreatedOn(Timestamp.valueOf(LocalDateTime.now().minusDays(1))); + event6.setCreatedOn(Timestamp.valueOf(LocalDate.now().minusDays(1).atStartOfDay(ZoneId.systemDefault()).toLocalDateTime())); entityManager.persist(event1); entityManager.persist(event2); @@ -83,7 +84,7 @@ public void test() { ") de3 " + "where de3.createdon = max_createdon") .getResultList(); - assertEquals(2, values.size()); + assertEquals(3, values.size()); }); } @@ -98,7 +99,7 @@ public static class DataEvent { private String kpi; - @Temporal(TemporalType.TIMESTAMP) + @Temporal(TemporalType.DATE) private Date createdOn; private Double value; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/WindowFunctionPercentilesTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/WindowFunctionPercentilesTest.java new file mode 100644 index 000000000..a11de363b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/WindowFunctionPercentilesTest.java @@ -0,0 +1,173 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.junit.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Locale; +import java.util.Properties; + +import static org.hibernate.cfg.SchemaToolingSettings.JAKARTA_HBM2DDL_LOAD_SCRIPT_SOURCE; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class WindowFunctionPercentilesTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Quote.class, + }; + } + + @Override + protected Database database() { + return Database.SQLSERVER; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + JAKARTA_HBM2DDL_LOAD_SCRIPT_SOURCE, + database() == Database.ORACLE ? "data/oracle_quotes.sql" : "data/quotes.sql" + ); + } + + @Test + public void test() { + if(!(database() == Database.ORACLE || database() == Database.POSTGRESQL)) { + return; + } + doInJPA(entityManager -> { + List prices = entityManager.createNativeQuery(""" + SELECT + PERCENTILE_CONT(0.50) WITHIN GROUP(ORDER BY close_price) AS median, + PERCENTILE_CONT(0.75) WITHIN GROUP(ORDER BY close_price) AS p75, + PERCENTILE_CONT(0.95) WITHIN GROUP(ORDER BY close_price) AS p95, + PERCENTILE_CONT(0.99) WITHIN GROUP(ORDER BY close_price) AS p99 + FROM quotes + WHERE + ticker = 'SPX' AND + quote_date BETWEEN DATE '2019-01-01' AND DATE '2023-12-31' + """, Tuple.class) + .getResultList(); + + assertEquals(1, prices.size()); + }); + + doInJPA(entityManager -> { + List prices = entityManager.createNativeQuery(""" + SELECT + extract(year from quote_date) AS year, + PERCENTILE_CONT(0.50) WITHIN GROUP(ORDER BY close_price) AS median, + PERCENTILE_CONT(0.75) WITHIN GROUP(ORDER BY close_price) AS p75, + PERCENTILE_CONT(0.95) WITHIN GROUP(ORDER BY close_price) AS p95, + PERCENTILE_CONT(0.99) WITHIN GROUP(ORDER BY close_price) AS p99 + FROM quotes + WHERE + ticker = 'SPX' AND + quote_date BETWEEN DATE '2019-01-01' AND DATE '2023-12-31' + GROUP BY extract(year from quote_date) + ORDER BY year + """, Tuple.class) + .getResultList(); + + assertEquals(5, prices.size()); + }); + } + + @Test + public void testSQLServer() { + if(!(database() == Database.SQLSERVER)) { + return; + } + doInJPA(entityManager -> { + List prices = entityManager.createNativeQuery(""" + SELECT DISTINCT + PERCENTILE_CONT(0.50) WITHIN GROUP(ORDER BY close_price) + OVER (PARTITION BY ticker) AS median, + PERCENTILE_CONT(0.75) WITHIN GROUP(ORDER BY close_price) + OVER (PARTITION BY ticker) AS p75, + PERCENTILE_CONT(0.95) WITHIN GROUP(ORDER BY close_price) + OVER (PARTITION BY ticker) AS p95, + PERCENTILE_CONT(0.99) WITHIN GROUP(ORDER BY close_price) + OVER (PARTITION BY ticker) AS p99 + FROM quotes + WHERE + ticker = 'SPX' AND + quote_date BETWEEN '2019-01-01' AND '2023-12-31' + """, Tuple.class) + .getResultList(); + + assertEquals(1, prices.size()); + }); + + doInJPA(entityManager -> { + List prices = entityManager.createNativeQuery(""" + SELECT DISTINCT + YEAR(quote_date) AS year, + PERCENTILE_CONT(0.50) WITHIN GROUP(ORDER BY close_price) + OVER (PARTITION BY YEAR(quote_date)) AS median, + PERCENTILE_CONT(0.75) WITHIN GROUP(ORDER BY close_price) + OVER (PARTITION BY YEAR(quote_date)) AS p75, + PERCENTILE_CONT(0.95) WITHIN GROUP(ORDER BY close_price) + OVER (PARTITION BY YEAR(quote_date)) AS p95, + PERCENTILE_CONT(0.99) WITHIN GROUP(ORDER BY close_price) + OVER (PARTITION BY YEAR(quote_date)) AS p99 + FROM quotes + WHERE + ticker = 'SPX' AND + quote_date BETWEEN '2019-01-01' AND '2023-12-31' + ORDER BY year + """, Tuple.class) + .getResultList(); + + assertEquals(5, prices.size()); + }); + } + + @Entity(name = "quotes") + public static class Quote { + + @Id + @Column(length = 4) + private String ticker; + + @Id + @Column(name = "quote_date") + private LocalDate date; + + @Column(name = "close_price", precision = 12, scale = 4) + private BigDecimal price; + + public String getTicker() { + return ticker; + } + + public void setTicker(String ticker) { + this.ticker = ticker; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/WindowFunctionUpdateByGroupingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/WindowFunctionUpdateByGroupingTest.java new file mode 100644 index 000000000..a956626a8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/WindowFunctionUpdateByGroupingTest.java @@ -0,0 +1,181 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import java.sql.Statement; +import java.util.List; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import org.hibernate.Session; + +import org.junit.Test; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class WindowFunctionUpdateByGroupingTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Entry.class, + }; + } + + @Override + public void init() { + super.init(); + + doInJPA(entityManager -> { + Session session = entityManager.unwrap( Session.class ); + + session.doWork( connection -> { + try(Statement statement = connection.createStatement() ) { + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2000, 'a', 1, 'x', 0)" ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2000, 'a', 1, 'y', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2000, 'a', 1, 'z', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2000, 'a', 2, 'z', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2000, 'a', 2, 'x', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2000, 'b', 1, 'x', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2000, 'b', 1, 'y', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2000, 'b', 1, 'z', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2000, 'b', 2, 'z', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2001, 'a', 1, 'x', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2001, 'a', 1, 'y', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2001, 'a', 1, 'z', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2001, 'a', 2, 'z', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2001, 'a', 2, 'x', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2001, 'a', 2, 'y', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2001, 'a', 2, 'w', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2001, 'a', 3, 'y', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2001, 'a', 3, 'w', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2001, 'b', 1, 'x', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2001, 'b', 1, 'y', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2001, 'b', 2, 'x', 0) " ); + statement.executeUpdate( "INSERT INTO entries (c1, c2, c3, c4, c5) VALUES (2001, 'b', 2, 'z', 0) " ); + } + } ); + }); + } + + @Test + public void testWindowFunction() { + doInJPA(entityManager -> { + List values = entityManager.createQuery( "select e from Entry e" ).getResultList(); + assertEquals(22, values.size()); + + int updateCount = entityManager.createNativeQuery( + "update entries set c5 = 1 " + + "where id in " + + "( " + + " select id " + + " from ( " + + " select *, MAX (c3) OVER (PARTITION BY c1, c2) as max_c3 " + + " from entries " + + " ) t " + + " where t.c3 = t.max_c3 " + + ") ") + .executeUpdate(); + + assertEquals( 7, updateCount ); + }); + } + + @Test + public void testGroupBy() { + doInJPA(entityManager -> { + List values = entityManager.createQuery( "select e from Entry e" ).getResultList(); + assertEquals(22, values.size()); + + int updateCount = entityManager.createNativeQuery( + "update entries set c5 = 1 " + + "where id in " + + "( " + + " select e.id " + + " from entries e " + + " inner join ( " + + " select c1, c2, max(c3) as max_c3 " + + " from entries " + + " group by c1, c2 " + + " ) t " + + " on e.c1 = t.c1 and e.c2 = t.c2 and e.c3 = t.max_c3 " + + ") " ) + .executeUpdate(); + + assertEquals( 7, updateCount ); + }); + } + + @Entity(name = "Entry") + @Table(name = "entries") + public static class Entry { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Integer c1; + + private String c2; + + private Integer c3; + + private String c4; + + private Integer c5; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Integer getC1() { + return c1; + } + + public void setC1(Integer c1) { + this.c1 = c1; + } + + public String getC2() { + return c2; + } + + public void setC2(String c2) { + this.c2 = c2; + } + + public Integer getC3() { + return c3; + } + + public void setC3(Integer c3) { + this.c3 = c3; + } + + public String getC4() { + return c4; + } + + public void setC4(String c4) { + this.c4 = c4; + } + + public Integer getC5() { + return c5; + } + + public void setC5(Integer c5) { + this.c5 = c5; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/YugabyteDBTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/YugabyteDBTest.java new file mode 100644 index 000000000..9f46d5370 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/YugabyteDBTest.java @@ -0,0 +1,127 @@ +package com.vladmihalcea.hpjp.hibernate.query; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class YugabyteDBTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.YUGABYTEDB; + } + + private final LocalDate today = LocalDate.now(); + + @Override + public void init() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + super.init(); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence, Part 1") + .setCreatedOn(today.minusDays(2).atStartOfDay()) + ); + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence, Part 2") + .setCreatedOn(today.minusDays(1).atStartOfDay()) + ); + + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence, Part 3") + .setCreatedOn(today.atStartOfDay()) + ); + }); + } + + @Test + public void testTimestampRange() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJPA(entityManager -> { + List posts = entityManager.createNativeQuery(""" + SELECT * + FROM post + WHERE + created_on >= :startTimestamp and + created_on < :endTimestamp + """, Post.class) + .setParameter("startTimestamp", today.minusDays(2)) + .setParameter("endTimestamp", today) + .getResultList(); + + assertEquals(2, posts.size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/any/PostgreSQLAnyClauseTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/any/PostgreSQLAnyClauseTest.java new file mode 100644 index 000000000..2061e92ba --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/any/PostgreSQLAnyClauseTest.java @@ -0,0 +1,115 @@ +package com.vladmihalcea.hpjp.hibernate.query.any; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.time.temporal.TemporalAdjusters; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLAnyClauseTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post part1 = new Post(); + part1.setTitle("High-Performance Java Persistence, Part 1"); + part1.setCreatedOn( + LocalDateTime.now().with(TemporalAdjusters.previous(DayOfWeek.MONDAY)) + ); + entityManager.persist(part1); + + Post part2 = new Post(); + part2.setTitle("High-Performance Java Persistence, Part 2"); + part2.setCreatedOn( + LocalDateTime.now().with(TemporalAdjusters.previous(DayOfWeek.TUESDAY)) + ); + entityManager.persist(part2); + + Post part3 = new Post(); + part3.setTitle("High-Performance Java Persistence, Part 3"); + part3.setCreatedOn( + LocalDateTime.now().with(TemporalAdjusters.previous(DayOfWeek.THURSDAY)) + ); + entityManager.persist(part3); + }); + } + + @Test + public void testCastOperator() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(PreparedStatement ps = connection.prepareStatement( + "SELECT id, title " + + "FROM post " + + "WHERE " + + " id = ANY(?)" + )) { + ps.setArray(1, connection.createArrayOf("BIGINT", new Long[]{1L, 2L, 3L})); + try(ResultSet resultSet = ps.executeQuery()) { + while (resultSet.next()) { + Long id = resultSet.getLong(1); + String title = resultSet.getString(1); + } + } + } + }); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/cte/DerivedTableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/cte/DerivedTableTest.java new file mode 100644 index 000000000..fa4852d3f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/cte/DerivedTableTest.java @@ -0,0 +1,364 @@ +package com.vladmihalcea.hpjp.hibernate.query.cte; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import io.hypersistence.utils.hibernate.query.SQLExtractor; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DerivedTableTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + /** + * post + * ---- + * + * | id | title | + * |----|----------| + * | 1 | SQL:2016 | + * | 2 | SQL:2011 | + * | 3 | SQL:2008 | + * | 4 | JPA 3.0 | + * | 5 | JPA 2.2 | + * | 6 | JPA 2.1 | + * | 7 | JPA 2.0 | + * | 8 | JPA 1.0 | + * + * post_comment + * ------------- + * + * | id | review | post_id | + * |----|------------------------|---------| + * | 1 | SQL:2016 is great! | 1 | + * | 2 | SQL:2016 is excellent! | 1 | + * | 3 | SQL:2016 is awesome! | 1 | + * | 4 | SQL:2011 is great! | 2 | + * | 5 | SQL:2011 is excellent! | 2 | + * | 6 | SQL:2008 is great! | 3 | + */ + @Override + public void afterInit() { + doInJPA(entityManager -> { + + short[] latestSQLStandards = { + 2016, + 2011, + 2008 + }; + + String[] comments = { + "great", + "excellent", + "awesome" + }; + + int post_id = 1; + int comment_id = 1; + + for (int i = 0; i < latestSQLStandards.length; i++) { + short sqlStandard = latestSQLStandards[i]; + Post post = new Post() + .setId(post_id++) + .setTitle(String.format("SQL:%d", sqlStandard)); + + entityManager.persist(post); + + for (int j = 0; j < comments.length - i; j++) { + entityManager.persist( + new PostComment() + .setId(comment_id++) + .setReview(String.format("SQL:%d is %s!", sqlStandard, comments[j])) + .setPost(post) + ); + } + } + + entityManager.persist( + new Post() + .setId(post_id++) + .setTitle("JPA 3.0") + ); + entityManager.persist( + new Post() + .setId(post_id++) + .setTitle("JPA 2.2") + ); + entityManager.persist( + new Post() + .setId(post_id++) + .setTitle("JPA 2.1") + ); + entityManager.persist( + new Post() + .setId(post_id++) + .setTitle("JPA 2.0") + ); + entityManager.persist( + new Post() + .setId(post_id++) + .setTitle("JPA 1.0") + ); + }); + } + + /** + * Get the first two most-commented posts along with all their comments. + * + * SELECT * + * FROM ( + * SELECT + * post_id, + * post_title, + * comment_id, + * comment_review, + * DENSE_RANK() OVER (ORDER BY p_pc.comment_count DESC) AS ranking + * FROM ( + * SELECT + * p.id AS post_id, + * p.title AS post_title, + * pc.id AS comment_id, + * pc.review AS comment_review, + * COUNT(post_id) OVER(PARTITION BY post_id) AS comment_count + * FROM post p + * LEFT JOIN post_comment pc ON p.id = pc.post_id + * WHERE p.title LIKE 'SQL%' + * ) p_pc + * ) p_pc_r + * WHERE p_pc_r.ranking <= 2 + * ORDER BY post_id, comment_id + * + * | post_id | post_title | comment_id | comment_review | ranking | + * |---------|------------|------------|------------------------|---------| + * | 1 | SQL:2016 | 1 | SQL:2016 is great! | 1 | + * | 1 | SQL:2016 | 2 | SQL:2016 is excellent! | 1 | + * | 1 | SQL:2016 | 3 | SQL:2016 is awesome! | 1 | + * | 2 | SQL:2011 | 4 | SQL:2011 is great! | 2 | + * | 2 | SQL:2011 | 5 | SQL:2011 is excellent! | 2 | + */ + @Test + public void testDerivedTableSQL() { + List tuples = doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + SELECT * + FROM ( + SELECT + post_id, + post_title, + comment_id, + comment_review, + DENSE_RANK() OVER (ORDER BY p_pc.comment_count DESC) AS ranking + FROM ( + SELECT + p.id AS post_id, + p.title AS post_title, + pc.id AS comment_id, + pc.review AS comment_review, + COUNT(post_id) OVER(PARTITION BY post_id) AS comment_count + FROM post p + LEFT JOIN post_comment pc ON p.id = pc.post_id + WHERE p.title LIKE :title + ) p_pc + ) p_pc_r + WHERE p_pc_r.ranking <= :ranking + ORDER BY post_id, comment_id + """, Tuple.class) + .setParameter("title", "SQL%") + .setParameter("ranking", 2) + .getResultList(); + }); + + assertEquals(5, tuples.size()); + + assertEquals(1, ((Number) tuples.get(0).get("post_id")).intValue()); + assertEquals("SQL:2016", tuples.get(0).get("post_title")); + assertEquals(1, ((Number) tuples.get(0).get("comment_id")).intValue()); + assertEquals("SQL:2016 is great!", tuples.get(0).get("comment_review")); + + assertEquals(2, ((Number) tuples.get(3).get("post_id")).intValue()); + assertEquals("SQL:2011", tuples.get(3).get("post_title")); + assertEquals(4, ((Number) tuples.get(3).get("comment_id")).intValue()); + assertEquals("SQL:2011 is great!", tuples.get(3).get("comment_review")); + + assertEquals(2, ((Number) tuples.get(4).get("post_id")).intValue()); + assertEquals("SQL:2011", tuples.get(4).get("post_title")); + assertEquals(5, ((Number) tuples.get(4).get("comment_id")).intValue()); + assertEquals("SQL:2011 is excellent!", tuples.get(4).get("comment_review")); + } + + @Test + public void testJPQLDerivedTable() { + List tuples = doInJPA(entityManager -> { + TypedQuery query = entityManager.createQuery(""" + SELECT + post_id AS post_id, + post_title AS post_title, + comment_id AS comment_id, + comment_review AS comment_review, + DENSE_RANK() OVER (ORDER BY p_pc.comment_count DESC) AS ranking + FROM ( + SELECT + p.id AS post_id, + p.title AS post_title, + pc.id AS comment_id, + pc.review AS comment_review, + COUNT(p.id) OVER(PARTITION BY p.id) AS comment_count + FROM PostComment pc + JOIN pc.post p + WHERE p.title LIKE :title + ) p_pc + ORDER BY post_id, comment_id + """, Tuple.class); + + String sqlQuery = SQLExtractor.from(query); + + LOGGER.info("The associated SQL query is [{}]", sqlQuery); + + return query + .setParameter("title", "SQL%") + .getResultList(); + }); + + assertEquals(6, tuples.size()); + } + + @Test + public void testJPQLDerivedTableMultipleLevels() { + List tuples = doInJPA(entityManager -> { + return entityManager.createQuery(""" + SELECT + post_id AS post_id, + post_title AS post_title, + comment_id AS comment_id, + comment_review AS comment_review + FROM ( + SELECT + post_id AS post_id, + post_title AS post_title, + comment_id AS comment_id, + comment_review AS comment_review, + DENSE_RANK() OVER (ORDER BY p_pc.comment_count DESC) AS ranking + FROM ( + SELECT + p.id AS post_id, + p.title AS post_title, + pc.id AS comment_id, + pc.review AS comment_review, + COUNT(p.id) OVER(PARTITION BY p.id) AS comment_count + FROM PostComment pc + JOIN pc.post p + WHERE p.title LIKE :title + ) p_pc + ) p_pc_r + WHERE ranking <= :ranking + ORDER BY post_id, comment_id + """, Tuple.class) + .setParameter("title", "SQL%") + .setParameter("ranking", 2) + .getResultList(); + }); + + assertEquals(5, tuples.size()); + + assertEquals(1, ((Number) tuples.get(0).get("post_id")).intValue()); + assertEquals("SQL:2016", tuples.get(0).get("post_title")); + assertEquals(1, ((Number) tuples.get(0).get("comment_id")).intValue()); + assertEquals("SQL:2016 is great!", tuples.get(0).get("comment_review")); + + assertEquals(2, ((Number) tuples.get(3).get("post_id")).intValue()); + assertEquals("SQL:2011", tuples.get(3).get("post_title")); + assertEquals(4, ((Number) tuples.get(3).get("comment_id")).intValue()); + assertEquals("SQL:2011 is great!", tuples.get(3).get("comment_review")); + + assertEquals(2, ((Number) tuples.get(4).get("post_id")).intValue()); + assertEquals("SQL:2011", tuples.get(4).get("post_title")); + assertEquals(5, ((Number) tuples.get(4).get("comment_id")).intValue()); + assertEquals("SQL:2011 is excellent!", tuples.get(4).get("comment_review")); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Integer id; + + private String title; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Integer getId() { + return id; + } + + public PostComment setId(Integer id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/cte/WithCTETest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/cte/WithCTETest.java new file mode 100644 index 000000000..b3003594b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/cte/WithCTETest.java @@ -0,0 +1,333 @@ +package com.vladmihalcea.hpjp.hibernate.query.cte; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class WithCTETest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + /** + * post + * ---- + * + * | id | title | + * |----|----------| + * | 1 | SQL:2016 | + * | 2 | SQL:2011 | + * | 3 | SQL:2008 | + * | 4 | JPA 3.0 | + * | 5 | JPA 2.2 | + * | 6 | JPA 2.1 | + * | 7 | JPA 2.0 | + * | 8 | JPA 1.0 | + * + * post_comment + * ------------- + * + * | id | review | post_id | + * |----|------------------------|---------| + * | 1 | SQL:2016 is great! | 1 | + * | 2 | SQL:2016 is excellent! | 1 | + * | 3 | SQL:2016 is awesome! | 1 | + * | 4 | SQL:2011 is great! | 2 | + * | 5 | SQL:2011 is excellent! | 2 | + * | 6 | SQL:2008 is great! | 3 | + */ + @Override + public void afterInit() { + doInJPA(entityManager -> { + + short[] latestSQLStandards = { + 2016, + 2011, + 2008 + }; + + String[] comments = { + "great", + "excellent", + "awesome" + }; + + int post_id = 1; + int comment_id = 1; + + for (int i = 0; i < latestSQLStandards.length; i++) { + short sqlStandard = latestSQLStandards[i]; + Post post = new Post() + .setId(post_id++) + .setTitle(String.format("SQL:%d", sqlStandard)); + + entityManager.persist(post); + + for (int j = 0; j < comments.length - i; j++) { + entityManager.persist( + new PostComment() + .setId(comment_id++) + .setReview(String.format("SQL:%d is %s!", sqlStandard, comments[j])) + .setPost(post) + ); + } + } + + entityManager.persist( + new Post() + .setId(post_id++) + .setTitle("JPA 3.0") + ); + entityManager.persist( + new Post() + .setId(post_id++) + .setTitle("JPA 2.2") + ); + entityManager.persist( + new Post() + .setId(post_id++) + .setTitle("JPA 2.1") + ); + entityManager.persist( + new Post() + .setId(post_id++) + .setTitle("JPA 2.0") + ); + entityManager.persist( + new Post() + .setId(post_id++) + .setTitle("JPA 1.0") + ); + }); + } + + /** + * Get the first two most-commented posts along with all their comments. + * + * WITH + * p_pc AS ( + * SELECT + * p.id AS post_id, + * p.title AS post_title, + * pc.id AS comment_id, + * pc.review AS comment_review, + * COUNT(post_id) OVER(PARTITION BY post_id) AS comment_count + * FROM post p + * LEFT JOIN post_comment pc ON p.id = pc.post_id + * WHERE p.title LIKE 'SQL%' + * ), + * p_pc_r AS ( + * SELECT + * post_id, + * post_title, + * comment_id, + * comment_review, + * DENSE_RANK() OVER (ORDER BY p_pc.comment_count DESC) AS ranking + * FROM p_pc + * ) + * SELECT * + * FROM p_pc_r + * WHERE p_pc_r.ranking <= 2 + * ORDER BY post_id, comment_id + * + * | post_id | post_title | comment_id | comment_review | ranking | + * |---------|------------|------------|------------------------|---------| + * | 1 | SQL:2016 | 1 | SQL:2016 is great! | 1 | + * | 1 | SQL:2016 | 2 | SQL:2016 is excellent! | 1 | + * | 1 | SQL:2016 | 3 | SQL:2016 is awesome! | 1 | + * | 2 | SQL:2011 | 4 | SQL:2011 is great! | 2 | + * | 2 | SQL:2011 | 5 | SQL:2011 is excellent! | 2 | + */ + @Test + public void testWithCTESQL() { + List tuples = doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + WITH + p_pc AS ( + SELECT + p.id AS post_id, + p.title AS post_title, + pc.id AS comment_id, + pc.review AS comment_review, + COUNT(post_id) OVER(PARTITION BY post_id) AS comment_count + FROM post p + LEFT JOIN post_comment pc ON p.id = pc.post_id + WHERE p.title LIKE :title + ), + p_pc_r AS ( + SELECT + post_id, + post_title, + comment_id, + comment_review, + DENSE_RANK() OVER (ORDER BY p_pc.comment_count DESC) AS ranking + FROM p_pc + ) + SELECT * + FROM p_pc_r + WHERE p_pc_r.ranking <= :ranking + ORDER BY post_id, comment_id + """, Tuple.class) + .setParameter("title", "SQL%") + .setParameter("ranking", 2) + .getResultList(); + }); + + assertEquals(5, tuples.size()); + + assertEquals(1, ((Number) tuples.get(0).get("post_id")).intValue()); + assertEquals("SQL:2016", tuples.get(0).get("post_title")); + assertEquals(1, ((Number) tuples.get(0).get("comment_id")).intValue()); + assertEquals("SQL:2016 is great!", tuples.get(0).get("comment_review")); + + assertEquals(2, ((Number) tuples.get(3).get("post_id")).intValue()); + assertEquals("SQL:2011", tuples.get(3).get("post_title")); + assertEquals(4, ((Number) tuples.get(3).get("comment_id")).intValue()); + assertEquals("SQL:2011 is great!", tuples.get(3).get("comment_review")); + + assertEquals(2, ((Number) tuples.get(4).get("post_id")).intValue()); + assertEquals("SQL:2011", tuples.get(4).get("post_title")); + assertEquals(5, ((Number) tuples.get(4).get("comment_id")).intValue()); + assertEquals("SQL:2011 is excellent!", tuples.get(4).get("comment_review")); + } + + @Test + @Ignore("Still not working on Hibernate 6.3") + public void testWithCTEJPQL() { + List tuples = doInJPA(entityManager -> { + return entityManager.createQuery(""" + WITH p_pc AS ( + SELECT + p.id AS post_id, + p.title AS post_title, + pc.id AS comment_id, + pc.review AS comment_review, + COUNT(p.id) OVER(PARTITION BY p.id) AS comment_count + FROM PostComment pc + JOIN pc.post p + WHERE p.title LIKE :title + ), + p_pc_r AS ( + SELECT + post_id, + post_title, + comment_id, + comment_review, + DENSE_RANK() OVER (ORDER BY p_pc.comment_count DESC) AS ranking + FROM p_pc + ) + SELECT * + FROM p_pc_r + WHERE p_pc_r.ranking <= :ranking + ORDER BY post_id, comment_id + """, Tuple.class) + .setParameter("title", "SQL%") + .setParameter("ranking", 2) + .getResultList(); + }); + + assertEquals(5, tuples.size()); + + assertEquals(1, ((Number) tuples.get(0).get("post_id")).intValue()); + assertEquals("SQL:2016", tuples.get(0).get("post_title")); + assertEquals(1, ((Number) tuples.get(0).get("comment_id")).intValue()); + assertEquals("SQL:2016 is great!", tuples.get(0).get("comment_review")); + + assertEquals(2, ((Number) tuples.get(3).get("post_id")).intValue()); + assertEquals("SQL:2011", tuples.get(3).get("post_title")); + assertEquals(4, ((Number) tuples.get(3).get("comment_id")).intValue()); + assertEquals("SQL:2011 is great!", tuples.get(3).get("comment_review")); + + assertEquals(2, ((Number) tuples.get(4).get("post_id")).intValue()); + assertEquals("SQL:2011", tuples.get(4).get("post_title")); + assertEquals(5, ((Number) tuples.get(4).get("comment_id")).intValue()); + assertEquals("SQL:2011 is excellent!", tuples.get(4).get("comment_review")); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Integer id; + + private String title; + + public Integer getId() { + return id; + } + + public Post setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Integer getId() { + return id; + } + + public PostComment setId(Integer id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/mixed/Country.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/mixed/Country.java new file mode 100644 index 000000000..029ad1100 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/mixed/Country.java @@ -0,0 +1,44 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.mixed; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Country") +public class Country { + + @Id + @GeneratedValue + private Long id; + + private String name; + + private String locale; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLocale() { + return locale; + } + + public void setLocale(String locale) { + this.locale = locale; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/mixed/DTOWithEntityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/mixed/DTOWithEntityTest.java new file mode 100644 index 000000000..6837cc25c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/mixed/DTOWithEntityTest.java @@ -0,0 +1,90 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.mixed; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import io.hypersistence.utils.hibernate.query.ListResultTransformer; +import org.hibernate.query.Query; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DTOWithEntityTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Person.class, + Country.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Country usa = new Country(); + usa.setName("USA"); + usa.setLocale("en-US"); + entityManager.persist(usa); + + Country romania = new Country(); + romania.setName("Romania"); + romania.setLocale("ro-RO"); + entityManager.persist(romania); + + Person chris = new Person(); + chris.setName("Chris"); + chris.setLocale("en-US"); + entityManager.persist(chris); + + Person vlad = new Person(); + vlad.setName("Vlad"); + vlad.setLocale("ro-RO"); + entityManager.persist(vlad); + }); + + doInJPA(entityManager -> { + LOGGER.info( "Using constructor resultl set" ); + List personAndAddressDTOs = entityManager.createQuery(""" + select new + com.vladmihalcea.hpjp.hibernate.query.dto.mixed.PersonAndCountryDTO( + p, + c.name + ) + from Person p + join Country c on p.locale = c.locale + order by p.id + """, PersonAndCountryDTO.class) + .getResultList(); + + PersonAndCountryDTO firstEntry = personAndAddressDTOs.get(0); + assertEquals("Chris", firstEntry.getPerson().getName()); + assertEquals("USA", firstEntry.getCountry()); + }); + + doInJPA(entityManager -> { + LOGGER.info( "Using ResultTransformer" ); + List personAndAddressDTOs = entityManager.createQuery(""" + select p, c.name + from Person p + join Country c on p.locale = c.locale + order by p.id + """) + .unwrap( Query.class ) + .setResultTransformer( + (ListResultTransformer) (tuple, aliases) -> new PersonAndCountryDTO( + (Person) tuple[0], + (String) tuple[1] + ) + ) + .getResultList(); + + PersonAndCountryDTO firstEntry = personAndAddressDTOs.get(0); + assertEquals("Chris", firstEntry.getPerson().getName()); + assertEquals("USA", firstEntry.getCountry()); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/mixed/Person.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/mixed/Person.java new file mode 100644 index 000000000..91fe77939 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/mixed/Person.java @@ -0,0 +1,44 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.mixed; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Person") +public class Person { + + @Id + @GeneratedValue + private Long id; + + private String name; + + private String locale; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLocale() { + return locale; + } + + public void setLocale(String locale) { + this.locale = locale; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/dto/PersonAndCountryDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/mixed/PersonAndCountryDTO.java similarity index 86% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/dto/PersonAndCountryDTO.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/mixed/PersonAndCountryDTO.java index b9ac11c88..733763d0f 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/dto/PersonAndCountryDTO.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/mixed/PersonAndCountryDTO.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.dto; +package com.vladmihalcea.hpjp.hibernate.query.dto.mixed; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/Post.java new file mode 100644 index 000000000..32e8a2570 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/Post.java @@ -0,0 +1,144 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@NamedQuery( + name = "PostDTOEntityQuery", + query = """ + select new + com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO( + p.id, + p.title + ) + from Post p + """ +) +@NamedNativeQuery( + name = "PostDTONativeQuery", + query = """ + SELECT + p.id AS id, + p.title AS title + FROM post p + """, + resultSetMapping = "PostDTOMapping" +) +@SqlResultSetMapping( + name = "PostDTOMapping", + classes = @ConstructorResult( + targetClass = PostDTO.class, + columns = { + @ColumnResult(name = "id"), + @ColumnResult(name = "title") + } + ) +) +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_on") + private LocalDateTime updatedOn; + + @Column(name = "updated_by") + private String updatedBy; + + @Version + private Short version; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public Post setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public Post setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + return this; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public Post setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + return this; + } + + public Short getVersion() { + return version; + } + + public Post setVersion(Short version) { + this.version = version; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/PostComment.java new file mode 100644 index 000000000..bfd5a7927 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/PostComment.java @@ -0,0 +1,46 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/hibernate/HibernateDTOProjectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/hibernate/HibernateDTOProjectionTest.java new file mode 100644 index 000000000..e060e3c60 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/hibernate/HibernateDTOProjectionTest.java @@ -0,0 +1,319 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.hibernate; + +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.Post; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.PostComment; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.DistinctListTransformer; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.PostDTO; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.PostDTOResultTransformer; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.PostDTOTupleTransformer; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.transform.Transformers; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@SuppressWarnings("unchecked") +public class HibernateDTOProjectionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn( + LocalDateTime.of(2016, 11, 2, 12, 0, 0) + ) + .setUpdatedBy("Vlad Mihalcea") + .setUpdatedOn( + LocalDateTime.now() + ) + .addComment( + new PostComment() + .setId(1L) + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("A must-read for every Java developer!") + ) + ); + + + entityManager.persist( + new Post() + .setId(2L) + .setTitle("Hypersistence Optimizer") + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn( + LocalDateTime.of(2019, 3, 19, 12, 0, 0) + ) + .setUpdatedBy("Vlad Mihalcea") + .setUpdatedOn( + LocalDateTime.now() + ) + .addComment( + new PostComment() + .setId(3L) + .setReview("It's like pair programming with Vlad!") + ) + ); + }); + } + + @Test + public void testJPQLResultTransformer() { + doInJPA( entityManager -> { + List postDTOs = entityManager.createQuery(""" + select p.id as id, + p.title as title + from Post p + order by p.id + """) + .unwrap(org.hibernate.query.Query.class) + .setTupleTransformer(Transformers.aliasToBean(PostDTO.class)) + .getResultList(); + + assertEquals(2, postDTOs.size()); + + PostDTO postDTO = postDTOs.get(0); + assertEquals(1L, postDTO.getId().longValue()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + } ); + } + + @Test + public void testJPQLTupleTransformer() { + doInJPA( entityManager -> { + List postDTOs = entityManager.createQuery(""" + select p.id as id, + p.title as title + from Post p + order by p.id + """) + .unwrap(org.hibernate.query.Query.class) + .setTupleTransformer(Transformers.aliasToBean(PostDTO.class)) + .getResultList(); + + assertEquals(2, postDTOs.size()); + + PostDTO postDTO = postDTOs.get(0); + assertEquals(1L, postDTO.getId().longValue()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + } ); + } + + @Test + public void testNativeQueryResultTransformer() { + doInJPA( entityManager -> { + List postDTOs = entityManager.createNativeQuery(""" + SELECT p.id AS id, + p.title AS title + FROM post p + ORDER BY p.id + """) + .unwrap(org.hibernate.query.Query.class) + .setTupleTransformer(Transformers.aliasToBean(PostDTO.class)) + .getResultList(); + + assertEquals(2, postDTOs.size()); + + PostDTO postDTO = postDTOs.get(0); + assertEquals(1L, postDTO.getId().longValue()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + } ); + } + + @Test + public void testRecord() { + doInJPA(entityManager -> { + List postRecords = entityManager.createQuery(""" + select + p.id, + p.title, + p.createdOn, + p.createdBy, + p.updatedOn, + p.updatedBy + from Post p + order by p.id + """) + .unwrap(org.hibernate.query.Query.class) + .setTupleTransformer((tuple, aliases) -> { + int i =0; + return new PostRecord( + longValue(tuple[i++]), + stringValue(tuple[i++]), + new AuditRecord( + localDateTimeValue(tuple[i++]), + stringValue(tuple[i++]), + localDateTimeValue(tuple[i++]), + stringValue(tuple[i++]) + ) + ); + } + ) + .getResultList(); + + assertEquals(2, postRecords.size()); + + PostRecord postRecord = postRecords.get(0); + + assertEquals( + 1L, postRecord.id().longValue() + ); + + assertEquals( + "High-Performance Java Persistence", postRecord.title() + ); + + assertEquals( + LocalDateTime.of(2016, 11, 2, 12, 0, 0), postRecord.audit().createdOn() + ); + + assertEquals( + "Vlad Mihalcea", postRecord.audit().createdBy() + ); + }); + } + + @Test + public void testParentChildDTOProjectionNativeQueryTupleTransformer() { + doInJPA( entityManager -> { + List postDTOs = entityManager.createNativeQuery(""" + SELECT p.id AS p_id, + p.title AS p_title, + pc.id AS pc_id, + pc.review AS pc_review + FROM post p + JOIN post_comment pc ON p.id = pc.post_id + ORDER BY pc.id + """) + .unwrap(org.hibernate.query.Query.class) + .setTupleTransformer(new PostDTOTupleTransformer()) + .setResultListTransformer(DistinctListTransformer.INSTANCE) + .getResultList(); + + assertEquals(2, postDTOs.size()); + assertEquals(2, postDTOs.get(0).getComments().size()); + assertEquals(1, postDTOs.get(1).getComments().size()); + + PostDTO post1DTO = postDTOs.get(0); + + assertEquals(1L, post1DTO.getId().longValue()); + assertEquals(2, post1DTO.getComments().size()); + assertEquals(1L, post1DTO.getComments().get(0).getId().longValue()); + assertEquals(2L, post1DTO.getComments().get(1).getId().longValue()); + + PostDTO post2DTO = postDTOs.get(1); + + assertEquals(2L, post2DTO.getId().longValue()); + assertEquals(1, post2DTO.getComments().size()); + assertEquals(3L, post2DTO.getComments().get(0).getId().longValue()); + } ); + } + + @Test + public void testParentChildDTOProjectionJPQLResultTransformer() { + doInJPA( entityManager -> { + List postDTOs = entityManager.createQuery(""" + select p.id as p_id, + p.title as p_title, + pc.id as pc_id, + pc.review as pc_review + from PostComment pc + join pc.post p + order by pc.id + """) + .unwrap(org.hibernate.query.Query.class) + .setTupleTransformer(new PostDTOResultTransformer()) + .setResultListTransformer(DistinctListTransformer.INSTANCE) + .getResultList(); + + assertEquals(2, postDTOs.size()); + assertEquals(2, postDTOs.get(0).getComments().size()); + assertEquals(1, postDTOs.get(1).getComments().size()); + + PostDTO post1DTO = postDTOs.get(0); + + assertEquals(1L, post1DTO.getId().longValue()); + assertEquals(2, post1DTO.getComments().size()); + assertEquals(1L, post1DTO.getComments().get(0).getId().longValue()); + assertEquals(2L, post1DTO.getComments().get(1).getId().longValue()); + + PostDTO post2DTO = postDTOs.get(1); + + assertEquals(2L, post2DTO.getId().longValue()); + assertEquals(1, post2DTO.getComments().size()); + assertEquals(3L, post2DTO.getComments().get(0).getId().longValue()); + } ); + } + + @Test + public void testParentChildEntityProjectionJPQLResultTransformer() { + doInJPA( entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + join fetch p.comments pc + order by pc.id + """) + .getResultList(); + + assertEquals(2, posts.size()); + assertEquals(2, posts.get(0).getComments().size()); + assertEquals(1, posts.get(1).getComments().size()); + + Post post1 = posts.get(0); + + assertEquals(1L, post1.getId().longValue()); + assertEquals(2, post1.getComments().size()); + assertEquals(1L, post1.getComments().get(0).getId().longValue()); + assertEquals(2L, post1.getComments().get(1).getId().longValue()); + + Post post2 = posts.get(1); + + assertEquals(2L, post2.getId().longValue()); + assertEquals(1, post2.getComments().size()); + assertEquals(3L, post2.getComments().get(0).getId().longValue()); + } ); + } + + public static record PostRecord( + Long id, + String title, + AuditRecord audit + ) { + } + + public static record AuditRecord( + LocalDateTime createdOn, + String createdBy, + LocalDateTime updatedOn, + String updatedBy + ) { + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/JPADTOProjectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/JPADTOProjectionTest.java new file mode 100644 index 000000000..72d2dd486 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/JPADTOProjectionTest.java @@ -0,0 +1,252 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.jpa; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.Post; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.PostComment; +import com.vladmihalcea.hpjp.util.AbstractTest; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.junit.Test; + +import jakarta.persistence.Tuple; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@SuppressWarnings("unchecked") +public class JPADTOProjectionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator( + List.of( + PostDTO.class, + PostRecord.class + ) + ) + ) + ); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn( + LocalDateTime.of(2016, 11, 2, 12, 0, 0) + ) + .setUpdatedBy("Vlad Mihalcea") + .setUpdatedOn( + LocalDateTime.now() + ) + ); + }); + } + + @Test + public void testDefaultProjection() { + doInJPA(entityManager -> { + List tuples = entityManager.createQuery(""" + select + p.id, + p.title + from Post p + """) + .getResultList(); + + assertEquals(1, tuples.size()); + + Object[] tuple = tuples.get(0); + long id = ((Number) tuple[0]).longValue(); + String title = (String) tuple[1]; + + assertEquals(1L, id); + assertEquals("High-Performance Java Persistence", title); + }); + } + + @Test + public void testDefaultProjectionNativeQuery() { + doInJPA(entityManager -> { + List tuples = entityManager.createNativeQuery(""" + SELECT + p.id AS id, + p.title AS title + FROM post p + """ + ) + .getResultList(); + + assertEquals(1, tuples.size()); + + Object[] tuple = tuples.get(0); + long id = ((Number) tuple[0]).longValue(); + String title = (String) tuple[1]; + + assertEquals(1L, id); + assertEquals("High-Performance Java Persistence", title); + }); + } + + @Test + public void testTuple() { + doInJPA(entityManager -> { + List tuples = entityManager.createQuery(""" + select + p.id as id, + p.title as title + from Post p + """, Tuple.class) + .getResultList(); + + assertEquals(1, tuples.size()); + + Tuple tuple = tuples.get(0); + long id = tuple.get("id", Number.class).longValue(); + String title = tuple.get("title", String.class); + + assertEquals(1L, id); + assertEquals("High-Performance Java Persistence", title); + }); + } + + @Test + public void testTupleNativeQuery() { + doInJPA(entityManager -> { + List tuples = entityManager.createNativeQuery(""" + SELECT + p.id AS id, + p.title AS title + FROM post p + """, Tuple.class) + .getResultList(); + + assertEquals(1, tuples.size()); + + Tuple tuple = tuples.get(0); + long id = tuple.get("id", Number.class).longValue(); + String title = tuple.get("title", String.class); + + assertEquals(1L, id); + assertEquals("High-Performance Java Persistence", title); + }); + } + + @Test + public void testConstructorExpression() { + doInJPA(entityManager -> { + List postDTOs = entityManager.createQuery(""" + select new com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO( + p.id, + p.title + ) + from Post p + """, PostDTO.class) + .getResultList(); + + assertEquals(1, postDTOs.size()); + + PostDTO postDTO = postDTOs.get(0); + assertEquals(1L, postDTO.getId().longValue()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + }); + } + + @Test + public void testConstructorExpressionSimpleClassName() { + doInJPA(entityManager -> { + List postDTOs = entityManager.createQuery(""" + select new PostDTO( + p.id, + p.title + ) + from Post p + """, PostDTO.class) + .getResultList(); + + assertEquals(1, postDTOs.size()); + + PostDTO postDTO = postDTOs.get(0); + assertEquals(1L, postDTO.getId().longValue()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + }); + } + + @Test + public void testNamedQuery() { + doInJPA(entityManager -> { + List postDTOs = entityManager.createNamedQuery( + "PostDTOEntityQuery", PostDTO.class) + .getResultList(); + + assertEquals(1, postDTOs.size()); + + PostDTO postDTO = postDTOs.get(0); + assertEquals(1L, postDTO.getId().longValue()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + }); + } + + @Test + public void testNamedNativeQuery() { + doInJPA(entityManager -> { + List postDTOs = entityManager.createNamedQuery( + "PostDTONativeQuery") + .getResultList(); + + assertEquals(1, postDTOs.size()); + + PostDTO postDTO = postDTOs.get(0); + assertEquals(1L, postDTO.getId().longValue()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + }); + } + + @Test + public void testRecord() { + doInJPA(entityManager -> { + List postRecords = entityManager.createQuery(""" + select new PostRecord( + p.id, + p.title + ) + from Post p + """, PostRecord.class) + .getResultList(); + + assertEquals(1, postRecords.size()); + + PostRecord postRecord = postRecords.get(0); + assertEquals(1L, postRecord.id().longValue()); + assertEquals("High-Performance Java Persistence", postRecord.title()); + }); + } + + public static record PostRecord( + Number id, + String title + ) { + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/compact/JPADTOProjectionClassImportIntegratorPropertyClassTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/compact/JPADTOProjectionClassImportIntegratorPropertyClassTest.java new file mode 100644 index 000000000..b65ff04f8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/compact/JPADTOProjectionClassImportIntegratorPropertyClassTest.java @@ -0,0 +1,97 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.jpa.compact; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.Post; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.PostComment; +import com.vladmihalcea.hpjp.util.AbstractTest; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.junit.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JPADTOProjectionClassImportIntegratorPropertyClassTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.integrator_provider", + ClassImportIntegratorIntegratorProvider.class + ); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn( + LocalDateTime.of(2016, 11, 2, 12, 0, 0) + ) + .setUpdatedBy("Vlad Mihalcea") + .setUpdatedOn( + LocalDateTime.now() + ) + ); + }); + } + + @Test + public void testConstructorExpression() { + doInJPA(entityManager -> { + List postDTOs = entityManager.createQuery(""" + select new PostDTO( + p.id, + p.title + ) + from Post p + where p.createdOn > :fromTimestamp + """, PostDTO.class) + .setParameter( + "fromTimestamp", + LocalDate.of(2016, 1, 1).atStartOfDay() + ) + .getResultList(); + + assertEquals(1, postDTOs.size()); + + PostDTO postDTO = postDTOs.get(0); + assertEquals(1L, postDTO.getId().longValue()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + }); + } + + public static class ClassImportIntegratorIntegratorProvider implements IntegratorProvider { + + @Override + public List getIntegrators() { + return List.of( + new ClassImportIntegrator( + List.of( + PostDTO.class + ) + ) + ); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/compact/JPADTOProjectionClassImportIntegratorPropertyObjectExcludePathTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/compact/JPADTOProjectionClassImportIntegratorPropertyObjectExcludePathTest.java new file mode 100644 index 000000000..e75cb36a0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/compact/JPADTOProjectionClassImportIntegratorPropertyObjectExcludePathTest.java @@ -0,0 +1,89 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.jpa.compact; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.Post; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.PostComment; +import com.vladmihalcea.hpjp.util.AbstractTest; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.junit.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JPADTOProjectionClassImportIntegratorPropertyObjectExcludePathTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> List.of( + new ClassImportIntegrator( + List.of( + PostDTO.class + ) + ) + .excludePath("com.vladmihalcea.hpjp.hibernate") + ) + ); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn( + LocalDateTime.of(2016, 11, 2, 12, 0, 0) + ) + .setUpdatedBy("Vlad Mihalcea") + .setUpdatedOn( + LocalDateTime.now() + ) + ); + }); + } + + @Test + public void testConstructorExpression() { + doInJPA(entityManager -> { + List postDTOs = entityManager.createQuery(""" + select new forum.dto.PostDTO( + p.id, + p.title + ) + from Post p + where p.createdOn > :fromTimestamp + """, PostDTO.class) + .setParameter( + "fromTimestamp", + LocalDate.of(2016, 1, 1).atStartOfDay() + ) + .getResultList(); + + assertEquals(1, postDTOs.size()); + + PostDTO postDTO = postDTOs.get(0); + assertEquals(1L, postDTO.getId().longValue()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/compact/JPADTOProjectionClassImportIntegratorPropertyObjectTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/compact/JPADTOProjectionClassImportIntegratorPropertyObjectTest.java new file mode 100644 index 000000000..66c641083 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/compact/JPADTOProjectionClassImportIntegratorPropertyObjectTest.java @@ -0,0 +1,86 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.jpa.compact; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.Post; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.PostComment; +import com.vladmihalcea.hpjp.util.AbstractTest; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.junit.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JPADTOProjectionClassImportIntegratorPropertyObjectTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> List.of( + new ClassImportIntegrator( + List.of(PostDTO.class) + ) + ) + ); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn( + LocalDateTime.of(2016, 11, 2, 12, 0, 0) + ) + .setUpdatedBy("Vlad Mihalcea") + .setUpdatedOn( + LocalDateTime.now() + ) + ); + }); + } + + @Test + public void testConstructorExpression() { + doInJPA(entityManager -> { + List postDTOs = entityManager.createQuery(""" + select new PostDTO( + p.id, + p.title + ) + from Post p + where p.createdOn > :fromTimestamp + """, PostDTO.class) + .setParameter( + "fromTimestamp", + LocalDate.of(2016, 1, 1).atStartOfDay() + ) + .getResultList(); + + assertEquals(1, postDTOs.size()); + + PostDTO postDTO = postDTOs.get(0); + assertEquals(1L, postDTO.getId().longValue()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/compact/JPADTOProjectionClassImportIntegratorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/compact/JPADTOProjectionClassImportIntegratorTest.java new file mode 100644 index 000000000..1742b4ab1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/jpa/compact/JPADTOProjectionClassImportIntegratorTest.java @@ -0,0 +1,80 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.jpa.compact; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.Post; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.PostComment; +import com.vladmihalcea.hpjp.util.AbstractTest; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import org.hibernate.integrator.spi.Integrator; +import org.junit.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JPADTOProjectionClassImportIntegratorTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected Integrator integrator() { + return new ClassImportIntegrator( + List.of(PostDTO.class) + ); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn( + LocalDateTime.of(2016, 11, 2, 12, 0, 0) + ) + .setUpdatedBy("Vlad Mihalcea") + .setUpdatedOn( + LocalDateTime.now() + ) + ); + }); + } + + @Test + public void testConstructorExpression() { + doInJPA(entityManager -> { + List postDTOs = entityManager.createQuery(""" + select new PostDTO( + p.id, + p.title + ) + from Post p + where p.createdOn > :fromTimestamp + """, PostDTO.class) + .setParameter( + "fromTimestamp", + LocalDate.of(2016, 1, 1).atStartOfDay() + ) + .getResultList(); + + assertEquals(1, postDTOs.size()); + + PostDTO postDTO = postDTOs.get(0); + assertEquals(1L, postDTO.getId().longValue()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/record/EntityToRecordTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/record/EntityToRecordTest.java new file mode 100644 index 000000000..694ef6d93 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/record/EntityToRecordTest.java @@ -0,0 +1,183 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.record; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Version; +import org.junit.Test; + +import java.time.LocalDateTime; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class EntityToRecordTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Override + public void afterInit() { + doInStatelessSession(session -> { + session.insert( + new PostRecord( + 1L, + "High-Performance Java Persistence", + LocalDateTime.of(2016, 11, 2, 12, 0, 0), + "Vlad Mihalcea", + LocalDateTime.now(), + "Vlad Mihalcea", + null + ).toPost() + ); + }); + } + + @Test + public void testRecordEntity() { + PostRecord postRecord = doInJPA(entityManager -> { + return entityManager.find(Post.class, 1L).toRecord(); + }); + + assertEquals( + "High-Performance Java Persistence", postRecord.title() + ); + + PostRecord updatedPostRecord = new PostRecord( + postRecord.id, + postRecord.title, + postRecord.createdOn, + postRecord.createdBy, + LocalDateTime.now(), + "Vlad", + postRecord.version + ); + + doInStatelessSession(session -> { + session.update( + updatedPostRecord.toPost() + ); + }); + } + + @Entity(name = "Post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_on") + private LocalDateTime updatedOn; + + @Column(name = "updated_by") + private String updatedBy; + + @Version + private Short version; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public Post setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public Post setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + return this; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public Post setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + return this; + } + + public Short getVersion() { + return version; + } + + public Post setVersion(Short version) { + this.version = version; + return this; + } + + public PostRecord toRecord() { + return new PostRecord( + id, title, createdOn, createdBy, updatedOn, updatedBy, version + ); + } + } + + public static record PostRecord ( + Long id, + String title, + LocalDateTime createdOn, + String createdBy, + LocalDateTime updatedOn, + String updatedBy, + Short version) { + + public Post toPost() { + return new Post() + .setId(id) + .setTitle(title) + .setCreatedOn(createdOn) + .setCreatedBy(createdBy) + .setUpdatedOn(updatedOn) + .setUpdatedBy(updatedBy) + .setVersion(version); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/record/PostLegacyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/record/PostLegacyTest.java new file mode 100644 index 000000000..7e0e87b85 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/record/PostLegacyTest.java @@ -0,0 +1,292 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.record; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Version; +import java.time.LocalDateTime; +import java.util.Objects; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostLegacyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + post.setCreatedBy("Vlad Mihalcea"); + post.setCreatedOn( + LocalDateTime.of(2020, 11, 2, 12, 0, 0) + ); + post.setUpdatedBy("Vlad Mihalcea"); + post.setUpdatedOn( + LocalDateTime.now() + ); + + entityManager.persist(post); + }); + } + + @Test + public void testRecord() { + doInJPA(entityManager -> { + PostInfo postInfo = new PostInfo( + 1L, + "High-Performance Java Persistence", + new AuditInfo( + LocalDateTime.of(2016, 11, 2, 12, 0, 0), + "Vlad Mihalcea", + LocalDateTime.now(), + "Vlad Mihalcea" + ) + ); + + assertEquals( + 1L, postInfo.getId().longValue() + ); + + assertEquals( + "High-Performance Java Persistence", postInfo.getTitle() + ); + + assertEquals( + LocalDateTime.of(2016, 11, 2, 12, 0, 0), postInfo.getAuditInfo().getCreatedOn() + ); + + assertEquals( + "Vlad Mihalcea", postInfo.getAuditInfo().getCreatedBy() + ); + + LOGGER.info("Audit info:\n{}", postInfo.getAuditInfo()); + LOGGER.info("Post info:\n{}", postInfo); + }); + } + + @Entity(name = "Post") + public class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_on") + private LocalDateTime updatedOn; + + @Column(name = "updated_by") + private String updatedBy; + + @Version + private Short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + } + + public Short getVersion() { + return version; + } + + public void setVersion(Short version) { + this.version = version; + } + } + + public static class AuditInfo { + + private final LocalDateTime createdOn; + + private final String createdBy; + + private final LocalDateTime updatedOn; + + private final String updatedBy; + + public AuditInfo( + LocalDateTime createdOn, + String createdBy, + LocalDateTime updatedOn, + String updatedBy) { + this.createdOn = createdOn; + this.createdBy = createdBy; + this.updatedOn = updatedOn; + this.updatedBy = updatedBy; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public String getUpdatedBy() { + return updatedBy; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AuditInfo)) return false; + + AuditInfo auditInfo = (AuditInfo) o; + return createdOn.equals(auditInfo.createdOn) && + createdBy.equals(auditInfo.createdBy) && + Objects.equals(updatedOn, auditInfo.updatedOn) && + Objects.equals(updatedBy, auditInfo.updatedBy); + } + + @Override + public int hashCode() { + return Objects.hash(createdOn, createdBy, updatedOn, updatedBy); + } + + @Override + public String toString() { + return String.format(""" + AuditInfo { + createdOn : '%s', + createdBy : '%s', + updatedOn : '%s', + updatedBy : '%s' + } + """, + createdOn, + createdBy, + updatedOn, + updatedBy + ); + } + } + + public static class PostInfo { + + private final Long id; + + private final String title; + + private final AuditInfo auditInfo; + + public PostInfo( + Long id, + String title, + AuditInfo auditInfo) { + this.id = id; + this.title = title; + this.auditInfo = auditInfo; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public AuditInfo getAuditInfo() { + return auditInfo; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostInfo)) return false; + PostInfo postInfo = (PostInfo) o; + return id.equals(postInfo.id) && + title.equals(postInfo.title) && + auditInfo.equals(postInfo.auditInfo); + } + + @Override + public int hashCode() { + return Objects.hash(id, title, auditInfo); + } + + @Override + public String toString() { + return String.format(""" + PostInfo { + id : '%s', + title : '%s', + auditInfo : '%s' + } + """, + id, + title, + auditInfo + ); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/record/PostRecordOverrideTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/record/PostRecordOverrideTest.java new file mode 100644 index 000000000..69ed7688b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/record/PostRecordOverrideTest.java @@ -0,0 +1,212 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.record; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Version; +import java.time.LocalDateTime; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostRecordOverrideTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + post.setCreatedBy("Vlad Mihalcea"); + post.setCreatedOn( + LocalDateTime.of(2020, 11, 2, 12, 0, 0) + ); + post.setUpdatedBy("Vlad Mihalcea"); + post.setUpdatedOn( + LocalDateTime.now() + ); + + entityManager.persist(post); + }); + } + + @Test + public void testRecord() { + doInJPA(entityManager -> { + PostInfo postInfo = new PostInfo( + 1L, + "High-Performance Java Persistence", + new AuditInfo( + LocalDateTime.of(2016, 11, 2, 12, 0, 0), + "Vlad Mihalcea", + LocalDateTime.now(), + "Vlad Mihalcea" + ) + ); + + assertEquals( + 1L, postInfo.id().longValue() + ); + + assertEquals( + "High-Performance Java Persistence", postInfo.title() + ); + + assertEquals( + LocalDateTime.of(2016, 11, 2, 12, 0, 0), postInfo.auditInfo().createdOn() + ); + + assertEquals( + "Vlad Mihalcea", postInfo.auditInfo().createdBy() + ); + + LOGGER.info("Audit info:\nenable-preview{}", postInfo.auditInfo()); + LOGGER.info("Post info:\n{}", postInfo); + }); + } + + @Entity(name = "Post") + public class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_on") + private LocalDateTime updatedOn; + + @Column(name = "updated_by") + private String updatedBy; + + @Version + private Short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + } + + public Short getVersion() { + return version; + } + + public void setVersion(Short version) { + this.version = version; + } + } + + public static record AuditInfo( + LocalDateTime createdOn, + String createdBy, + LocalDateTime updatedOn, + String updatedBy + ) { + @Override + public String toString() { + return String.format(""" + AuditInfo { + createdOn : '%s', + createdBy : '%s', + updatedOn : '%s', + updatedBy : '%s' + } + """, + createdOn, + createdBy, + updatedOn, + updatedBy + ); + } + } + + public static record PostInfo( + Long id, + String title, + AuditInfo auditInfo + ) { + @Override + public String toString() { + return String.format(""" + PostInfo { + id : '%s', + title : '%s', + auditInfo : { + createdOn : '%s', + createdBy : '%s', + updatedOn : '%s', + updatedBy : '%s' + } + } + """, + id, + title, + auditInfo.createdOn, + auditInfo.createdBy, + auditInfo.updatedOn, + auditInfo.updatedBy + ); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/record/PostRecordTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/record/PostRecordTest.java new file mode 100644 index 000000000..d5b1f48b5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/record/PostRecordTest.java @@ -0,0 +1,296 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.record; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import io.hypersistence.utils.hibernate.query.ListResultTransformer; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.hibernate.query.Query; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Version; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostRecordTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator( + List.of( + AuditInfo.class, + PostInfo.class + ) + ) + ) + ); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn( + LocalDateTime.of(2016, 11, 2, 12, 0, 0) + ) + .setUpdatedBy("Vlad Mihalcea") + .setUpdatedOn( + LocalDateTime.now() + ) + ); + entityManager.persist( + new Post() + .setId(2L) + .setTitle("Hypersistence Optimizer") + .setCreatedBy("Vlad Mihalcea") + .setCreatedOn( + LocalDateTime.of(2020, 3, 19, 12, 0, 0) + ) + .setUpdatedBy("Vlad Mihalcea") + .setUpdatedOn( + LocalDateTime.now() + ) + ); + }); + } + + @Test + public void testRecord() { + doInJPA(entityManager -> { + PostInfo postInfo = new PostInfo( + 1L, + "High-Performance Java Persistence", + new AuditInfo( + LocalDateTime.of(2016, 11, 2, 12, 0, 0), + "Vlad Mihalcea", + LocalDateTime.now(), + "Vlad Mihalcea" + ) + ); + + assertEquals( + 1L, postInfo.id().longValue() + ); + + assertEquals( + "High-Performance Java Persistence", postInfo.title() + ); + + assertEquals( + LocalDateTime.of(2016, 11, 2, 12, 0, 0), postInfo.auditInfo().createdOn() + ); + + assertEquals( + "Vlad Mihalcea", postInfo.auditInfo().createdBy() + ); + + LOGGER.info("Audit info:\n{}", postInfo.auditInfo()); + LOGGER.info("Post info:\n{}", postInfo); + }); + } + + @Test + public void testRecordDTO() { + doInJPA(entityManager -> { + AuditInfo auditInfo = entityManager.createQuery(""" + select + new AuditInfo ( + p.createdOn, + p.createdBy, + p.updatedOn, + p.updatedBy + ) + from Post p + where p.id = :postId + """, AuditInfo.class) + .setParameter("postId", 1L) + .getSingleResult(); + + assertEquals( + LocalDateTime.of(2016, 11, 2, 12, 0, 0), auditInfo.createdOn() + ); + + assertEquals( + "Vlad Mihalcea", auditInfo.createdBy() + ); + + assertEquals( + "Vlad Mihalcea", auditInfo.updatedBy() + ); + }); + + doInJPA(entityManager -> { + List postInfos = entityManager.createQuery(""" + select + p.id, + p.title, + p.createdOn, + p.createdBy, + p.updatedOn, + p.updatedBy + from Post p + order by p.id + """) + .unwrap(Query.class) + .setResultTransformer( + (ListResultTransformer) (tuple, aliases) -> { + int i =0; + return new PostInfo( + ((Number) tuple[i++]).longValue(), + (String) tuple[i++], + new AuditInfo( + (LocalDateTime) tuple[i++], + (String) tuple[i++], + (LocalDateTime) tuple[i++], + (String) tuple[i++] + ) + ); + } + ) + .getResultList(); + + assertEquals(2, postInfos.size()); + + PostInfo postInfo = postInfos.get(0); + + assertEquals( + 1L, postInfo.id().longValue() + ); + + assertEquals( + "High-Performance Java Persistence", postInfo.title() + ); + + assertEquals( + LocalDateTime.of(2016, 11, 2, 12, 0, 0), postInfo.auditInfo().createdOn() + ); + + assertEquals( + "Vlad Mihalcea", postInfo.auditInfo().createdBy() + ); + }); + } + + @Entity(name = "Post") + public class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_on") + private LocalDateTime updatedOn; + + @Column(name = "updated_by") + private String updatedBy; + + @Version + private Short version; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public Post setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public Post setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + return this; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public Post setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + return this; + } + + public Short getVersion() { + return version; + } + + public Post setVersion(Short version) { + this.version = version; + return this; + } + } + + public static record AuditInfo( + LocalDateTime createdOn, + String createdBy, + LocalDateTime updatedOn, + String updatedBy + ) {} + + public static record PostInfo( + Long id, + String title, + AuditInfo auditInfo + ) {} +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/DistinctListTransformer.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/DistinctListTransformer.java new file mode 100644 index 000000000..9030acec9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/DistinctListTransformer.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer; + +import org.hibernate.query.ResultListTransformer; + +import java.util.List; + +/** + * + * @author Vlad Mihalcea + */ +public class DistinctListTransformer implements ResultListTransformer { + + public static final DistinctListTransformer INSTANCE = new DistinctListTransformer(); + + @Override + public List transformList(List collection) { + return collection.stream().distinct().toList(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/PostCommentDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/PostCommentDTO.java new file mode 100644 index 000000000..e082717a0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/PostCommentDTO.java @@ -0,0 +1,47 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer; + +import com.vladmihalcea.hpjp.util.AbstractTest; + +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentDTO { + public static final String ID_ALIAS = "pc_id"; + public static final String REVIEW_ALIAS = "pc_review"; + + private Long id; + + private String review; + + public PostCommentDTO(Long id, String review) { + this.id = id; + this.review = review; + } + + public PostCommentDTO(Object[] tuples, Map aliasToIndexMap) { + this.id = AbstractTest.longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]); + this.review = AbstractTest.stringValue(tuples[aliasToIndexMap.get(REVIEW_ALIAS)]); + } + + public Long getId() { + return id; + } + + public void setId(Number id) { + this.id = id.longValue(); + } + + public void setId(Long id) { + this.id = id; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/PostDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/PostDTO.java new file mode 100644 index 000000000..6351b7454 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/PostDTO.java @@ -0,0 +1,55 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer; + +import com.vladmihalcea.hpjp.util.AbstractTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class PostDTO { + + public static final String ID_ALIAS = "p_id"; + public static final String TITLE_ALIAS = "p_title"; + + private Long id; + + private String title; + + private List comments = new ArrayList<>(); + + public PostDTO() { + } + + public PostDTO(Long id, String title) { + this.id = id; + this.title = title; + } + + public PostDTO(Object[] tuples, Map aliasToIndexMap) { + this.id = AbstractTest.longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]); + this.title = AbstractTest.stringValue(tuples[aliasToIndexMap.get(TITLE_ALIAS)]); + } + + public Long getId() { + return id; + } + + public void setId(Number id) { + this.id = id.longValue(); + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/PostDTOResultTransformer.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/PostDTOResultTransformer.java new file mode 100644 index 000000000..74da99399 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/PostDTOResultTransformer.java @@ -0,0 +1,9 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer; + +/** + * + * @author Vlad Mihalcea + */ +public class PostDTOResultTransformer extends PostDTOTupleTransformer { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/PostDTOTupleTransformer.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/PostDTOTupleTransformer.java new file mode 100644 index 000000000..d55366edc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/dto/projection/transformer/PostDTOTupleTransformer.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.query.TupleTransformer; + +import java.util.*; + +/** + * + * @author Vlad Mihalcea + */ +public class PostDTOTupleTransformer implements TupleTransformer { + + private Map postDTOMap = new LinkedHashMap<>(); + + @Override + public PostDTO transformTuple(Object[] tuple, String[] aliases) { + Map aliasToIndexMap = aliasToIndexMap(aliases); + Long postId = AbstractTest.longValue(tuple[aliasToIndexMap.get(PostDTO.ID_ALIAS)]); + + PostDTO postDTO = postDTOMap.computeIfAbsent( + postId, + id -> new PostDTO(tuple, aliasToIndexMap) + ); + postDTO.getComments().add(new PostCommentDTO(tuple, aliasToIndexMap)); + + return postDTO; + } + + private Map aliasToIndexMap(String[] aliases) { + Map aliasToIndexMap = new LinkedHashMap<>(); + for (int i = 0; i < aliases.length; i++) { + aliasToIndexMap.put(aliases[i].toLowerCase(Locale.ROOT), i); + } + return aliasToIndexMap; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/HibernateEscapeKeywordTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/HibernateEscapeKeywordTest.java new file mode 100644 index 000000000..5d5f14a82 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/HibernateEscapeKeywordTest.java @@ -0,0 +1,114 @@ +package com.vladmihalcea.hpjp.hibernate.query.escape; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class HibernateEscapeKeywordTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Table.class, + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Table() + .setCatalog("library") + .setSchema("public") + .setName("book") + .setDescription("The book table stores book-related info") + ); + }); + + doInJPA(entityManager -> { + List tables = entityManager.createQuery( + "select t " + + "from Table t " + + "where t.description like '%book%'", Table.class) + .getResultList(); + + assertEquals(1, tables.size()); + }); + } + + @Entity(name = "Table") + @jakarta.persistence.Table(name = "`table`") + public static class Table { + + @Id + @GeneratedValue + private Long id; + + @Column(name = "`catalog`") + private String catalog; + + @Column(name = "`schema`") + private String schema; + + @Column(name = "`name`") + private String name; + + @Column(name = "`desc`") + private String description; + + public Long getId() { + return id; + } + + public Table setId(Long id) { + this.id = id; + return this; + } + + public String getCatalog() { + return catalog; + } + + public Table setCatalog(String catalog) { + this.catalog = catalog; + return this; + } + + public String getSchema() { + return schema; + } + + public Table setSchema(String schema) { + this.schema = schema; + return this; + } + + public String getName() { + return name; + } + + public Table setName(String name) { + this.name = name; + return this; + } + + public String getDescription() { + return description; + } + + public Table setDescription(String description) { + this.description = description; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/HibernateGlobalEscapeKeywordSkipReservedIdentifierTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/HibernateGlobalEscapeKeywordSkipReservedIdentifierTest.java new file mode 100644 index 000000000..a2560a423 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/HibernateGlobalEscapeKeywordSkipReservedIdentifierTest.java @@ -0,0 +1,118 @@ +package com.vladmihalcea.hpjp.hibernate.query.escape; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class HibernateGlobalEscapeKeywordSkipReservedIdentifierTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Table.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS, Boolean.TRUE.toString()); + properties.put(AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS_SKIP_COLUMN_DEFINITIONS, Boolean.TRUE.toString()); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Table() + .setCatalog("library") + .setSchema("public") + .setName("book") + .setDescription("The book table stores book-related info") + ); + }); + + doInJPA(entityManager -> { + List
tables = entityManager.createQuery( + "select t " + + "from Table t " + + "where t.description like '%book%'", Table.class) + .getResultList(); + + assertEquals(1, tables.size()); + }); + } + + @Entity(name = "Table") + public static class Table { + + @Id + @GeneratedValue + @Column(columnDefinition = "smallint") + private Integer id; + + private String catalog; + + private String schema; + + private String name; + + private String description; + + public Integer getId() { + return id; + } + + public Table setId(Integer id) { + this.id = id; + return this; + } + + public String getCatalog() { + return catalog; + } + + public Table setCatalog(String catalog) { + this.catalog = catalog; + return this; + } + + public String getSchema() { + return schema; + } + + public Table setSchema(String schema) { + this.schema = schema; + return this; + } + + public String getName() { + return name; + } + + public Table setName(String name) { + this.name = name; + return this; + } + + public String getDescription() { + return description; + } + + public Table setDescription(String description) { + this.description = description; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/HibernateGlobalEscapeKeywordTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/HibernateGlobalEscapeKeywordTest.java new file mode 100644 index 000000000..ed834678a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/HibernateGlobalEscapeKeywordTest.java @@ -0,0 +1,115 @@ +package com.vladmihalcea.hpjp.hibernate.query.escape; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class HibernateGlobalEscapeKeywordTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Table.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS, Boolean.TRUE.toString()); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Table() + .setCatalog("library") + .setSchema("public") + .setName("book") + .setDescription("The book table stores book-related info") + ); + }); + + doInJPA(entityManager -> { + List
tables = entityManager.createQuery( + "select t " + + "from Table t " + + "where t.description like '%book%'", Table.class) + .getResultList(); + + assertEquals(1, tables.size()); + }); + } + + @Entity(name = "Table") + public static class Table { + + @Id + @GeneratedValue + private Long id; + + private String catalog; + + private String schema; + + private String name; + + private String description; + + public Long getId() { + return id; + } + + public Table setId(Long id) { + this.id = id; + return this; + } + + public String getCatalog() { + return catalog; + } + + public Table setCatalog(String catalog) { + this.catalog = catalog; + return this; + } + + public String getSchema() { + return schema; + } + + public Table setSchema(String schema) { + this.schema = schema; + return this; + } + + public String getName() { + return name; + } + + public Table setName(String name) { + this.name = name; + return this; + } + + public String getDescription() { + return description; + } + + public Table setDescription(String description) { + this.description = description; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/JPAEscapeKeywordTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/JPAEscapeKeywordTest.java new file mode 100644 index 000000000..312f3276e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/JPAEscapeKeywordTest.java @@ -0,0 +1,112 @@ +package com.vladmihalcea.hpjp.hibernate.query.escape; + +import java.util.List; +import jakarta.persistence.*; + +import org.junit.Test; + +import com.vladmihalcea.hpjp.util.AbstractTest; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JPAEscapeKeywordTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Table.class, + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Table() + .setCatalog("library") + .setSchema("public") + .setName("book") + .setDescription("The book table stores book-related info") + ); + }); + + doInJPA(entityManager -> { + List
tables = entityManager.createQuery( + "select t " + + "from Table t " + + "where t.description like '%book%'", Table.class) + .getResultList(); + + assertEquals(1, tables.size()); + }); + } + + @Entity(name = "Table") + @jakarta.persistence.Table(name = "\"table\"") + public static class Table { + + @Id + @GeneratedValue + private Long id; + + @Column(name = "\"catalog\"") + private String catalog; + + @Column(name = "\"schema\"") + private String schema; + + @Column(name = "\"name\"") + private String name; + + @Column(name = "\"desc\"") + private String description; + + public Long getId() { + return id; + } + + public Table setId(Long id) { + this.id = id; + return this; + } + + public String getCatalog() { + return catalog; + } + + public Table setCatalog(String catalog) { + this.catalog = catalog; + return this; + } + + public String getSchema() { + return schema; + } + + public Table setSchema(String schema) { + this.schema = schema; + return this; + } + + public String getName() { + return name; + } + + public Table setName(String name) { + this.name = name; + return this; + } + + public String getDescription() { + return description; + } + + public Table setDescription(String description) { + this.description = description; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/NoEscapeKeywordTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/NoEscapeKeywordTest.java new file mode 100644 index 000000000..33b357770 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/NoEscapeKeywordTest.java @@ -0,0 +1,111 @@ +package com.vladmihalcea.hpjp.hibernate.query.escape; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class NoEscapeKeywordTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Table.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(AvailableSettings.HBM2DDL_HALT_ON_ERROR, Boolean.TRUE); + } + + @Override + public EntityManagerFactory newEntityManagerFactory() { + EntityManagerFactory entityManagerFactory = null; + try { + entityManagerFactory = super.newEntityManagerFactory(); + fail("Should have thrown exception!"); + } catch (Exception expected) { + expected.getMessage(); + } + return entityManagerFactory; + } + + @Test + public void test() { + + } + + @Entity(name = "Table") + public static class Table { + + @Id + @GeneratedValue + private Long id; + + private String catalog; + + private String schema; + + private String name; + + private String description; + + public Long getId() { + return id; + } + + public Table setId(Long id) { + this.id = id; + return this; + } + + public String getCatalog() { + return catalog; + } + + public Table setCatalog(String catalog) { + this.catalog = catalog; + return this; + } + + public String getSchema() { + return schema; + } + + public Table setSchema(String schema) { + this.schema = schema; + return this; + } + + public String getName() { + return name; + } + + public Table setName(String name) { + this.name = name; + return this; + } + + public String getDescription() { + return description; + } + + public Table setDescription(String description) { + this.description = description; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/SQLServerEscapeQuestionCharacterTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/SQLServerEscapeQuestionCharacterTest.java new file mode 100644 index 000000000..d5ddb6359 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/escape/SQLServerEscapeQuestionCharacterTest.java @@ -0,0 +1,95 @@ +package com.vladmihalcea.hpjp.hibernate.query.escape; + +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerEscapeQuestionCharacterTest extends AbstractSQLServerIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + post.setActive(true); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + List posts = entityManager + .createQuery( + "select p " + + "from Post p " + + "where p.active = :active", Post.class) + .setParameter("active", true) + .getResultList(); + + assertEquals(1, posts.size()); + }); + + doInJPA(entityManager -> { + List posts = entityManager + .createNativeQuery( + "select p.title " + + "from [post] p " + + "where p.[active\\?] = :active") + .setParameter("active", true) + .getResultList(); + + assertEquals(1, posts.size()); + }); + } + + @Entity(name = "Post") + @Table(name = "[post]") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "[active?]") + private boolean active; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/DateTruncTimeZoneFunctionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/DateTruncTimeZoneFunctionTest.java new file mode 100644 index 000000000..7e2ac89ed --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/DateTruncTimeZoneFunctionTest.java @@ -0,0 +1,152 @@ +package com.vladmihalcea.hpjp.hibernate.query.function; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.boot.MetadataBuilder; +import org.hibernate.boot.spi.MetadataBuilderContributor; +import org.hibernate.query.sqm.function.NamedSqmFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.junit.Ignore; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DateTruncTimeZoneFunctionTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.metadata_builder_contributor", + SqlFunctionsMetadataBuilderContributor.class + ); + } + + public static class SqlFunctionsMetadataBuilderContributor + implements MetadataBuilderContributor { + + @Override + public void contribute(MetadataBuilder metadataBuilder) { + metadataBuilder.applySqlFunction( + "date_trunc", + DateTruncFunction.INSTANCE + ); + } + + public static class DateTruncFunction extends NamedSqmFunctionDescriptor { + + public static final DateTruncFunction INSTANCE = new DateTruncFunction(); + + public DateTruncFunction() { + super( + "date_trunc", + false, + StandardArgumentsValidators.exactly(2), + null + ); + } + + public void render(SqlAppender sqlAppender, List arguments, SqlAstTranslator walker) { + Expression timestamp = (Expression) arguments.get(0); + Expression timezone = (Expression) arguments.get(1); + sqlAppender.appendSql("date_trunc('day', ("); + walker.render(timestamp, SqlAstNodeRenderingMode.DEFAULT); + sqlAppender.appendSql(" AT TIME ZONE "); + walker.render(timezone, SqlAstNodeRenderingMode.DEFAULT); + sqlAppender.appendSql("))"); + } + } + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + post.setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2018, 11, 23, 11, 22, 33))); + + entityManager.persist(post); + }); + } + + @Test + @Ignore("Doesn't work on Hibernate 6.2. Workaround needed.") + public void test() { + doInJPA(entityManager -> { + Tuple tuple = entityManager.createQuery(""" + select + p.title as title, + date_trunc(p.createdOn, :timezone) as creation_date + from + Post p + where + p.id = :postId + """, Tuple.class) + .setParameter("postId", 1L) + .setParameter("timezone", "UTC") + .getSingleResult(); + + assertEquals("High-Performance Java Persistence", tuple.get("title")); + assertEquals(Timestamp.valueOf(LocalDateTime.of(2018, 11, 23, 0, 0, 0)), tuple.get("creation_date")); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private Timestamp createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Timestamp getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Timestamp createdOn) { + this.createdOn = createdOn; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/DateTruncUtcFunctionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/DateTruncUtcFunctionTest.java new file mode 100644 index 000000000..66a863ddc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/DateTruncUtcFunctionTest.java @@ -0,0 +1,144 @@ +package com.vladmihalcea.hpjp.hibernate.query.function; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.boot.MetadataBuilder; +import org.hibernate.boot.spi.MetadataBuilderContributor; +import org.hibernate.query.sqm.function.NamedSqmFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.junit.Ignore; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DateTruncUtcFunctionTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.metadata_builder_contributor", + SqlFunctionsMetadataBuilderContributor.class + ); + } + + public static class SqlFunctionsMetadataBuilderContributor + implements MetadataBuilderContributor { + + @Override + public void contribute(MetadataBuilder metadataBuilder) { + metadataBuilder.applySqlFunction( + "date_trunc", + DateTruncFunction.INSTANCE + ); + } + + public static class DateTruncFunction extends NamedSqmFunctionDescriptor { + + public static final DateTruncFunction INSTANCE = new DateTruncFunction(); + + public DateTruncFunction() { + super( + "date_trunc", + false, + StandardArgumentsValidators.exactly(1), + null + ); + } + + public void render(SqlAppender sqlAppender, List arguments, SqlAstTranslator walker) { + Expression timestamp = (Expression) arguments.get(0); + sqlAppender.appendSql("date_trunc('day', ("); + walker.render(timestamp, SqlAstNodeRenderingMode.DEFAULT); + sqlAppender.appendSql(" AT TIME ZONE 'UTC'))"); + } + } + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + post.setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2018, 11, 23, 11, 22, 33))); + + entityManager.persist(post); + }); + } + + @Test + @Ignore("Doesn't work on Hibernate 6.2. Workaround needed.") + public void test() { + doInJPA(entityManager -> { + Tuple tuple = entityManager + .createQuery( + "select p.title as title, date_trunc(p.createdOn) as creation_date " + + "from Post p " + + "where p.id = :postId", Tuple.class) + .setParameter("postId", 1L) + .getSingleResult(); + + assertEquals("High-Performance Java Persistence", tuple.get("title")); + assertEquals(Timestamp.valueOf(LocalDateTime.of(2018, 11, 23, 0, 0, 0)), tuple.get("creation_date")); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private Timestamp createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Timestamp getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Timestamp createdOn) { + this.createdOn = createdOn; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/GroupConcatFunctionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/GroupConcatFunctionTest.java new file mode 100644 index 000000000..685108a87 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/GroupConcatFunctionTest.java @@ -0,0 +1,188 @@ +package com.vladmihalcea.hpjp.hibernate.query.function; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import jakarta.persistence.criteria.*; +import org.hibernate.query.Query; +import org.hibernate.transform.Transformers; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class GroupConcatFunctionTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + Tag.class, + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + + Tag jdbc = new Tag(); + jdbc.setName("JDBC"); + post.getTags().add(jdbc); + + Tag hibernate = new Tag(); + hibernate.setName("Hibernate"); + post.getTags().add(hibernate); + + Tag jooq = new Tag(); + jooq.setName("jOOQ"); + post.getTags().add(jooq); + + entityManager.persist(post); + }); + } + + @Test + public void testGroupConcatNaiveQuery() { + doInJPA(entityManager -> { + List postSummaries = entityManager.createNativeQuery(""" + select p.id, p.title, group_concat(t.name) + from Post p + left join post_tag pt on p.id = pt.post_id + left join tag t on t.id = pt.tag_id + group by p.id, p.title + """) + .getResultList(); + + assertEquals(1, postSummaries.size()); + }); + } + + @Test + public void testGroupConcatJPQLQuery() { + doInJPA(entityManager -> { + List postSummaries = entityManager.createQuery(""" + select + p.id as id, + p.title as title, + group_concat(t.name) as tags + from Post p + left join p.tags t + group by p.id, p.title + """) + .unwrap(Query.class) + .setTupleTransformer(Transformers.aliasToBean(PostSummaryDTO.class)) + .getResultList(); + + assertEquals(1, postSummaries.size()); + LOGGER.info("Post tags: {}", postSummaries.get(0).getTags()); + }); + } + + @Test + public void testGroupConcatCriteriaAPIQuery() { + doInJPA(entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createQuery( + PostSummaryDTO.class + ); + + Root post = cq.from(Post.class); + Join tags = post.join("tags", JoinType.LEFT); + cq.groupBy(post.get("id"), post.get("title")); + + cq.select( + cb.construct( + PostSummaryDTO.class, + post.get("id"), + post.get("title"), + cb.function( + "group_concat", + String.class, + tags.get("name") + ) + ) + ); + + List postSummaries = entityManager.createQuery(cq) + .getResultList(); + + assertEquals(1, postSummaries.size()); + LOGGER.info("Post tags: {}", postSummaries.get(0).getTags()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @ManyToMany(cascade = CascadeType.PERSIST) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private Set tags = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Set getTags() { + return tags; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/PostSummaryDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/PostSummaryDTO.java new file mode 100644 index 000000000..46cf127cc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/PostSummaryDTO.java @@ -0,0 +1,46 @@ +package com.vladmihalcea.hpjp.hibernate.query.function; + +/** + * @author Vlad Mihalcea + */ +public class PostSummaryDTO { + + private Long id; + + private String title; + + private String tags; + + public PostSummaryDTO() { + } + + public PostSummaryDTO(Long id, String title, String tags) { + this.id = id; + this.title = title; + this.tags = tags; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getTags() { + return tags; + } + + public void setTags(String tags) { + this.tags = tags; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/ToDateFunctionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/ToDateFunctionTest.java new file mode 100644 index 000000000..524bfa2be --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/function/ToDateFunctionTest.java @@ -0,0 +1,88 @@ +package com.vladmihalcea.hpjp.hibernate.query.function; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.LocalDate; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ToDateFunctionTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + post.setCreatedOn(LocalDate.of(2018, 11, 23).toString()); + + entityManager.persist(post); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Tuple tuple = entityManager + .createQuery( + "select p.title as title, TO_DATE(p.createdOn, 'YYYY-MM-dd') " + + "from Post p " + + "where p.id = :postId", Tuple.class) + .setParameter("postId", 1L) + .getSingleResult(); + + assertEquals("High-Performance Java Persistence", tuple.get("title")); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private String createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(String createdOn) { + this.createdOn = createdOn; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/AbstractTreeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/AbstractTreeTest.java similarity index 93% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/AbstractTreeTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/AbstractTreeTest.java index 92816d37e..92d39156c 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/AbstractTreeTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/AbstractTreeTest.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.hierarchical; +package com.vladmihalcea.hpjp.hibernate.query.hierarchical; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/PostComment.java new file mode 100644 index 000000000..051102309 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/PostComment.java @@ -0,0 +1,65 @@ +package com.vladmihalcea.hpjp.hibernate.query.hierarchical; + +import com.vladmihalcea.hpjp.spring.data.recursive.domain.PostCommentDTO; +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +public class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne + @JoinColumn(name = "parent_id") + private PostComment parent; + + private String description; + + @Enumerated(EnumType.STRING) + private Status status; + + @Transient + private List children = new ArrayList<>(); + + public PostComment() { + } + + public PostComment(String value, Status status) { + this.description = value; + this.status = status; + } + + public PostComment getParent() { + return parent; + } + + public String getDescription() { + return description; + } + + public Status getStatus() { + return status; + } + + public List getChildren() { + return children; + } + + public void addChild(PostComment child) { + children.add(child); + child.parent = this; + } + + public PostComment getRoot() { + if(parent != null) { + return parent.getRoot(); + } + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/PostCommentTreeTransformer.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/PostCommentTreeTransformer.java similarity index 93% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/PostCommentTreeTransformer.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/PostCommentTreeTransformer.java index 5bcd60ae5..12406568e 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/PostCommentTreeTransformer.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/PostCommentTreeTransformer.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.hierarchical; +package com.vladmihalcea.hpjp.hibernate.query.hierarchical; import org.hibernate.transform.ResultTransformer; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/PostCommentTreeTupleTransformer.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/PostCommentTreeTupleTransformer.java new file mode 100644 index 000000000..c775d2871 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/PostCommentTreeTupleTransformer.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.hibernate.query.hierarchical; + +import org.hibernate.query.TupleTransformer; +import org.hibernate.transform.ResultTransformer; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentTreeTupleTransformer implements TupleTransformer { + + public static final PostCommentTreeTupleTransformer INSTANCE = new PostCommentTreeTupleTransformer(); + + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + PostComment comment = (PostComment) tuple[0]; + if (comment.getParent() != null) { + comment.getParent().addChild(comment); + } + return comment.getRoot(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/Status.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/Status.java new file mode 100644 index 000000000..e4fbfa4c2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/Status.java @@ -0,0 +1,10 @@ +package com.vladmihalcea.hpjp.hibernate.query.hierarchical; + +/** + * @author Vlad Mihalcea + */ +public enum Status { + + APPROVED, + PENDING +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/TreeCTETest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/TreeCTETest.java new file mode 100644 index 000000000..44e564ca5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/TreeCTETest.java @@ -0,0 +1,51 @@ +package com.vladmihalcea.hpjp.hibernate.query.hierarchical; + +import io.hypersistence.utils.hibernate.query.DistinctListTransformer; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import java.util.List; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.PostgreSQLDataSourceProvider; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class TreeCTETest extends AbstractTreeTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider(); + } + + @Test + public void test() { + List comments = doInJPA(entityManager -> { + return (List) entityManager.createNativeQuery(""" + WITH RECURSIVE comment_tree(id, parent_id, description, status) AS ( + SELECT c.id, c.parent_id, c.description, status + FROM PostComment c + WHERE LOWER(c.description) LIKE :token AND c.status = :status + UNION ALL + SELECT c.id, c.parent_id, c.description, c.status + FROM PostComment c + INNER JOIN comment_tree ct on ct.id = c.parent_id + WHERE c.status = :status + ) + SELECT id, parent_id, description, status + FROM comment_tree + """) + .setParameter("status", Status.APPROVED.name()) + .setParameter("token", "high-performance%") + .unwrap(NativeQuery.class) + .addEntity(PostComment.class) + .setTupleTransformer(PostCommentTreeTupleTransformer.INSTANCE) + .setResultListTransformer(DistinctListTransformer.INSTANCE) + .getResultList(); + }); + assertEquals(1, comments.size()); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/TreeConnectByTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/TreeConnectByTest.java similarity index 79% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/TreeConnectByTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/TreeConnectByTest.java index 0b52c11e4..0e6a95336 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/hierarchical/TreeConnectByTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/TreeConnectByTest.java @@ -1,12 +1,12 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.hierarchical; +package com.vladmihalcea.hpjp.hibernate.query.hierarchical; -import org.hibernate.SQLQuery; +import org.hibernate.query.NativeQuery; import org.junit.Test; import java.util.List; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.OracleDataSourceProvider; import static org.junit.Assert.assertEquals; @@ -32,7 +32,7 @@ public void test() { "START WITH c.parent_id IS NULL AND lower(c.description) like :token ") .setParameter("status", Status.APPROVED.name()) .setParameter("token", "high-performance%") - .unwrap(SQLQuery.class) + .unwrap(NativeQuery.class) .addEntity(PostComment.class) .setResultTransformer(PostCommentTreeTransformer.INSTANCE) .list(); diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/TreeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/TreeTest.java new file mode 100644 index 000000000..5e2a4bf13 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/hierarchical/TreeTest.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.hpjp.hibernate.query.hierarchical; + +import org.hibernate.*; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class TreeTest extends AbstractTreeTest { + + @Test + public void test() { + + List comments = doInJPA(entityManager -> { + return (List) entityManager + .unwrap(Session.class) + .createQuery( + "SELECT c " + + "FROM PostComment c " + + "WHERE c.status = :status") + .setParameter("status", Status.APPROVED) + .setResultTransformer(PostCommentTreeTransformer.INSTANCE) + .getResultList(); + }); + assertEquals(2, comments.size()); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/SQLInnerJoinTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/SQLInnerJoinTest.java new file mode 100644 index 000000000..1df4590e3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/SQLInnerJoinTest.java @@ -0,0 +1,353 @@ +package com.vladmihalcea.hpjp.hibernate.query.join; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SQLInnerJoinTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + /** + * post + * ---- + * + * | id | title | + * |----|-----------| + * | 1 | Java | + * | 2 | Hibernate | + * | 3 | JPA | + * + * post_comment + * ------------- + * + * | id | review | p.id | + * |----|-----------|---------| + * | 1 | Good | 1 | + * | 2 | Excellent | 1 | + * | 3 | Awesome | 2 | + */ + @Override + public void afterInit() { + doInJPA(entityManager -> { + + Post post1 = new Post() + .setId(1L) + .setTitle("Java"); + + entityManager.persist(post1); + + Post post2 = new Post() + .setId(2L) + .setTitle("Hibernate"); + + entityManager.persist(post2); + + Post post3 = new Post() + .setId(3L) + .setTitle("JPA"); + + entityManager.persist(post3); + + entityManager.persist( + new PostComment() + .setId(1L) + .setReview("Good") + .setPost(post1) + ); + + entityManager.persist( + new PostComment() + .setId(2L) + .setReview("Excellent") + .setPost(post1) + ); + + entityManager.persist( + new PostComment() + .setId(3L) + .setReview("Awesome") + .setPost(post2) + ); + }); + } + + /** + * Get all posts with their associated post_comments when the condition is always false. + * + * SELECT + * p.id AS "p.id", + * pc.id AS "pc.id" + * FROM post p + * INNER JOIN post_comment pc ON 1 = 0 + * + * | p.id | pc.id | + * |---------|------------| + */ + @Test + public void testInnerJoinOnFalse() { + doInJPA(entityManager -> { + List tuples = entityManager.createNativeQuery(""" + SELECT + p.id AS "p.id", + pc.id AS "pc.id" + FROM post p + INNER JOIN post_comment pc ON 1 = 0 + """, Tuple.class) + .getResultList(); + + assertEquals(0, tuples.size()); + }); + } + + /** + * Get all posts with their associated post_comments when the condition is always true. + * + * SELECT + * p.id AS "p.id", + * pc.id AS "pc.id" + * FROM post p + * INNER JOIN post_comment pc ON 1 = 1 + * ORDER BY p.id, pc.id + * + * | p.id | pc.id | + * |---------|------------| + * | 1 | 1 | + * | 1 | 2 | + * | 1 | 3 | + * | 2 | 1 | + * | 2 | 2 | + * | 2 | 3 | + * | 3 | 1 | + * | 3 | 2 | + * | 3 | 3 | + */ + @Test + public void testInnerJoinOnTrue() { + doInJPA(entityManager -> { + List tuples = entityManager.createNativeQuery(""" + SELECT + p.id AS "p.id", + pc.id AS "pc.id" + FROM post p + INNER JOIN post_comment pc ON 1 = 1 + ORDER BY p.id, pc.id + """, Tuple.class) + .getResultList(); + + assertEquals(3 * 3, tuples.size()); + + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + Tuple tuple = tuples.get(i * 3 + j); + + assertEquals((i + 1), intValue(tuple.get("p.id"))); + assertEquals((j + 1), intValue(tuple.get("pc.id"))); + } + } + }); + } + + /** + * Get all posts with their associated post_comments. + * + * SELECT + * p.id AS "p.id", + * pc.post_id AS "pc.post_id", + * pc.id AS "pc.id", + * p.title AS "p.title", + * pc.review AS "pc.review" + * FROM post p + * INNER JOIN post_comment pc ON pc.post_id = p.id + * ORDER BY p.id, pc.id + * + * | p.id | pc.post_id | pc.id | p.title | pc.review | + * |---------|------------|------------|------------|-----------| + * | 1 | 1 | 1 | Java | Good | + * | 1 | 1 | 2 | Java | Excellent | + * | 2 | 2 | 3 | Hibernate | Awesome | + */ + @Test + public void testInnerJoin() { + doInJPA(entityManager -> { + List tuples = entityManager.createNativeQuery(""" + SELECT + p.id AS "p.id", + pc.post_id AS "pc.post_id", + pc.id AS "pc.id", + p.title AS "p.title", + pc.review AS "pc.review" + FROM post p + INNER JOIN post_comment pc ON pc.post_id = p.id + ORDER BY p.id, pc.id + """, Tuple.class) + .getResultList(); + + Tuple tuple1 = tuples.get(0); + assertEquals(1L, longValue(tuple1.get("p.id"))); + assertEquals(1L, longValue(tuple1.get("pc.post_id"))); + assertEquals(1L, longValue(tuple1.get("pc.id"))); + assertEquals("Java", tuple1.get("p.title")); + assertEquals("Good", tuple1.get("pc.review")); + + Tuple tuple2 = tuples.get(1); + assertEquals(1L, longValue(tuple2.get("p.id"))); + assertEquals(1L, longValue(tuple2.get("pc.post_id"))); + assertEquals(2L, longValue(tuple2.get("pc.id"))); + assertEquals("Java", tuple2.get("p.title")); + assertEquals("Excellent", tuple2.get("pc.review")); + + Tuple tuple3 = tuples.get(2); + assertEquals(2L, longValue(tuple3.get("p.id"))); + assertEquals(2L, longValue(tuple3.get("pc.post_id"))); + assertEquals(3L, longValue(tuple3.get("pc.id"))); + assertEquals("Hibernate", tuple3.get("p.title")); + assertEquals("Awesome", tuple3.get("pc.review")); + }); + } + + /** + * Get all posts with their associated post_comments. + * + * SELECT + * p.id AS "p.id", + * pc.post_id AS "pc.post_id", + * pc.id AS "pc.id", + * p.title AS "p.title", + * pc.review AS "pc.review" + * FROM post p, post_comment pc + * WHERE pc.post_id = p.id + * ORDER BY p.id, pc.id + * + * | p.id | pc.post_id | pc.id | p.title | pc.review | + * |---------|------------|------------|------------|-----------| + * | 1 | 1 | 1 | Java | Good | + * | 1 | 1 | 2 | Java | Excellent | + * | 2 | 2 | 3 | Hibernate | Awesome | + */ + @Test + public void testThetaStyleInnerJoin() { + doInJPA(entityManager -> { + List tuples = entityManager.createNativeQuery(""" + SELECT + p.id AS "p.id", + pc.post_id AS "pc.post_id", + pc.id AS "pc.id", + p.title AS "p.title", + pc.review AS "pc.review" + FROM post p, post_comment pc + WHERE pc.post_id = p.id + """, Tuple.class) + .getResultList(); + + Tuple tuple1 = tuples.get(0); + assertEquals(1L, longValue(tuple1.get("p.id"))); + assertEquals(1L, longValue(tuple1.get("pc.post_id"))); + assertEquals(1L, longValue(tuple1.get("pc.id"))); + assertEquals("Java", tuple1.get("p.title")); + assertEquals("Good", tuple1.get("pc.review")); + + Tuple tuple2 = tuples.get(1); + assertEquals(1L, longValue(tuple2.get("p.id"))); + assertEquals(1L, longValue(tuple2.get("pc.post_id"))); + assertEquals(2L, longValue(tuple2.get("pc.id"))); + assertEquals("Java", tuple2.get("p.title")); + assertEquals("Excellent", tuple2.get("pc.review")); + + Tuple tuple3 = tuples.get(2); + assertEquals(2L, longValue(tuple3.get("p.id"))); + assertEquals(2L, longValue(tuple3.get("pc.post_id"))); + assertEquals(3L, longValue(tuple3.get("pc.id"))); + assertEquals("Hibernate", tuple3.get("p.title")); + assertEquals("Awesome", tuple3.get("pc.review")); + }); + + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/HashJoinSqlTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/HashJoinSqlTest.java new file mode 100644 index 000000000..f6bb62723 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/HashJoinSqlTest.java @@ -0,0 +1,178 @@ +package com.vladmihalcea.hpjp.hibernate.query.join.algorithm; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class HashJoinSqlTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + /** + * post + * ---- + * + * | id | title | + * |----|------------| + * | 1 | Post no. 1 | + * | 2 | Post no. 2 | + * | .. | .. | + * + * post_comment + * ------------- + * + * | id | review | post_id | + * |----|---------------|---------| + * | 1 | Comment no. 1 | 1 | + * | 2 | Comment no. 2 | 1 | + * | .. | .. | .. | + */ + @Override + public void afterInit() { + doInJPA(entityManager -> { + int postCount = 1000; + int postCommentCount = 10; + long postCommentId = 1; + + for (long postId = 1; postId <= postCount; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post no. %d", postId)); + entityManager.persist(post); + + for (int i = 0; i < postCommentCount; i++) { + entityManager.persist( + new PostComment() + .setId(postCommentId++) + .setReview(String.format("Comment no. %d", postCommentId)) + .setPost(post) + ); + } + } + }); + executeStatement("CREATE INDEX IDX_post_id ON post (id)"); + executeStatement("CREATE INDEX IDX_post_comment_id ON post_comment (id)"); + executeStatement("CREATE INDEX IDX_post_comment_post_id ON post_comment (post_id)"); + executeStatement("VACUUM ANALYZE"); + } + + /** + * Get all posts with their associated post_comments. + * + * EXPLAIN ANALYZE + * SELECT + * p.id AS post_id, + * p.title AS post_title, + * pc.review AS review + * FROM post p + * INNER JOIN post_comment pc ON pc.post_id = p.id + */ + @Test + public void testInnerJoin() { + doInJPA(entityManager -> { + List planLines = entityManager + .unwrap(Session.class) + .doReturningWork(connection -> selectColumnList( + connection, + "EXPLAIN ANALYZE\n" + + "SELECT\n" + + " p.id AS post_id,\n" + + " p.title AS post_title,\n" + + " pc.review AS review\n" + + "FROM post p\n" + + "INNER JOIN post_comment pc ON pc.post_id = p.id\n", + String.class) + ); + + assertTrue(planLines.size() > 1); + + LOGGER.info("Execution plan: {}{}", + System.lineSeparator(), + planLines.stream().collect(Collectors.joining(System.lineSeparator())) + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/HashJoinTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/HashJoinTest.java new file mode 100644 index 000000000..27beb3c21 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/HashJoinTest.java @@ -0,0 +1,193 @@ +package com.vladmihalcea.hpjp.hibernate.query.join.algorithm; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class HashJoinTest { + + private List posts = new ArrayList<>(); + + private List postComments = new ArrayList<>(); + + /** + * post + * ---- + * + * | id | title | + * |----|-----------| + * | 1 | Java | + * | 2 | Hibernate | + * | 3 | JPA | + * + * post_comment + * ------------- + * + * | id | review | post_id | + * |----|-----------|---------| + * | 1 | Good | 1 | + * | 2 | Excellent | 1 | + * | 3 | Awesome | 2 | + */ + public HashJoinTest() { + posts.add( + new Post() + .setId(1L) + .setTitle("Java") + ); + + posts.add( + new Post() + .setId(2L) + .setTitle("Hibernate") + ); + + posts.add( + new Post() + .setId(3L) + .setTitle("JPA") + ); + + postComments.add( + new PostComment() + .setId(1L) + .setReview("Good") + .setPost(1L) + ); + + postComments.add( + new PostComment() + .setId(2L) + .setReview("Excellent") + .setPost(1L) + ); + + postComments.add( + new PostComment() + .setId(3L) + .setReview("Awesome") + .setPost(2L) + ); + } + + /** + * Get all posts with their associated post_comments. + * + * + * | post_id | post_title | review | + * |---------|------------|-----------| + * | 1 | Java | Good | + * | 1 | Java | Excellent | + * | 2 | Hibernate | Awesome | + */ + @Test + public void testInnerJoin() { + + Map postMap = new HashMap<>(); + + for (Post post : posts) { + postMap.put(post.getId(), post); + } + + List tuples = new ArrayList<>(); + + for (PostComment postComment : postComments) { + Long postId = postComment.getPostId(); + Post post = postMap.get(postId); + + if (post != null) { + tuples.add( + new Tuple() + .add("post_id", postComment.getPostId()) + .add("post_title", post.getTitle()) + .add("review", postComment.getReview()) + ); + } + } + + Tuple tuple1 = tuples.get(0); + assertEquals(1L, tuple1.getLong("post_id")); + assertEquals("Java", tuple1.get("post_title")); + assertEquals("Good", tuple1.get("review")); + + Tuple tuple2 = tuples.get(1); + assertEquals(1L, tuple2.getLong("post_id")); + assertEquals("Java", tuple2.get("post_title")); + assertEquals("Excellent", tuple2.get("review")); + + Tuple tuple3 = tuples.get(2); + assertEquals(2L, tuple3.getLong("post_id")); + assertEquals("Hibernate", tuple3.get("post_title")); + assertEquals("Awesome", tuple3.get("review")); + } + + public static class Post { + + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + public static class PostComment { + + private Long id; + + private String review; + + private Long postId; + + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Long getPostId() { + return postId; + } + + public PostComment setPost(Long postId) { + this.postId = postId; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/MergeJoinSqlTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/MergeJoinSqlTest.java new file mode 100644 index 000000000..112ca04f3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/MergeJoinSqlTest.java @@ -0,0 +1,182 @@ +package com.vladmihalcea.hpjp.hibernate.query.join.algorithm; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class MergeJoinSqlTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + /** + * post + * ---- + * + * | id | title | + * |----|------------| + * | 1 | Post no. 1 | + * | 2 | Post no. 2 | + * | .. | .. | + * + * post_comment + * ------------- + * + * | id | review | post_id | + * |----|---------------|---------| + * | 1 | Comment no. 1 | 1 | + * | 2 | Comment no. 2 | 1 | + * | .. | .. | .. | + */ + @Override + public void afterInit() { + doInJPA(entityManager -> { + int postCount = 1000; + int postCommentCount = 10; + long postCommentId = 1; + + for (long postId = 1; postId <= postCount; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post no. %d", postId)); + entityManager.persist(post); + + for (int i = 0; i < postCommentCount; i++) { + entityManager.persist( + new PostComment() + .setId(postCommentId++) + .setReview(String.format("Comment no. %d", postCommentId)) + .setPost(post) + ); + } + } + }); + executeStatement("CREATE INDEX IDX_post_id ON post (id)"); + executeStatement("CREATE INDEX IDX_post_comment_id ON post_comment (id)"); + executeStatement("CREATE INDEX IDX_post_comment_post_id ON post_comment (post_id)"); + executeStatement("VACUUM ANALYZE"); + } + + /** + * Get all posts with their associated post_comments. + * + * EXPLAIN ANALYZE + * SELECT + * p.id AS post_id, + * p.title AS post_title, + * pc.review AS review + * FROM post p + * INNER JOIN post_comment pc ON pc.post_id = p.id + * ORDER BY pc.post_id + */ + @Test + public void testInnerJoin() { + doInJPA(entityManager -> { + List planLines = entityManager + .unwrap(Session.class) + .doReturningWork(connection -> selectColumnList( + connection, """ + EXPLAIN ANALYZE + SELECT + p.id AS post_id, + p.title AS post_title, + pc.review AS review + FROM post p + INNER JOIN post_comment pc ON pc.post_id = p.id + ORDER BY pc.post_id + """, + String.class + ) + ); + + assertTrue(planLines.size() > 1); + + LOGGER.info("Execution plan: {}{}", + System.lineSeparator(), + planLines.stream().collect(Collectors.joining(System.lineSeparator())) + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/MergeJoinTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/MergeJoinTest.java new file mode 100644 index 000000000..50749b7af --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/MergeJoinTest.java @@ -0,0 +1,198 @@ +package com.vladmihalcea.hpjp.hibernate.query.join.algorithm; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MergeJoinTest { + + private List posts = new ArrayList<>(); + + private List postComments = new ArrayList<>(); + + /** + * post + * ---- + * + * | id | title | + * |----|-----------| + * | 1 | Java | + * | 2 | Hibernate | + * | 3 | JPA | + * + * post_comment + * ------------- + * + * | id | review | post_id | + * |----|-----------|---------| + * | 1 | Good | 1 | + * | 2 | Excellent | 1 | + * | 3 | Awesome | 2 | + */ + public MergeJoinTest() { + posts.add( + new Post() + .setId(1L) + .setTitle("Java") + ); + + posts.add( + new Post() + .setId(2L) + .setTitle("Hibernate") + ); + + posts.add( + new Post() + .setId(3L) + .setTitle("JPA") + ); + + postComments.add( + new PostComment() + .setId(1L) + .setReview("Good") + .setPost(1L) + ); + + postComments.add( + new PostComment() + .setId(2L) + .setReview("Excellent") + .setPost(1L) + ); + + postComments.add( + new PostComment() + .setId(3L) + .setReview("Awesome") + .setPost(2L) + ); + } + + /** + * Get all posts with their associated post_comments. + * + * + * | post_id | post_title | review | + * |---------|------------|-----------| + * | 1 | Java | Good | + * | 1 | Java | Excellent | + * | 2 | Hibernate | Awesome | + */ + @Test + public void testInnerJoin() { + posts.sort(Comparator.comparing(Post::getId)); + + postComments.sort((pc1, pc2) -> { + int result = Comparator.comparing(PostComment::getPostId).compare(pc1, pc2); + return result != 0 ? result : + Comparator.comparing(PostComment::getId).compare(pc1, pc2); + }); + + List tuples = new ArrayList<>(); + int postCount = posts.size(), postCommentCount = postComments.size(); + int i = 0, j = 0; + + while(i < postCount && j < postCommentCount) { + Post post = posts.get(i); + PostComment postComment = postComments.get(j); + + if(post.getId().equals(postComment.getPostId())) { + tuples.add( + new Tuple() + .add("post_id", postComment.getPostId()) + .add("post_title", post.getTitle()) + .add("review", postComment.getReview()) + ); + j++; + } else { + i++; + } + } + + Tuple tuple1 = tuples.get(0); + assertEquals(1L, tuple1.getLong("post_id")); + assertEquals("Java", tuple1.get("post_title")); + assertEquals("Good", tuple1.get("review")); + + Tuple tuple2 = tuples.get(1); + assertEquals(1L, tuple2.getLong("post_id")); + assertEquals("Java", tuple2.get("post_title")); + assertEquals("Excellent", tuple2.get("review")); + + Tuple tuple3 = tuples.get(2); + assertEquals(2L, tuple3.getLong("post_id")); + assertEquals("Hibernate", tuple3.get("post_title")); + assertEquals("Awesome", tuple3.get("review")); + } + + public static class Post { + + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + public static class PostComment { + + private Long id; + + private String review; + + private Long postId; + + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Long getPostId() { + return postId; + } + + public PostComment setPost(Long postId) { + this.postId = postId; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/NestedLoopsSqlTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/NestedLoopsSqlTest.java new file mode 100644 index 000000000..46429186c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/NestedLoopsSqlTest.java @@ -0,0 +1,182 @@ +package com.vladmihalcea.hpjp.hibernate.query.join.algorithm; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class NestedLoopsSqlTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + /** + * post + * ---- + * + * | id | title | + * |----|------------| + * | 1 | Post no. 1 | + * | 2 | Post no. 2 | + * | .. | .. | + * + * post_comment + * ------------- + * + * | id | review | post_id | + * |----|---------------|---------| + * | 1 | Comment no. 1 | 1 | + * | 2 | Comment no. 2 | 1 | + * | .. | .. | .. | + */ + @Override + public void afterInit() { + doInJPA(entityManager -> { + int postCount = 1000; + int postCommentCount = 10; + long postCommentId = 1; + + for (long postId = 1; postId <= postCount; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post no. %d", postId)); + entityManager.persist(post); + + for (int i = 0; i < postCommentCount; i++) { + entityManager.persist( + new PostComment() + .setId(postCommentId++) + .setReview(String.format("Comment no. %d", postCommentId)) + .setPost(post) + ); + } + } + }); + executeStatement("CREATE INDEX IDX_post_id ON post (id)"); + executeStatement("CREATE INDEX IDX_post_comment_id ON post_comment (id)"); + executeStatement("CREATE INDEX IDX_post_comment_post_id ON post_comment (post_id)"); + executeStatement("VACUUM ANALYZE"); + } + + /** + * Get all posts with their associated post_comments. + * + * EXPLAIN ANALYZE + * SELECT + * p.id AS post_id, + * p.title AS post_title, + * pc.review AS review + * FROM post p + * INNER JOIN post_comment pc ON pc.post_id = p.id + * WHERE p.id BETWEEN 1 AND 10 + */ + @Test + public void testInnerJoin() { + doInJPA(entityManager -> { + List planLines = entityManager + .unwrap(Session.class) + .doReturningWork(connection -> selectColumnList( + connection, + """ + EXPLAIN ANALYZE + SELECT + p.id AS post_id, + p.title AS post_title, + pc.review AS review + FROM post p + INNER JOIN post_comment pc ON pc.post_id = p.id + WHERE p.id BETWEEN 1 AND 10 + """, + String.class) + ); + + assertTrue(planLines.size() > 1); + + LOGGER.info("Execution plan: {}{}", + System.lineSeparator(), + planLines.stream().collect(Collectors.joining(System.lineSeparator())) + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/NestedLoopsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/NestedLoopsTest.java new file mode 100644 index 000000000..4980367c5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/NestedLoopsTest.java @@ -0,0 +1,243 @@ +package com.vladmihalcea.hpjp.hibernate.query.join.algorithm; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class NestedLoopsTest { + + private List posts = new ArrayList<>(); + + private List postComments = new ArrayList<>(); + + /** + * post + * ---- + * + * | id | title | + * |----|-----------| + * | 1 | Java | + * | 2 | Hibernate | + * | 3 | JPA | + * + * post_comment + * ------------- + * + * | id | review | post_id | + * |----|-----------|---------| + * | 1 | Good | 1 | + * | 2 | Excellent | 1 | + * | 3 | Awesome | 2 | + */ + public NestedLoopsTest() { + posts.add( + new Post() + .setId(1L) + .setTitle("Java") + ); + + posts.add( + new Post() + .setId(2L) + .setTitle("Hibernate") + ); + + posts.add( + new Post() + .setId(3L) + .setTitle("JPA") + ); + + postComments.add( + new PostComment() + .setId(1L) + .setReview("Good") + .setPost(1L) + ); + + postComments.add( + new PostComment() + .setId(2L) + .setReview("Excellent") + .setPost(1L) + ); + + postComments.add( + new PostComment() + .setId(3L) + .setReview("Awesome") + .setPost(2L) + ); + } + + /** + * Get all posts with their associated post_comments. + * + * + * | post_id | post_title | review | + * |---------|------------|-----------| + * | 1 | Java | Good | + * | 1 | Java | Excellent | + * | 2 | Hibernate | Awesome | + */ + @Test + public void testInnerJoin() { + + List tuples = new ArrayList<>(); + + for (Post post : posts) { + for (PostComment postComment : postComments) { + if(post.getId().equals(postComment.getPostId())) { + tuples.add( + new Tuple() + .add("post_id", postComment.getPostId()) + .add("post_title", post.getTitle()) + .add("review", postComment.getReview()) + ); + } + } + } + + Tuple tuple1 = tuples.get(0); + assertEquals(1L, tuple1.getLong("post_id")); + assertEquals("Java", tuple1.get("post_title")); + assertEquals("Good", tuple1.get("review")); + + Tuple tuple2 = tuples.get(1); + assertEquals(1L, tuple2.getLong("post_id")); + assertEquals("Java", tuple2.get("post_title")); + assertEquals("Excellent", tuple2.get("review")); + + Tuple tuple3 = tuples.get(2); + assertEquals(2L, tuple3.getLong("post_id")); + assertEquals("Hibernate", tuple3.get("post_title")); + assertEquals("Awesome", tuple3.get("review")); + } + + /** + * Get all posts with their associated post_comments using Streams. + * + * + * | post_id | post_title | review | + * |---------|------------|-----------| + * | 1 | Java | Good | + * | 1 | Java | Excellent | + * | 2 | Hibernate | Awesome | + */ + @Test + public void testInnerJoinUsingStreams() { + List tuples = posts.stream() + .flatMap(post -> postComments.stream() + .map(postComment -> new Pair(post, postComment)) + ) + .filter(pair -> pair.getPost().getId().equals(pair.getPostComment().getPostId())) + .map(pair -> new Tuple() + .add("post_id", pair.getPostComment().getPostId()) + .add("post_title", pair.getPost().getTitle()) + .add("review", pair.getPostComment().getReview()) + ) + .collect(Collectors.toList()); + + Tuple tuple1 = tuples.get(0); + assertEquals(1L, tuple1.getLong("post_id")); + assertEquals("Java", tuple1.get("post_title")); + assertEquals("Good", tuple1.get("review")); + + Tuple tuple2 = tuples.get(1); + assertEquals(1L, tuple2.getLong("post_id")); + assertEquals("Java", tuple2.get("post_title")); + assertEquals("Excellent", tuple2.get("review")); + + Tuple tuple3 = tuples.get(2); + assertEquals(2L, tuple3.getLong("post_id")); + assertEquals("Hibernate", tuple3.get("post_title")); + assertEquals("Awesome", tuple3.get("review")); + } + + public static class Post { + + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + public static class PostComment { + + private Long id; + + private String review; + + private Long postId; + + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Long getPostId() { + return postId; + } + + public PostComment setPost(Long postId) { + this.postId = postId; + return this; + } + } + + public static class Pair { + private final Post post; + private final PostComment postComment; + + public Pair(Post post, PostComment postComment) { + this.post = post; + this.postComment = postComment; + } + + public Post getPost() { + return post; + } + + public PostComment getPostComment() { + return postComment; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/Tuple.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/Tuple.java new file mode 100644 index 000000000..ff93b1540 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/join/algorithm/Tuple.java @@ -0,0 +1,25 @@ +package com.vladmihalcea.hpjp.hibernate.query.join.algorithm; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class Tuple { + + private final Map valueMap = new HashMap<>(); + + public Tuple add(String alias, Object value) { + valueMap.put(alias, value); + return this; + } + + public E get(String alias) { + return (E) valueMap.get(alias); + } + + public long getLong(String alias) { + return (Long) valueMap.get(alias); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/mapping/ResultSetMappingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/mapping/ResultSetMappingTest.java new file mode 100644 index 000000000..0afc5ac04 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/mapping/ResultSetMappingTest.java @@ -0,0 +1,432 @@ +package com.vladmihalcea.hpjp.hibernate.query.mapping; + +import com.vladmihalcea.hpjp.hibernate.identifier.Identifiable; +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class ResultSetMappingTest extends AbstractOracleIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "25"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + public static final int POST_COUNT = 50; + + public static final int COMMENT_COUNT = 5; + + @Override + public void afterInit() { + doInJPA(entityManager -> { + LocalDateTime timestamp = LocalDateTime.of( + 2016, 10, 9, 12, 0, 0, 0 + ); + + LongStream.rangeClosed(1, POST_COUNT) + .forEach(postId -> { + Post post = new Post() + .setId(postId) + .setTitle( + String.format( + "High-Performance Java Persistence - Chapter %d", + postId + ) + ) + .setCreatedOn( + Timestamp.valueOf(timestamp.plusDays(postId)) + ); + + LongStream.rangeClosed(1, COMMENT_COUNT) + .forEach(commentOffset -> { + long commentId = ((postId - 1) * COMMENT_COUNT) + commentOffset; + + post.addComment( + new PostComment() + .setId(commentId) + .setReview( + String.format("Comment nr. %d - A must-read!", commentId) + ) + .setCreatedOn( + Timestamp.valueOf( + timestamp + .plusDays(postId) + .plusMinutes(commentId) + ) + ) + ); + + }); + + entityManager.persist(post); + }); + }); + } + + @Test + public void testEntityResult() { + doInJPA(entityManager -> { + final int POST_RESULT_COUNT = 5; + + List postAndCommentList = entityManager + .createNamedQuery("PostWithCommentByRank") + .setParameter("titlePattern", "High-Performance Java Persistence %") + .setParameter("rank", POST_RESULT_COUNT) + .getResultList(); + + assertEquals(POST_RESULT_COUNT * COMMENT_COUNT, postAndCommentList.size()); + + for (int i = 0; i < COMMENT_COUNT; i++) { + Post post = (Post) postAndCommentList.get(i)[0]; + PostComment comment = (PostComment) postAndCommentList.get(i)[1]; + + assertTrue(entityManager.contains(post)); + assertTrue(entityManager.contains(comment)); + + assertEquals( + "High-Performance Java Persistence - Chapter 1", + post.getTitle() + ); + + assertEquals( + String.format( + "Comment nr. %d - A must-read!", + i + 1 + ), + comment.getReview() + ); + } + }); + } + + @Test + public void testConstructorResult() { + doInJPA(entityManager -> { + final int POST_RESULT_COUNT = 5; + + List postTitleAndCommentCountList = entityManager + .createNamedQuery("PostTitleWithCommentCount") + .setMaxResults(POST_RESULT_COUNT) + .getResultList(); + + assertEquals(POST_RESULT_COUNT, postTitleAndCommentCountList.size()); + + for (int i = 0; i < POST_RESULT_COUNT; i++) { + PostTitleWithCommentCount postTitleWithCommentCount = postTitleAndCommentCountList.get(i); + + assertEquals( + String.format( + "High-Performance Java Persistence - Chapter %d", + i + 1 + ), + postTitleWithCommentCount.getPostTitle() + ); + + assertEquals(COMMENT_COUNT, postTitleWithCommentCount.getCommentCount()); + } + }); + } + + @Test + public void testColumnResult() { + doInJPA(entityManager -> { + final int POST_RESULT_COUNT = 5; + + List postWithCommentCountList = entityManager + .createNamedQuery("PostWithCommentCount") + .setMaxResults(POST_RESULT_COUNT) + .getResultList(); + + assertEquals(POST_RESULT_COUNT, postWithCommentCountList.size()); + + for (int i = 0; i < POST_RESULT_COUNT; i++) { + Post post = (Post) postWithCommentCountList.get(i)[0]; + int commentCount = (int) postWithCommentCountList.get(i)[1]; + + assertTrue(entityManager.contains(post)); + + assertEquals(i + 1, post.getId().intValue()); + assertEquals( + String.format( + "High-Performance Java Persistence - Chapter %d", + i + 1 + ), + post.getTitle() + ); + + assertEquals(COMMENT_COUNT, commentCount); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + @NamedNativeQuery( + name = "PostWithCommentByRank", + query = """ + SELECT * + FROM ( + SELECT + *, + DENSE_RANK() OVER ( + ORDER BY + "p.created_on", + "p.id" + ) rank + FROM ( + SELECT + p.id AS "p.id", p.created_on AS "p.created_on", + p.title AS "p.title", pc.post_id AS "pc.post_id", + pc.id as "pc.id", pc.created_on AS "pc.created_on", + pc.review AS "pc.review" + FROM post p + LEFT JOIN post_comment pc ON p.id = pc.post_id + WHERE p.title LIKE :titlePattern + ORDER BY p.created_on + ) p_pc + ) p_pc_r + WHERE p_pc_r.rank <= :rank + """, + resultSetMapping = "PostWithCommentByRankMapping" + ) + @SqlResultSetMapping( + name = "PostWithCommentByRankMapping", + entities = { + @EntityResult( + entityClass = Post.class, + fields = { + @FieldResult(name = "id", column = "p.id"), + @FieldResult(name = "createdOn", column = "p.created_on"), + @FieldResult(name = "title", column = "p.title"), + } + ), + @EntityResult( + entityClass = PostComment.class, + fields = { + @FieldResult(name = "id", column = "pc.id"), + @FieldResult(name = "createdOn", column = "pc.created_on"), + @FieldResult(name = "review", column = "pc.review"), + @FieldResult(name = "post", column = "pc.post_id"), + } + ) + } + ) + @NamedNativeQuery( + name = "PostTitleWithCommentCount", + query = """ + SELECT + p.id AS "p.id", + p.title AS "p.title", + COUNT(pc.*) AS "comment_count" + FROM post_comment pc + LEFT JOIN post p ON p.id = pc.post_id + GROUP BY p.id, p.title + ORDER BY p.id + """, + resultSetMapping = "PostTitleWithCommentCountMapping" + ) + @SqlResultSetMapping( + name = "PostTitleWithCommentCountMapping", + classes = { + @ConstructorResult( + columns = { + @ColumnResult(name = "p.title"), + @ColumnResult(name = "comment_count", type = int.class) + }, + targetClass = PostTitleWithCommentCount.class + ) + } + ) + @NamedNativeQuery( + name = "PostWithCommentCount", + query = """ + SELECT + p.id AS "p.id", + p.title AS "p.title", + p.created_on AS "p.created_on", + COUNT(pc.*) AS "comment_count" + FROM post_comment pc + LEFT JOIN post p ON p.id = pc.post_id + GROUP BY p.id, p.title + ORDER BY p.id + """, + resultSetMapping = "PostWithCommentCountMapping" + ) + @SqlResultSetMapping( + name = "PostWithCommentCountMapping", + entities = @EntityResult( + entityClass = Post.class, + fields = { + @FieldResult(name = "id", column = "p.id"), + @FieldResult(name = "createdOn", column = "p.created_on"), + @FieldResult(name = "title", column = "p.title"), + } + ), + columns = @ColumnResult( + name = "comment_count", + type = int.class + ) + ) + public static class Post implements Identifiable { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private Timestamp createdOn; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(Timestamp createdOn) { + this.createdOn = createdOn; + return this; + } + + public List getComments() { + return comments; + } + + public Post setComments(List comments) { + this.comments = comments; + return this; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment implements Identifiable { + + @Id + private Long id; + + @ManyToOne + private Post post; + + private String review; + + @Column(name = "created_on") + private Timestamp createdOn; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(Timestamp createdOn) { + this.createdOn = createdOn; + return this; + } + } + + public static class PostTitleWithCommentCount { + + private final String postTitle; + private final int commentCount; + + public PostTitleWithCommentCount( + String postTitle, + int commentCount) { + this.postTitle = postTitle; + this.commentCount = commentCount; + } + + public String getPostTitle() { + return postTitle; + } + + public int getCommentCount() { + return commentCount; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/Component.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/Component.java new file mode 100644 index 000000000..e6fda7f94 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/Component.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.hibernate.query.pivot; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +/** + * @author Vlad Mihalcea + */ +@Entity +public class Component { + + @Id + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/DataSourceConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/DataSourceConfiguration.java similarity index 97% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/DataSourceConfiguration.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/DataSourceConfiguration.java index e44a090b4..ccc15f5d2 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/DataSourceConfiguration.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/DataSourceConfiguration.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.pivot; +package com.vladmihalcea.hpjp.hibernate.query.pivot; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/PivotTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/PivotTest.java similarity index 91% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/PivotTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/PivotTest.java index e78f903b8..52d0052e9 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/PivotTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/PivotTest.java @@ -1,7 +1,6 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.pivot; +package com.vladmihalcea.hpjp.hibernate.query.pivot; -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; import org.hibernate.query.Query; import org.hibernate.transform.Transformers; import org.junit.Test; @@ -14,7 +13,7 @@ /** * @author Vlad Mihalcea */ -public class PivotTest extends AbstractPostgreSQLIntegrationTest { +public class PivotTest extends AbstractOracleIntegrationTest { @Override protected Class[] entities() { @@ -175,7 +174,7 @@ public void test() { doInJPA(entityManager -> { List dataSources = entityManager .createNativeQuery( - "SELECT distinct " + + "SELECT DISTINCT " + " userName.service_name AS \"serviceName\", " + " c.name AS \"componentName\", " + " databaseName.property_value AS \"databaseName\", " + @@ -184,20 +183,20 @@ public void test() { " userName.property_value AS \"userName\", " + " password.property_value AS \"password\" " + "FROM Component c " + - "left join Property databaseName " + - " on databaseName.component_name = c.name and " + + "LEFT JOIN Property databaseName " + + " ON databaseName.component_name = c.name AND " + " databaseName.property_name = 'databaseName' " + - "left join Property url " + - " on url.component_name = c.name and " + + "LEFT JOIN Property url " + + " ON url.component_name = c.name AND " + " url.property_name = 'url' " + - "left join Property serverName " + - " on serverName.component_name = c.name and " + + "LEFT JOIN Property serverName " + + " ON serverName.component_name = c.name AND " + " serverName.property_name = 'serverName' " + - "left join Property userName " + - " on userName.component_name = c.name and " + + "LEFT JOIN Property userName " + + " ON userName.component_name = c.name AND " + " userName.property_name = 'username' " + - "left join Property password " + - " on password.component_name = c.name and " + + "LEFT JOIN Property password " + + " ON password.component_name = c.name AND " + " password.property_name = 'password' " + "WHERE " + " c.name = :name") @@ -233,9 +232,7 @@ public void test() { .getResultList(); assertEquals(2, dataSources.size()); }); - - //http://modern-sql.com/use-case/pivot - + doInJPA(entityManager -> { List dataSources = entityManager .createNativeQuery( diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/Property.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/Property.java new file mode 100644 index 000000000..8d61ec46d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/Property.java @@ -0,0 +1,35 @@ +package com.vladmihalcea.hpjp.hibernate.query.pivot; + +import jakarta.persistence.Column; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; + +/** + * + * @author Vlad Mihalcea + */ +@Entity +public class Property { + + @EmbeddedId + private PropertyId id; + + @Column(name = "property_value") + private String value; + + public PropertyId getId() { + return id; + } + + public void setId(PropertyId id) { + this.id = id; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/PropertyId.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/PropertyId.java similarity index 87% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/PropertyId.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/PropertyId.java index 97fbc912f..08cc463f7 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/pivot/PropertyId.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/PropertyId.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.pivot; +package com.vladmihalcea.hpjp.hibernate.query.pivot; -import javax.persistence.Column; -import javax.persistence.Embeddable; -import javax.persistence.FetchType; -import javax.persistence.ManyToOne; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; import java.io.Serializable; import java.util.Objects; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/Service.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/Service.java new file mode 100644 index 000000000..9fd5e0cdc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/pivot/Service.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.hibernate.query.pivot; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +/** + * @author Vlad Mihalcea + */ +@Entity +public class Service { + + @Id + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/plan/DefaultInQueryPlanCacheTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/plan/DefaultInQueryPlanCacheTest.java new file mode 100644 index 000000000..b27f60685 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/plan/DefaultInQueryPlanCacheTest.java @@ -0,0 +1,185 @@ +package com.vladmihalcea.hpjp.hibernate.query.plan; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.SessionFactory; +import org.hibernate.stat.Statistics; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.stream.IntStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DefaultInQueryPlanCacheTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.generate_statistics", "true"); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + for (int i = 1; i <= 15; i++) { + Post post = new Post(); + post.setId(i); + post.setTitle(String.format("Post no. %d", i)); + + entityManager.persist(post); + } + }); + } + + + @Test + public void testInQueryCachePlan() { + SessionFactory sessionFactory = entityManagerFactory().unwrap(SessionFactory.class); + Statistics statistics = sessionFactory.getStatistics(); + statistics.clear(); + + doInJPA(entityManager -> { + for (int i = 1; i < 16; i++) { + getPostByIds( + entityManager, + IntStream.range(1, i + 1).boxed().toArray(Integer[]::new) + ); + } + }); + + LOGGER.info("Hibernate 6 generates a single plan now!"); + assertEquals(1L, statistics.getQueryPlanCacheMissCount()); + + for (String query : statistics.getQueries()) { + LOGGER.info("Executed query: {}", query); + } + } + + @Test + public void testJPQL() { + SessionFactory sessionFactory = entityManagerFactory().unwrap(SessionFactory.class); + Statistics statistics = sessionFactory.getStatistics(); + statistics.clear(); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + where p.id in :ids + """, Post.class) + .setParameter("ids", Arrays.asList(1, 2, 3)) + .getResultList(); + }); + + for (String query : statistics.getQueries()) { + LOGGER.info("Executed query: {}", query); + } + } + + @Test + public void testCriteriaAPI() { + SessionFactory sessionFactory = entityManagerFactory().unwrap(SessionFactory.class); + Statistics statistics = sessionFactory.getStatistics(); + statistics.clear(); + + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteria = builder.createQuery(Post.class); + Root fromPost = criteria.from(Post.class); + + criteria.where(builder.in(fromPost.get("id")).value(Arrays.asList(1, 2, 3))); + List posts = entityManager.createQuery(criteria).getResultList(); + }); + + for (String query : statistics.getQueries()) { + LOGGER.info("Executed query: {}", query); + } + } + + @Test + public void testSQLQueryCachePlan() { + SessionFactory sessionFactory = entityManagerFactory().unwrap(SessionFactory.class); + Statistics statistics = sessionFactory.getStatistics(); + statistics.clear(); + + doInJPA(entityManager -> { + for (int i = 1; i < 16; i++) { + List posts = entityManager.createNativeQuery(""" + select p.* + from post p + where p.id = :id + """, Post.class) + .setParameter("id", 1) + .getResultList(); + } + }); + + assertEquals(1, statistics.getQueryPlanCacheMissCount()); + + for (String query : statistics.getQueries()) { + LOGGER.info("Executed query: {}", query); + } + } + + private List getPostByIds(EntityManager entityManager, Integer... ids) { + return entityManager.createQuery(""" + select p + from Post p + where p.id in :ids + """, Post.class) + .setParameter("ids", Arrays.asList(ids)) + .getResultList(); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Integer id; + + private String title; + + public Post() {} + + public Post(String title) { + this.title = title; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/plan/PaddingInQueryPlanCacheTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/plan/PaddingInQueryPlanCacheTest.java new file mode 100644 index 000000000..02457085d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/plan/PaddingInQueryPlanCacheTest.java @@ -0,0 +1,121 @@ +package com.vladmihalcea.hpjp.hibernate.query.plan; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.SessionFactory; +import org.hibernate.stat.Statistics; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.stream.IntStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PaddingInQueryPlanCacheTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.generate_statistics", "true"); + properties.put("hibernate.query.in_clause_parameter_padding", "true"); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + for (int i = 1; i <= 15; i++) { + Post post = new Post(); + post.setId(i); + post.setTitle(String.format("Post no. %d", i)); + + entityManager.persist(post); + } + }); + } + + @Test + public void testInQueryCachePlan() { + SessionFactory sessionFactory = entityManagerFactory().unwrap(SessionFactory.class); + Statistics statistics = sessionFactory.getStatistics(); + statistics.clear(); + + doInJPA(entityManager -> { + for (int i = 2; i < 16; i++) { + getPostByIds( + entityManager, + IntStream.range(1, i).boxed().toArray(Integer[]::new) + ); + } + LOGGER.info("Hibernate 6 generates a single plan now!"); + assertEquals(1L, statistics.getQueryPlanCacheMissCount()); + + for (String query : statistics.getQueries()) { + LOGGER.info("Executed query: {}", query); + } + }); + } + + private List getPostByIds(EntityManager entityManager, Integer... ids) { + return entityManager.createQuery(""" + select p + from Post p + where p.id in :ids + """, Post.class) + .setParameter("ids", Arrays.asList(ids)) + .getResultList(); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Integer id; + + private String title; + + public Post() {} + + public Post(String title) { + this.title = title; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/PostCommentScore.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/PostCommentScore.java new file mode 100644 index 000000000..a0c3e93b0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/PostCommentScore.java @@ -0,0 +1,89 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentScore { + + private Long id; + private Long parentId; + private String review; + private Date createdOn; + private long score; + + private List children = new ArrayList<>(); + + public PostCommentScore(Number id, Number parentId, String review, Date createdOn, Number score) { + this.id = id.longValue(); + this.parentId = parentId != null ? parentId.longValue() : null; + this.review = review; + this.createdOn = createdOn; + this.score = score.longValue(); + } + + public PostCommentScore() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public long getScore() { + return score; + } + + public void setScore(long score) { + this.score = score; + } + + public long getTotalScore() { + long total = getScore(); + for(PostCommentScore child : children) { + total += child.getTotalScore(); + } + return total; + } + + public List getChildren() { + List copy = new ArrayList<>(children); + copy.sort(Comparator.comparing(PostCommentScore::getCreatedOn)); + return copy; + } + + public void addChild(PostCommentScore child) { + children.add(child); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/PostCommentScoreResultTransformer.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/PostCommentScoreResultTransformer.java new file mode 100644 index 000000000..ce67257eb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/PostCommentScoreResultTransformer.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive; + +import org.hibernate.transform.ResultTransformer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentScoreResultTransformer implements ResultTransformer { + + private Map postCommentScoreMap = new HashMap<>(); + + private List roots = new ArrayList<>(); + + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + PostCommentScore commentScore = (PostCommentScore) tuple[0]; + Long parentId = commentScore.getParentId(); + if (parentId == null) { + roots.add(commentScore); + } else { + PostCommentScore parent = postCommentScoreMap.get(parentId); + if (parent != null) { + parent.addChild(commentScore); + } + } + postCommentScoreMap.putIfAbsent(commentScore.getId(), commentScore); + return commentScore; + } + + @Override + public List transformList(List collection) { + return roots; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/WithRecursiveCTEFetchingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/WithRecursiveCTEFetchingTest.java new file mode 100644 index 000000000..087750562 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/WithRecursiveCTEFetchingTest.java @@ -0,0 +1,508 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.query.NativeQuery; +import org.hibernate.transform.ResultTransformer; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.*; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class WithRecursiveCTEFetchingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + /** + * post + * ---- + * + * | id | title | + * |----|--------| + * | 1 | Post 1 | + * + * post_comment + * ------------- + * + * | id | created_on | review | score | parent_id | post_id | + * |----|---------------------|---------------|-------|-----------|---------| + * | 1 | 2024-10-13 12:23:05 | Comment 1 | 1 | | 1 | + * | 2 | 2024-10-14 13:23:10 | Comment 1.1 | 2 | 1 | 1 | + * | 3 | 2024-10-14 15:45:15 | Comment 1.2 | 2 | 1 | 1 | + * | 4 | 2024-10-15 10:15:20 | Comment 1.2.1 | 1 | 3 | 1 | + * | 5 | 2024-10-13 15:23:25 | Comment 2 | 1 | | 1 | + * | 6 | 2024-10-14 11:23:30 | Comment 2.1 | 1 | 5 | 1 | + * | 7 | 2024-10-14 14:45:35 | Comment 2.2 | 1 | 5 | 1 | + * | 8 | 2024-10-15 10:15:40 | Comment 3 | 1 | | 1 | + * | 9 | 2024-10-16 11:15:45 | Comment 3.1 | 10 | 8 | 1 | + * | 10 | 2024-10-17 18:30:50 | Comment 3.2 | -2 | 8 | 1 | + * | 11 | 2024-10-19 21:43:55 | Comment 4 | -5 | | 1 | + * | 12 | 2024-10-22 23:45:00 | Comment 5 | 0 | | 1 | + */ + @Override + public void afterInit() { + doInJPA(entityManager -> { + + Post post = new Post() + .setId(1L) + .setTitle("Post 1"); + + PostComment comment1 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2024, 10, 13, 12, 23, 5))) + .setScore(1) + .setReview("Comment 1"); + + PostComment comment1_1 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2024, 10, 14, 13, 23, 10))) + .setScore(2) + .setReview("Comment 1.1") + .setParent(comment1); + + PostComment comment1_2 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2024, 10, 14, 15, 45, 15))) + .setScore(2) + .setParent(comment1) + .setReview("Comment 1.2"); + + PostComment comment1_2_1 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2024, 10, 15, 10, 15, 20))) + .setScore(1) + .setReview("Comment 1.2.1") + .setParent(comment1_2); + + PostComment comment2 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2024, 10, 13, 15, 23, 25))) + .setScore(1) + .setReview("Comment 2"); + + PostComment comment2_1 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2024, 10, 14, 11, 23, 30))) + .setScore(1) + .setReview("Comment 2.1") + .setParent(comment2); + + PostComment comment2_2 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2024, 10, 14, 14, 45, 35))) + .setScore(1) + .setReview("Comment 2.2") + .setParent(comment2); + + PostComment comment3 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2024, 10, 15, 10, 15, 40))) + .setScore(1) + .setReview("Comment 3"); + + PostComment comment3_1 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2024, 10, 16, 11, 15, 45))) + .setScore(10) + .setReview("Comment 3.1") + .setParent(comment3); + + PostComment comment3_2 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2024, 10, 17, 18, 30, 50))) + .setScore(-2) + .setReview("Comment 3.2") + .setParent(comment3); + + PostComment comment4 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2024, 10, 19, 21, 43, 55))) + .setReview("Comment 4") + .setScore(-5); + + PostComment comment5 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2024, 10, 22, 23, 45, 0))) + .setReview("Comment 5"); + + entityManager.persist(post); + entityManager.persist(comment1); + entityManager.persist(comment1_1); + entityManager.persist(comment1_2); + entityManager.persist(comment1_2_1); + entityManager.persist(comment2); + entityManager.persist(comment2_1); + entityManager.persist(comment2_2); + entityManager.persist(comment3); + entityManager.persist(comment3_1); + entityManager.persist(comment3_2); + entityManager.persist(comment4); + entityManager.persist(comment5); + }); + } + + /** + * Get the top two comment hierarchies ordered by total score. + * + * This implementation allows the database to calculate the comment hierarchy score, + * so we can fetch just the top comment hierarchies. + * + * This SQL query combines a WITH RECURSIVE query with several Derives Table subsequent queries. + * + * SELECT id, parent_id, review, created_on, score, total_score + * FROM ( + * SELECT + * id, parent_id, review, created_on, score, total_score, + * DENSE_RANK() OVER (ORDER BY total_score DESC) AS ranking + * FROM ( + * SELECT + * id, parent_id, review, created_on, score, + * SUM(score) OVER (PARTITION BY root_id) AS total_score + * FROM ( + * WITH RECURSIVE post_comment_score( + * id, root_id, post_id, parent_id, review, created_on, score) + * AS ( + * SELECT + * id, id, post_id, parent_id, review, created_on, score + * FROM post_comment + * WHERE post_id = 1 AND parent_id IS NULL + * UNION ALL + * SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, + * pc.review, pc.created_on, pc.score + * FROM post_comment pc + * INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id + * ) + * SELECT id, parent_id, root_id, review, created_on, score + * FROM post_comment_score + * ) total_score_comment + * ) total_score_ranking + * ) total_score_filtering + * WHERE ranking <= 3 + * ORDER BY total_score DESC, id ASC + * + * | id | parent_id | review | created_on | score | total_score | + * |----|-----------|---------------|---------------------|-------|-------------| + * | 8 | | Comment 3 | 2024-10-15 10:15:40 | 1 | 9 | + * | 9 | 8 | Comment 3.1 | 2024-10-16 11:15:45 | 10 | 9 | + * | 10 | 8 | Comment 3.2 | 2024-10-17 18:30:50 | -2 | 9 | + * | 1 | | Comment 1 | 2024-10-13 12:23:05 | 1 | 6 | + * | 2 | 1 | Comment 1.1 | 2024-10-14 13:23:10 | 2 | 6 | + * | 3 | 1 | Comment 1.2 | 2024-10-14 15:45:15 | 2 | 6 | + * | 4 | 3 | Comment 1.2.1 | 2024-10-15 10:15:20 | 1 | 6 | + * | 5 | | Comment 2 | 2024-10-13 15:23:25 | 1 | 3 | + * | 6 | 5 | Comment 2.1 | 2024-10-14 11:23:30 | 1 | 3 | + * | 7 | 5 | Comment 2.2 | 2024-10-14 14:45:35 | 1 | 3 | + */ + @Test + public void testFetchAndSortUsingRecursiveCTEAndDerivedTables() { + int ranking = 3; + + doInJPA(entityManager -> { + if (database() == Database.MYSQL) { + entityManager.createNativeQuery(""" + UPDATE performance_schema.setup_instruments + SET enabled = 'YES', timed = 'YES' + """) + .executeUpdate(); + + entityManager.createNativeQuery(""" + UPDATE performance_schema.setup_consumers + SET enabled = 'YES' + """) + .executeUpdate(); + } + + List postCommentRoots = entityManager.createNativeQuery(""" + SELECT id, parent_id, review, created_on, score, total_score + FROM ( + SELECT + id, parent_id, review, created_on, score, total_score, + dense_rank() OVER (ORDER BY total_score DESC) AS ranking + FROM ( + SELECT + id, parent_id, review, created_on, score, + SUM(score) OVER (PARTITION BY root_id) AS total_score + FROM ( + WITH RECURSIVE post_comment_score( + id, root_id, post_id, parent_id, review, created_on, score) + AS ( + SELECT + id, id, post_id, parent_id, review, created_on, score + FROM post_comment + WHERE post_id = :postId AND parent_id IS NULL + UNION ALL + SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, + pc.review, pc.created_on, pc.score + FROM post_comment pc + INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id + ) + SELECT id, parent_id, root_id, review, created_on, score + FROM post_comment_score + ) total_score_comment + ) total_score_ranking + ) total_score_filtering + WHERE ranking <= :ranking + ORDER BY total_score DESC, id ASC + """, "PostCommentScore") + .unwrap(NativeQuery.class) + .setParameter("postId", 1L) + .setParameter("ranking", ranking) + .setResultTransformer(new PostCommentScoreResultTransformer()) + .getResultList(); + + assertEquals(3, postCommentRoots.size()); + + assertEquals(9, postCommentRoots.get(0).getTotalScore()); + assertEquals(6, postCommentRoots.get(1).getTotalScore()); + assertEquals(3, postCommentRoots.get(2).getTotalScore()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @SqlResultSetMapping( + name = "PostCommentScore", + classes = @ConstructorResult( + targetClass = PostCommentScore.class, + columns = { + @ColumnResult(name = "id"), + @ColumnResult(name = "parent_id"), + @ColumnResult(name = "review"), + @ColumnResult(name = "created_on"), + @ColumnResult(name = "score") + } + ) + ) + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne + @JoinColumn(name = "parent_id") + private PostComment parent; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn; + + private String review; + + private int score; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public PostComment getParent() { + return parent; + } + + public PostComment setParent(PostComment parent) { + this.parent = parent; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public int getScore() { + return score; + } + + public PostComment setScore(int score) { + this.score = score; + return this; + } + } + + public static class PostCommentScore { + + private Long id; + private Long parentId; + private String review; + private Date createdOn; + private long score; + + private List children = new ArrayList<>(); + + public PostCommentScore(Number id, Number parentId, String review, Date createdOn, Number score) { + this.id = id.longValue(); + this.parentId = parentId != null ? parentId.longValue() : null; + this.review = review; + this.createdOn = createdOn; + this.score = score.longValue(); + } + + public PostCommentScore() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public long getScore() { + return score; + } + + public void setScore(long score) { + this.score = score; + } + + public long getTotalScore() { + long total = getScore(); + for (PostCommentScore child : children) { + total += child.getTotalScore(); + } + return total; + } + + public List getChildren() { + List copy = new ArrayList<>(children); + copy.sort(Comparator.comparing(PostCommentScore::getCreatedOn)); + return copy; + } + + public void addChild(PostCommentScore child) { + children.add(child); + } + } + + public static class PostCommentScoreResultTransformer implements ResultTransformer { + + private Map postCommentScoreMap = new HashMap<>(); + + private List roots = new ArrayList<>(); + + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + PostCommentScore commentScore = (PostCommentScore) tuple[0]; + Long parentId = commentScore.getParentId(); + if (parentId == null) { + roots.add(commentScore); + } else { + PostCommentScore parent = postCommentScoreMap.get(parentId); + if (parent != null) { + parent.addChild(commentScore); + } + } + postCommentScoreMap.putIfAbsent(commentScore.getId(), commentScore); + return commentScore; + } + + @Override + public List transformList(List collection) { + return roots; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/WithRecursiveCTEHibernateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/WithRecursiveCTEHibernateTest.java new file mode 100644 index 000000000..2e2bc1678 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/WithRecursiveCTEHibernateTest.java @@ -0,0 +1,270 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import jakarta.persistence.*; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.hibernate.jpa.boot.spi.JpaSettings; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class WithRecursiveCTEHibernateTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + JpaSettings.INTEGRATOR_PROVIDER, + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator( + List.of( + PostCommentRecord.class + ) + ) + ) + ); + //properties.put("hibernate.hbm2ddl.auto", "none"); + } + + /*@Override + protected void beforeInit() { + executeStatement("drop table if exists post cascade"); + executeStatement("drop table if exists post_comment cascade"); + executeStatement("drop sequence if exists post_comment_SEQ"); + executeStatement("create sequence post_comment_SEQ start with 1 increment by 1"); + executeStatement("create table post (id bigint not null, title varchar(100), primary key (id))"); + executeStatement("create table post_comment (id bigint not null, post_id bigint, created_on timestamp(6), review varchar(250), score integer not null, parent_id bigint, primary key (id))"); + executeStatement("alter table if exists post_comment add constraint FK_post_comment_parent_id foreign key (parent_id) references post_comment"); + executeStatement("alter table if exists post_comment add constraint FK_post_comment_post_id foreign key (post_id) references post"); + }*/ + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("Post 1"); + + entityManager.persist(post); + + entityManager.persist( + new PostComment() + .setPost(post) + .setCreatedOn(LocalDateTime.of(2024, 6, 13, 12, 23, 5)) + .setScore(1) + .setReview("Comment 1") + .addChild( + new PostComment() + .setPost(post) + .setCreatedOn(LocalDateTime.of(2024, 6, 14, 13, 23, 10)) + .setScore(2) + .setReview("Comment 1.1") + ) + .addChild( + new PostComment() + .setPost(post) + .setCreatedOn(LocalDateTime.of(2024, 6, 14, 15, 45, 15)) + .setScore(2) + .setReview("Comment 1.2") + .addChild( + new PostComment() + .setPost(post) + .setCreatedOn(LocalDateTime.of(2024, 6, 15, 10, 15, 20)) + .setScore(1) + .setReview("Comment 1.2.1") + ) + ) + ); + }); + } + + @Test + public void testJPQLWithRecursive() { + doInJPA(entityManager -> { + List postComments = entityManager.createQuery(""" + WITH postCommentChildHierarchy AS ( + SELECT pc.children pc + FROM PostComment pc + WHERE pc.id = :commentId + UNION ALL + SELECT pc.children pc + FROM PostComment pc + JOIN postCommentChildHierarchy pch ON pc = pch.pc + ORDER BY pc.id + ) + SELECT new PostCommentRecord( + pch.pc.id, + pch.pc.createdOn, + pch.pc.review, + pch.pc.score, + pch.pc.parent.id + ) + FROM postCommentChildHierarchy pch + """, PostCommentRecord.class) + .setParameter("commentId", 1L) + .getResultList(); + + assertEquals(3, postComments.size()); + assertEquals("Comment 1.1", postComments.get(0).review); + assertEquals("Comment 1.2", postComments.get(1).review); + assertEquals("Comment 1.2.1", postComments.get(2).review); + }); + } + + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @Column(length = 100) + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + @GeneratedValue(generator = "post_comment_seq", strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "post_comment_seq", allocationSize = 1) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(length = 250) + private String review; + + private int score; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private PostComment parent; + + @OneToMany( + mappedBy = "parent", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List children = new ArrayList<>(); + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public int getScore() { + return score; + } + + public PostComment setScore(int score) { + this.score = score; + return this; + } + + public PostComment getParent() { + return parent; + } + + public PostComment setParent(PostComment parent) { + this.parent = parent; + return this; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public PostComment addChild(PostComment child) { + children.add(child); + child.setParent(this); + return this; + } + } + + public record PostCommentRecord( + Long id, + LocalDateTime createdOn, + String review, + int score, + Long parentId + ) { + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/BookCategoryWithRecursiveCTETest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/BookCategoryWithRecursiveCTETest.java new file mode 100644 index 000000000..41479e509 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/BookCategoryWithRecursiveCTETest.java @@ -0,0 +1,267 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.category; + +import com.blazebit.persistence.Criteria; +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.spi.CriteriaBuilderConfiguration; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.DistinctListTransformer; +import com.vladmihalcea.hpjp.hibernate.query.recursive.category.model.Book; +import com.vladmihalcea.hpjp.hibernate.query.recursive.category.model.Category; +import com.vladmihalcea.hpjp.hibernate.query.recursive.category.model.CategoryView; +import com.vladmihalcea.hpjp.hibernate.query.recursive.category.model.Category_; +import com.vladmihalcea.hpjp.hibernate.query.recursive.category.model.dto.BookDTO; +import com.vladmihalcea.hpjp.hibernate.query.recursive.category.model.dto.CategoryDTO; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.EntityManagerFactory; +import org.hibernate.query.NativeQuery; +import org.hibernate.query.TupleTransformer; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BookCategoryWithRecursiveCTETest extends AbstractTest { + + private CriteriaBuilderFactory criteriaBuilderFactory; + + @Override + protected EntityManagerFactory newEntityManagerFactory() { + EntityManagerFactory entityManagerFactory = super.newEntityManagerFactory(); + CriteriaBuilderConfiguration config = Criteria.getDefault(); + criteriaBuilderFactory = config.createCriteriaBuilderFactory(entityManagerFactory); + return entityManagerFactory; + } + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class, + Category.class, + CategoryView.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Category java = new Category().setName("Java"); + Category jpa = new Category().setName("JPA").setParent(java); + Category hibernate = new Category().setName("Hibernate").setParent(jpa); + Category hibernate6 = new Category().setName("Hibernate 6").setParent(hibernate); + + entityManager.persist(java); + entityManager.persist(jpa); + entityManager.persist(hibernate); + entityManager.persist(hibernate6); + + entityManager.flush(); + + entityManager.persist( + new Book() + .setTitle("High-Performance Java Persistence") + .setIsbn(9789730228236L) + .setCategory(hibernate6) + ); + + entityManager.persist( + new Book() + .setTitle("Effective Java") + .setIsbn(9780134685991L) + .setCategory(java) + ); + }); + } + + @Test + public void testFetchManually() { + Book hpjp = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select b + from Book b + left join fetch b.category c1 + left join fetch c1.parent c2 + left join fetch c2.parent c3 + left join fetch c3.parent c4 + where b.isbn = :isbn + """, Book.class) + .setParameter("isbn", 9789730228236L) + .getSingleResult(); + }); + + Category hpjpCategory = hpjp.getCategory(); + + assertEquals("Hibernate 6", hpjpCategory.getName()); + assertEquals("Hibernate", hpjpCategory.getParent().getName()); + assertEquals("JPA", hpjpCategory.getParent().getParent().getName()); + assertEquals("Java", hpjpCategory.getParent().getParent().getParent().getName()); + + Book effectiveJava = doInJPA(entityManager -> { + return entityManager.createQuery(""" + select b + from Book b + left join fetch b.category c + where b.isbn = :isbn + """, Book.class) + .setParameter("isbn", 9780134685991L) + .getSingleResult(); + }); + + assertEquals("Java", effectiveJava.getCategory().getName()); + } + + @Test + public void testFetchAutomatically() { + BookDTO hpjp = doInJPA(entityManager -> + (BookDTO) entityManager.createNativeQuery(""" + SELECT + b.id AS "b.id", + b.title AS "b.title", + b.isbn AS "b.isbn", + b.category_id AS "b.category_id", + c.id AS "c.id", + c.name AS "c.name", + c.parent_id AS "c.parent_id" + FROM + book b, + LATERAL ( + WITH RECURSIVE book_category_hierarchy AS ( + SELECT + category.id AS id, + category.name AS name, + category.parent_id AS parent_id + FROM category + WHERE category.id = b.category_id + UNION ALL + SELECT + category.id AS id, + category.name AS name, + category.parent_id AS parent_id + FROM category category + JOIN book_category_hierarchy bch ON bch.parent_id = category.id + ) + SELECT * + FROM book_category_hierarchy + ) c + WHERE isbn = :isbn + """, "BookCategory") + .setParameter("isbn", 9789730228236L) + .unwrap(NativeQuery.class) + .setTupleTransformer(new BookDTOTupleTransformer()) + .setResultListTransformer(DistinctListTransformer.INSTANCE) + .getSingleResult()); + + CategoryDTO hpjpCategory = hpjp.getCategory(); + + assertEquals("Hibernate 6", hpjpCategory.getName()); + assertEquals("Hibernate", hpjpCategory.getParent().getName()); + assertEquals("JPA", hpjpCategory.getParent().getParent().getName()); + assertEquals("Java", hpjpCategory.getParent().getParent().getParent().getName()); + } + + @Test + public void testFetchAllParents() { + List categoryWithParents = doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + SELECT + c.id AS "c.id", + c.name AS "c.name", + c.parent_id AS "c.parent_id" + FROM ( + WITH RECURSIVE book_category_hierarchy AS ( + SELECT + category.id AS id, + category.name AS name, + category.parent_id AS parent_id + FROM category + WHERE category.name = :categoryName + UNION ALL + SELECT + category.id AS id, + category.name AS name, + category.parent_id AS parent_id + FROM category category + JOIN book_category_hierarchy bch ON bch.parent_id = category.id + ) + SELECT * + FROM book_category_hierarchy + ) c + """, CategoryDTO.class) + .setParameter("categoryName", "Hibernate 6") + .getResultList(); + } + ); + + int index = 0; + assertEquals(4, categoryWithParents.size()); + assertEquals("Hibernate 6", categoryWithParents.get(index++).getName()); + assertEquals("Hibernate", categoryWithParents.get(index++).getName()); + assertEquals("JPA", categoryWithParents.get(index++).getName()); + assertEquals("Java", categoryWithParents.get(index).getName()); + } + + public static class BookDTOTupleTransformer implements TupleTransformer { + + private BookDTO book; + + @Override + public BookDTO transformTuple(Object[] tuple, String[] aliases) { + CategoryDTO category = (CategoryDTO) tuple[1]; + + if(book == null) { + book = (BookDTO) tuple[0]; + book.setCategory(category); + } else { + CategoryDTO childCategory = book.getCategory().findByParentId(category.getId()); + if (childCategory != null) { + childCategory.setParent(category); + } + } + + return book; + } + } + + @Test + public void testFetchCategoriesWithBlaze() { + List categories = doInJPA(entityManager -> { + return criteriaBuilderFactory + .create(entityManager, CategoryView.class) + .withRecursive(CategoryView.class) + .from(Category.class, "c") + .bind(Category_.ID).select("c.id") + .bind(Category_.NAME).select("c.name") + .bind(Category_.PARENT).select("c.parent") + .where("c.id").in() + .from(Book.class, "book") + .select("book.category.id") + .where("book.isbn").eqExpression(":isbn") + .end() + .unionAll() + .from(Category.class, "c") + .from(CategoryView.class, "child") + .bind(Category_.ID).select("c.id") + .bind(Category_.NAME).select("c.name") + .bind(Category_.PARENT).select("c.parent") + .where("c.id").eqExpression("child.parent.id") + .end() + .setParameter("isbn", 9789730228236L) + .getResultList(); + }); + + assertEquals(4, categories.size()); + CategoryView hibernateCategoryView = categories.get(0); + assertEquals("Hibernate 6", hibernateCategoryView.getName()); + assertEquals("Hibernate", hibernateCategoryView.getParent().getName()); + assertEquals("JPA", hibernateCategoryView.getParent().getParent().getName()); + assertEquals("Java", hibernateCategoryView.getParent().getParent().getParent().getName()); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/Book.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/Book.java new file mode 100644 index 000000000..cd0bfafde --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/Book.java @@ -0,0 +1,86 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.category.model; + +import com.vladmihalcea.hpjp.hibernate.query.recursive.category.model.dto.BookDTO; +import com.vladmihalcea.hpjp.hibernate.query.recursive.category.model.dto.CategoryDTO; +import jakarta.persistence.*; +import org.hibernate.annotations.NaturalId; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "book") +@SqlResultSetMapping( + name = "BookCategory", + classes = { + @ConstructorResult( + targetClass = BookDTO.class, + columns = { + @ColumnResult(name = "b.id"), + @ColumnResult(name = "b.title"), + @ColumnResult(name = "b.isbn"), + @ColumnResult(name = "b.category_id") + } + ), + @ConstructorResult( + targetClass = CategoryDTO.class, + columns = { + @ColumnResult(name = "c.id"), + @ColumnResult(name = "c.name"), + @ColumnResult(name = "c.parent_id") + } + ) + } +) +public class Book { + + @Id + @GeneratedValue + private Long id; + + @Column(length = 50) + private String title; + + @Column(columnDefinition = "numeric(13)") + @NaturalId + private long isbn; + + @ManyToOne(fetch = FetchType.LAZY) + private Category category; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public long getIsbn() { + return isbn; + } + + public Book setIsbn(long isbn) { + this.isbn = isbn; + return this; + } + + public Category getCategory() { + return category; + } + + public Book setCategory(Category category) { + this.category = category; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/Category.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/Category.java new file mode 100644 index 000000000..989927aaa --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/Category.java @@ -0,0 +1,48 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.category.model; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "category") +public class Category { + + @Id + @GeneratedValue + private Short id; + + @Column(length = 25) + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + private Category parent; + + public Short getId() { + return id; + } + + public Category setId(Short id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Category setName(String name) { + this.name = name; + return this; + } + + public Category getParent() { + return parent; + } + + public Category setParent(Category parent) { + this.parent = parent; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/CategoryView.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/CategoryView.java new file mode 100644 index 000000000..dafbb8119 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/CategoryView.java @@ -0,0 +1,49 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.category.model; + +import com.blazebit.persistence.CTE; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +/** + * @author Vlad Mihalcea + */ +@CTE +@Entity +public class CategoryView { + + @Id + private Short id; + + private String name; + + @ManyToOne + private Category parent; + + public Short getId() { + return id; + } + + public CategoryView setId(Short id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public CategoryView setName(String name) { + this.name = name; + return this; + } + + public Category getParent() { + return parent; + } + + public CategoryView setParent(Category parent) { + this.parent = parent; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/dto/BookDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/dto/BookDTO.java new file mode 100644 index 000000000..fe19a4036 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/dto/BookDTO.java @@ -0,0 +1,58 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.category.model.dto; + +/** + * @author Vlad Mihalcea + */ +public class BookDTO { + + private Long id; + + private String title; + + private long isbn; + + private CategoryDTO category; + + public BookDTO(Long id, String title, Number isbn, Short categoryId) { + this.id = id; + this.title = title; + this.isbn = isbn.longValue(); + this.category = new CategoryDTO(categoryId); + } + + public Long getId() { + return id; + } + + public BookDTO setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public BookDTO setTitle(String title) { + this.title = title; + return this; + } + + public long getIsbn() { + return isbn; + } + + public BookDTO setIsbn(long isbn) { + this.isbn = isbn; + return this; + } + + public CategoryDTO getCategory() { + return category; + } + + public BookDTO setCategory(CategoryDTO category) { + this.category = category; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/dto/CategoryDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/dto/CategoryDTO.java new file mode 100644 index 000000000..17d534323 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/category/model/dto/CategoryDTO.java @@ -0,0 +1,61 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.category.model.dto; + +/** + * @author Vlad Mihalcea + */ +public class CategoryDTO { + + private Short id; + + private String name; + + private CategoryDTO parent; + + public Short getId() { + return id; + } + + public CategoryDTO(Short id) { + this.id = id; + } + + public CategoryDTO(Short id, String name, Short parentId) { + this.id = id; + this.name = name; + this.parent = parentId != null ? new CategoryDTO(parentId) : null; + } + + public CategoryDTO setId(Short id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public CategoryDTO setName(String name) { + this.name = name; + return this; + } + + public CategoryDTO getParent() { + return parent; + } + + public CategoryDTO setParent(CategoryDTO parent) { + this.parent = parent; + return this; + } + + public CategoryDTO findByParentId(Short id) { + if (parent != null) { + if (parent.id.equals(id)) { + return this; + } else { + return parent.findByParentId(id); + } + } + return null; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/AbstractPostCommentScorePerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/AbstractPostCommentScorePerformanceTest.java new file mode 100644 index 000000000..16cd785d8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/AbstractPostCommentScorePerformanceTest.java @@ -0,0 +1,407 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.complex; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public abstract class AbstractPostCommentScorePerformanceTest extends AbstractPostgreSQLIntegrationTest { + + private MetricRegistry metricRegistry = new MetricRegistry(); + + protected com.codahale.metrics.Timer timer = metricRegistry.timer(getClass().getSimpleName()); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .build(); + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + User.class, + PostCommentVote.class, + }; + } + + private User user1; + private User user2; + + private int postCount; + private int commentCount; + + public AbstractPostCommentScorePerformanceTest(int postCount, int commentCount) { + this.postCount = postCount; + this.commentCount = commentCount; + } + + @Parameterized.Parameters + public static Collection parameters() { + List postCountSizes = new ArrayList<>(); + int postCount = 10; + postCountSizes.add(new Integer[] {postCount, 4}); + postCountSizes.add(new Integer[] {postCount, 4}); + postCountSizes.add(new Integer[] {postCount, 4}); + postCountSizes.add(new Integer[] {postCount, 8}); + postCountSizes.add(new Integer[] {postCount, 16}); + postCountSizes.add(new Integer[] {postCount, 32}); + postCountSizes.add(new Integer[] {postCount, 64}); + return postCountSizes; + } + + @Override + public void init() { + super.init(); + doInJPA(entityManager -> { + user1 = new User(); + user1.setUsername("JohnDoe"); + entityManager.persist(user1); + + user2 = new User(); + user2.setUsername("JohnDoeJr"); + entityManager.persist(user2); + }); + for (long i = 0; i < postCount; i++) { + insertPost(i); + } + } + + private void insertPost(Long postId) { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(postId); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + for (int i = 0; i < commentCount; i++) { + PostComment comment1 = new PostComment(); + comment1.setPost(post); + comment1.setReview(String.format("Comment %d", i)); + entityManager.persist(comment1); + + PostCommentVote user1Comment1 = new PostCommentVote(user1, comment1); + user1Comment1.setUp(entropy()); + entityManager.persist(user1Comment1); + + for (int j = 0; j < commentCount / 2; j++) { + PostComment comment1_1 = new PostComment(); + comment1_1.setParent(comment1); + comment1_1.setPost(post); + comment1_1.setReview(String.format("Comment %d-%d", i, j)); + entityManager.persist(comment1_1); + + PostCommentVote user1Comment1_1 = new PostCommentVote(user1, comment1_1); + user1Comment1_1.setUp(entropy()); + entityManager.persist(user1Comment1_1); + + PostCommentVote user2Comment1_1 = new PostCommentVote(user2, comment1_1); + user2Comment1_1.setUp(entropy()); + entityManager.persist(user2Comment1_1); + + for (int k = 0; k < commentCount / 4 ; k++) { + PostComment comment1_1_1 = new PostComment(); + comment1_1_1.setParent(comment1_1_1); + comment1_1_1.setPost(post); + comment1_1_1.setReview(String.format("Comment %d-%d-%d", i, j, k)); + entityManager.persist(comment1_1_1); + + PostCommentVote user1Comment1_1_1 = new PostCommentVote(user1, comment1_1_1); + user1Comment1_1_1.setUp(entropy()); + entityManager.persist(user1Comment1_1_1); + + PostCommentVote user2Comment1_1_2 = new PostCommentVote(user2, comment1_1_1); + user2Comment1_1_2.setUp(entropy()); + entityManager.persist(user2Comment1_1_2); + } + } + } + }); + } + + private boolean entropy() { + return Math.random() > 0.5d; + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.jdbc.batch_size", "5"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_versioned_data", "true"); + return properties; + } + + @Test + @Ignore + public void test() { + int rank = 3; + for (long postId = 0; postId < postCount; postId++) { + List result = postCommentScores(postId, rank); + assertNotNull(result); + } + logReporter.report(); + } + + protected abstract List postCommentScores(Long postId, int rank); + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @SqlResultSetMapping( + name = "PostCommentScore", + classes = @ConstructorResult( + targetClass = PostCommentScore.class, + columns = { + @ColumnResult(name = "id"), + @ColumnResult(name = "parent_id"), + @ColumnResult(name = "root_id"), + @ColumnResult(name = "review"), + @ColumnResult(name = "created_on"), + @ColumnResult(name = "score") + } + ) + ) + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne + @JoinColumn(name = "parent_id") + private PostComment parent; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn = new Date(); + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public PostComment getParent() { + return parent; + } + + public void setParent(PostComment parent) { + this.parent = parent; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostComment that = (PostComment) o; + return Objects.equals(getPost(), that.getPost()) && + Objects.equals(getParent(), that.getParent()) && + Objects.equals(getReview(), that.getReview()); + } + + @Override + public int hashCode() { + return Objects.hash(getPost(), getReview()); + } + + @Override + public String toString() { + return "PostComment{" + + "review='" + review + '\'' + + ", post=" + post + + '}'; + } + } + + @Entity(name = "User") + @Table(name = "forum_user") + public static class User { + + @Id + @GeneratedValue + private Long id; + + private String username; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return Objects.equals(getUsername(), user.getUsername()); + } + + @Override + public int hashCode() { + return Objects.hash(getUsername()); + } + + @Override + public String toString() { + return "User{" + + "username='" + username + '\'' + + '}'; + } + } + + @Entity(name = "PostCommentVote") + @Table(name = "post_comment_vote") + public static class PostCommentVote implements Serializable { + + @Id + @ManyToOne + private User user; + + @Id + @ManyToOne + private PostComment comment; + + private boolean up; + + private PostCommentVote() { + } + + public PostCommentVote(User user, PostComment comment) { + this.user = user; + this.comment = comment; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public PostComment getComment() { + return comment; + } + + public void setComment(PostComment comment) { + this.comment = comment; + } + + public boolean isUp() { + return up; + } + + public void setUp(boolean up) { + this.up = up; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostCommentVote that = (PostCommentVote) o; + return Objects.equals(getUser(), that.getUser()) && + Objects.equals(getComment(), that.getComment()); + } + + @Override + public int hashCode() { + return Objects.hash(getUser(), getComment()); + } + + @Override + public String toString() { + return "PostCommentVote{" + + "user=" + user + + ", comment=" + comment + + '}'; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/AllFetch.txt b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/AllFetch.txt similarity index 95% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/AllFetch.txt rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/AllFetch.txt index 56b9a02e3..a963b50a0 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/AllFetch.txt +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/AllFetch.txt @@ -1,4 +1,4 @@ -"C:\Program Files\Java\jdk1.8.0_71\bin\java" -ea -Didea.launcher.port=7533 "-Didea.launcher.bin.path=C:\Program Files (x86)\JetBrains\IntelliJ IDEA 15.0.2\bin" -Didea.junit.sm_runner -Dfile.encoding=UTF-8 -classpath "C:\Program Files (x86)\JetBrains\IntelliJ IDEA 15.0.2\lib\idea_rt.jar;C:\Program Files (x86)\JetBrains\IntelliJ IDEA 15.0.2\plugins\junit\lib\junit-rt.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\rt.jar;D:\Vlad\Work\GitHub\high-performance-java-persistence\core\target\test-classes;D:\Vlad\Work\GitHub\high-performance-java-persistence\core\target\classes;C:\Users\Vlad\.m2\repository\org\slf4j\slf4j-api\1.7.7\slf4j-api-1.7.7.jar;C:\Users\Vlad\.m2\repository\ch\qos\logback\logback-classic\1.1.2\logback-classic-1.1.2.jar;C:\Users\Vlad\.m2\repository\ch\qos\logback\logback-core\1.1.2\logback-core-1.1.2.jar;C:\Users\Vlad\.m2\repository\org\apache\commons\commons-lang3\3.3.2\commons-lang3-3.3.2.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-core\5.1.0.Final\hibernate-core-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\org\jboss\logging\jboss-logging\3.3.0.Final\jboss-logging-3.3.0.Final.jar;C:\Users\Vlad\.m2\repository\org\hibernate\javax\persistence\hibernate-jpa-2.1-api\1.0.0.Final\hibernate-jpa-2.1-api-1.0.0.Final.jar;C:\Users\Vlad\.m2\repository\org\javassist\javassist\3.20.0-GA\javassist-3.20.0-GA.jar;C:\Users\Vlad\.m2\repository\antlr\antlr\2.7.7\antlr-2.7.7.jar;C:\Users\Vlad\.m2\repository\org\apache\geronimo\specs\geronimo-jta_1.1_spec\1.1.1\geronimo-jta_1.1_spec-1.1.1.jar;C:\Users\Vlad\.m2\repository\org\jboss\jandex\2.0.0.Final\jandex-2.0.0.Final.jar;C:\Users\Vlad\.m2\repository\com\fasterxml\classmate\1.3.0\classmate-1.3.0.jar;C:\Users\Vlad\.m2\repository\dom4j\dom4j\1.6.1\dom4j-1.6.1.jar;C:\Users\Vlad\.m2\repository\xml-apis\xml-apis\1.0.b2\xml-apis-1.0.b2.jar;C:\Users\Vlad\.m2\repository\org\hibernate\common\hibernate-commons-annotations\5.0.1.Final\hibernate-commons-annotations-5.0.1.Final.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-java8\5.1.0.Final\hibernate-java8-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-c3p0\5.1.0.Final\hibernate-c3p0-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\com\mchange\c3p0\0.9.2.1\c3p0-0.9.2.1.jar;C:\Users\Vlad\.m2\repository\com\mchange\mchange-commons-java\0.2.3.4\mchange-commons-java-0.2.3.4.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-entitymanager\5.1.0.Final\hibernate-entitymanager-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-hikaricp\5.1.0.Final\hibernate-hikaricp-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\com\zaxxer\HikariCP-java6\2.3.9\HikariCP-java6-2.3.9.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-spatial\5.1.0.Final\hibernate-spatial-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\org\geolatte\geolatte-geom\1.0.1\geolatte-geom-1.0.1.jar;C:\Users\Vlad\.m2\repository\com\vividsolutions\jts\1.13\jts-1.13.jar;C:\Users\Vlad\.m2\repository\org\apfloat\apfloat\1.8.2\apfloat-1.8.2.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-validator\5.2.3.Final\hibernate-validator-5.2.3.Final.jar;C:\Users\Vlad\.m2\repository\javax\validation\validation-api\1.1.0.Final\validation-api-1.1.0.Final.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-jpamodelgen\5.1.0.Final\hibernate-jpamodelgen-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\javax\el\javax.el-api\2.2.4\javax.el-api-2.2.4.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-ehcache\5.1.0.Final\hibernate-ehcache-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\net\sf\ehcache\ehcache\2.10.1\ehcache-2.10.1.jar;C:\Users\Vlad\.m2\repository\net\sf\ehcache\ehcache-core\2.6.9\ehcache-core-2.6.9.jar;C:\Users\Vlad\.m2\repository\net\ttddyy\datasource-proxy\1.3.3\datasource-proxy-1.3.3.jar;C:\Users\Vlad\.m2\repository\p6spy\p6spy\2.1.4\p6spy-2.1.4.jar;C:\Users\Vlad\.m2\repository\org\hsqldb\hsqldb\2.3.2\hsqldb-2.3.2.jar;C:\Users\Vlad\.m2\repository\org\postgresql\postgresql\9.4-1202-jdbc41\postgresql-9.4-1202-jdbc41.jar;C:\Users\Vlad\.m2\repository\com\oracle\ojdbc7_g\12.1.0.1\ojdbc7_g-12.1.0.1.jar;C:\Users\Vlad\.m2\repository\mysql\mysql-connector-java\5.1.38\mysql-connector-java-5.1.38.jar;C:\Users\Vlad\.m2\repository\com\microsoft\sqlserver\sqljdbc4\4.0\sqljdbc4-4.0.jar;C:\Users\Vlad\.m2\repository\net\sourceforge\jtds\jtds\1.3.1\jtds-1.3.1.jar;C:\Users\Vlad\.m2\repository\io\dropwizard\metrics\metrics-core\3.1.0\metrics-core-3.1.0.jar;C:\Users\Vlad\.m2\repository\com\zaxxer\HikariCP\1.3.3\HikariCP-1.3.3.jar;C:\Users\Vlad\.m2\repository\org\codehaus\btm\btm\2.1.4\btm-2.1.4.jar;C:\Users\Vlad\.m2\repository\javax\transaction\jta\1.1\jta-1.1.jar;C:\Users\Vlad\.m2\repository\org\flywaydb\flyway-core\3.2.1\flyway-core-3.2.1.jar;C:\Users\Vlad\.m2\repository\com\vladmihalcea\flexy-pool\flexy-pool-core\1.2.4\flexy-pool-core-1.2.4.jar;C:\Users\Vlad\.m2\repository\com\vladmihalcea\flexy-pool\flexy-btm\1.2.4\flexy-btm-1.2.4.jar;C:\Users\Vlad\.m2\repository\com\vladmihalcea\flexy-pool\flexy-codahale-metrics\1.2.4\flexy-codahale-metrics-1.2.4.jar;C:\Users\Vlad\.m2\repository\com\vladmihalcea\flexy-pool\flexy-dropwizard-metrics\1.2.4\flexy-dropwizard-metrics-1.2.4.jar;C:\Users\Vlad\.m2\repository\com\vladmihalcea\db-util\0.0.1\db-util-0.0.1.jar;C:\Users\Vlad\.m2\repository\org\slf4j\jcl-over-slf4j\1.6.1\jcl-over-slf4j-1.6.1.jar;C:\Users\Vlad\.m2\repository\commons-lang\commons-lang\2.5\commons-lang-2.5.jar;C:\Users\Vlad\.m2\repository\org\aspectj\aspectjrt\1.8.7\aspectjrt-1.8.7.jar;C:\Users\Vlad\.m2\repository\org\aspectj\aspectjweaver\1.8.7\aspectjweaver-1.8.7.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-beans\4.2.3.RELEASE\spring-beans-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-core\4.2.3.RELEASE\spring-core-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\commons-logging\commons-logging\1.2\commons-logging-1.2.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-context\4.2.3.RELEASE\spring-context-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-aop\4.2.3.RELEASE\spring-aop-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\aopalliance\aopalliance\1.0\aopalliance-1.0.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-expression\4.2.3.RELEASE\spring-expression-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-tx\4.2.3.RELEASE\spring-tx-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-orm\4.2.3.RELEASE\spring-orm-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-jdbc\4.2.3.RELEASE\spring-jdbc-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\junit\junit\4.11\junit-4.11.jar;C:\Users\Vlad\.m2\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-test\4.2.3.RELEASE\spring-test-4.2.3.RELEASE.jar" com.intellij.rt.execution.application.AppMain com.intellij.rt.execution.junit.JUnitStarter -ideVersion5 com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScoreFetchAllPerformanceTest +"C:\Program Files\Java\jdk1.8.0_71\bin\java" -ea -Didea.launcher.port=7533 "-Didea.launcher.bin.path=C:\Program Files (x86)\JetBrains\IntelliJ IDEA 15.0.2\bin" -Didea.junit.sm_runner -Dfile.encoding=UTF-8 -classpath "C:\Program Files (x86)\JetBrains\IntelliJ IDEA 15.0.2\lib\idea_rt.jar;C:\Program Files (x86)\JetBrains\IntelliJ IDEA 15.0.2\plugins\junit\lib\junit-rt.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_71\jre\lib\rt.jar;D:\Vlad\Work\GitHub\high-performance-java-persistence\core\target\test-classes;D:\Vlad\Work\GitHub\high-performance-java-persistence\core\target\classes;C:\Users\Vlad\.m2\repository\org\slf4j\slf4j-api\1.7.7\slf4j-api-1.7.7.jar;C:\Users\Vlad\.m2\repository\ch\qos\logback\logback-classic\1.1.2\logback-classic-1.1.2.jar;C:\Users\Vlad\.m2\repository\ch\qos\logback\logback-core\1.1.2\logback-core-1.1.2.jar;C:\Users\Vlad\.m2\repository\org\apache\commons\commons-lang3\3.3.2\commons-lang3-3.3.2.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-core\5.1.0.Final\hibernate-core-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\org\jboss\logging\jboss-logging\3.3.0.Final\jboss-logging-3.3.0.Final.jar;C:\Users\Vlad\.m2\repository\org\hibernate\javax\persistence\hibernate-jpa-2.1-api\1.0.0.Final\hibernate-jpa-2.1-api-1.0.0.Final.jar;C:\Users\Vlad\.m2\repository\org\javassist\javassist\3.20.0-GA\javassist-3.20.0-GA.jar;C:\Users\Vlad\.m2\repository\antlr\antlr\2.7.7\antlr-2.7.7.jar;C:\Users\Vlad\.m2\repository\org\apache\geronimo\specs\geronimo-jta_1.1_spec\1.1.1\geronimo-jta_1.1_spec-1.1.1.jar;C:\Users\Vlad\.m2\repository\org\jboss\jandex\2.0.0.Final\jandex-2.0.0.Final.jar;C:\Users\Vlad\.m2\repository\com\fasterxml\classmate\1.3.0\classmate-1.3.0.jar;C:\Users\Vlad\.m2\repository\dom4j\dom4j\1.6.1\dom4j-1.6.1.jar;C:\Users\Vlad\.m2\repository\xml-apis\xml-apis\1.0.b2\xml-apis-1.0.b2.jar;C:\Users\Vlad\.m2\repository\org\hibernate\common\hibernate-commons-annotations\5.0.1.Final\hibernate-commons-annotations-5.0.1.Final.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-java8\5.1.0.Final\hibernate-java8-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-c3p0\5.1.0.Final\hibernate-c3p0-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\com\mchange\c3p0\0.9.2.1\c3p0-0.9.2.1.jar;C:\Users\Vlad\.m2\repository\com\mchange\mchange-commons-java\0.2.3.4\mchange-commons-java-0.2.3.4.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-entitymanager\5.1.0.Final\hibernate-entitymanager-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-hikaricp\5.1.0.Final\hibernate-hikaricp-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\com\zaxxer\HikariCP-java6\2.3.9\HikariCP-java6-2.3.9.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-spatial\5.1.0.Final\hibernate-spatial-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\org\geolatte\geolatte-geom\1.0.1\geolatte-geom-1.0.1.jar;C:\Users\Vlad\.m2\repository\com\vividsolutions\jts\1.13\jts-1.13.jar;C:\Users\Vlad\.m2\repository\org\apfloat\apfloat\1.8.2\apfloat-1.8.2.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-validator\5.2.3.Final\hibernate-validator-5.2.3.Final.jar;C:\Users\Vlad\.m2\repository\javax\validation\validation-api\1.1.0.Final\validation-api-1.1.0.Final.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-jpamodelgen\5.1.0.Final\hibernate-jpamodelgen-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\javax\el\javax.el-api\2.2.4\javax.el-api-2.2.4.jar;C:\Users\Vlad\.m2\repository\org\hibernate\hibernate-ehcache\5.1.0.Final\hibernate-ehcache-5.1.0.Final.jar;C:\Users\Vlad\.m2\repository\net\sf\ehcache\ehcache\2.10.1\ehcache-2.10.1.jar;C:\Users\Vlad\.m2\repository\net\sf\ehcache\ehcache-core\2.6.9\ehcache-core-2.6.9.jar;C:\Users\Vlad\.m2\repository\net\ttddyy\datasource-proxy\1.3.3\datasource-proxy-1.3.3.jar;C:\Users\Vlad\.m2\repository\p6spy\p6spy\2.1.4\p6spy-2.1.4.jar;C:\Users\Vlad\.m2\repository\org\hsqldb\hsqldb\2.3.2\hsqldb-2.3.2.jar;C:\Users\Vlad\.m2\repository\org\postgresql\postgresql\9.4-1202-jdbc41\postgresql-9.4-1202-jdbc41.jar;C:\Users\Vlad\.m2\repository\com\oracle\ojdbc7_g\12.1.0.1\ojdbc7_g-12.1.0.1.jar;C:\Users\Vlad\.m2\repository\mysql\mysql-connector-java\5.1.38\mysql-connector-java-5.1.38.jar;C:\Users\Vlad\.m2\repository\com\microsoft\sqlserver\sqljdbc4\4.0\sqljdbc4-4.0.jar;C:\Users\Vlad\.m2\repository\net\sourceforge\jtds\jtds\1.3.1\jtds-1.3.1.jar;C:\Users\Vlad\.m2\repository\io\dropwizard\metrics\metrics-core\3.1.0\metrics-core-3.1.0.jar;C:\Users\Vlad\.m2\repository\com\zaxxer\HikariCP\1.3.3\HikariCP-1.3.3.jar;C:\Users\Vlad\.m2\repository\org\codehaus\btm\btm\2.1.4\btm-2.1.4.jar;C:\Users\Vlad\.m2\repository\javax\transaction\jta\1.1\jta-1.1.jar;C:\Users\Vlad\.m2\repository\org\flywaydb\flyway-core\3.2.1\flyway-core-3.2.1.jar;C:\Users\Vlad\.m2\repository\com\vladmihalcea\flexy-pool\flexy-pool-core\1.2.4\flexy-pool-core-1.2.4.jar;C:\Users\Vlad\.m2\repository\com\vladmihalcea\flexy-pool\flexy-btm\1.2.4\flexy-btm-1.2.4.jar;C:\Users\Vlad\.m2\repository\com\vladmihalcea\flexy-pool\flexy-codahale-metrics\1.2.4\flexy-codahale-metrics-1.2.4.jar;C:\Users\Vlad\.m2\repository\com\vladmihalcea\flexy-pool\flexy-dropwizard-metrics\1.2.4\flexy-dropwizard-metrics-1.2.4.jar;C:\Users\Vlad\.m2\repository\com\vladmihalcea\db-util\0.0.1\db-util-0.0.1.jar;C:\Users\Vlad\.m2\repository\org\slf4j\jcl-over-slf4j\1.6.1\jcl-over-slf4j-1.6.1.jar;C:\Users\Vlad\.m2\repository\commons-lang\commons-lang\2.5\commons-lang-2.5.jar;C:\Users\Vlad\.m2\repository\org\aspectj\aspectjrt\1.8.7\aspectjrt-1.8.7.jar;C:\Users\Vlad\.m2\repository\org\aspectj\aspectjweaver\1.8.7\aspectjweaver-1.8.7.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-beans\4.2.3.RELEASE\spring-beans-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-core\4.2.3.RELEASE\spring-core-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\commons-logging\commons-logging\1.2\commons-logging-1.2.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-context\4.2.3.RELEASE\spring-context-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-aop\4.2.3.RELEASE\spring-aop-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\aopalliance\aopalliance\1.0\aopalliance-1.0.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-expression\4.2.3.RELEASE\spring-expression-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-tx\4.2.3.RELEASE\spring-tx-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-orm\4.2.3.RELEASE\spring-orm-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-jdbc\4.2.3.RELEASE\spring-jdbc-4.2.3.RELEASE.jar;C:\Users\Vlad\.m2\repository\junit\junit\4.11\junit-4.11.jar;C:\Users\Vlad\.m2\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar;C:\Users\Vlad\.m2\repository\org\springframework\spring-test\4.2.3.RELEASE\spring-test-4.2.3.RELEASE.jar" com.intellij.rt.execution.application.AppMain com.intellij.rt.execution.junit.JUnitStarter -ideVersion5 com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScoreFetchAllPerformanceTest DEBUG [Alice]: o.j.logging - Logging Provider: org.jboss.logging.Slf4jLoggerProvider INFO [Alice]: o.h.j.i.u.LogHelper - HHH000204: Processing PersistenceUnitInfo [ name: PostCommentScoreFetchAllPerformanceTest @@ -42,9 +42,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -110,9 +110,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -178,9 +178,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -246,9 +246,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -321,9 +321,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -401,9 +401,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -469,9 +469,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -537,9 +537,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -605,9 +605,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -680,9 +680,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -760,9 +760,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -828,9 +828,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -896,9 +896,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -964,9 +964,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -1039,9 +1039,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -1119,9 +1119,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -1187,9 +1187,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -1255,9 +1255,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -1323,9 +1323,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] @@ -1398,9 +1398,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:70) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/FetchProjection.txt b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/FetchProjection.txt similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/FetchProjection.txt rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/FetchProjection.txt index e4c7c46b7..8020b9d25 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/FetchProjection.txt +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/FetchProjection.txt @@ -50,9 +50,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:72) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:72) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreFetchAllPerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreFetchAllPerformanceTest.java similarity index 82% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreFetchAllPerformanceTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreFetchAllPerformanceTest.java index 388903769..d4b0ebb8d 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreFetchAllPerformanceTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreFetchAllPerformanceTest.java @@ -1,6 +1,7 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive.complex; +package com.vladmihalcea.hpjp.hibernate.query.recursive.complex; -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore; +import org.junit.Ignore; import java.util.*; import java.util.concurrent.TimeUnit; @@ -8,6 +9,7 @@ /** * @author Vlad Mihalcea */ +@Ignore public class PostCommentScoreFetchAllPerformanceTest extends AbstractPostCommentScorePerformanceTest { public PostCommentScoreFetchAllPerformanceTest(int postCount, int commentCount) { @@ -18,12 +20,15 @@ public PostCommentScoreFetchAllPerformanceTest(int postCount, int commentCount) protected List postCommentScores(Long postId, int rank) { return doInJPA(entityManager -> { long startNanos = System.nanoTime(); - List postCommentVotes = entityManager.createQuery( - "select pcv " + - "from PostCommentVote pcv " + - "left join fetch pcv.comment pc " + - "left join fetch pc.parent pcp " + - "where pc.post.id = :postId", PostCommentVote.class) + List postCommentVotes = entityManager.createQuery(""" + select pcv + from PostCommentVote pcv + left join fetch pcv.comment pc + left join fetch pc.parent pcp + join fetch pc.post p + where p.id = :postId + order by pc.id + """, PostCommentVote.class) .setParameter("postId", postId) .getResultList(); diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreFetchProjectionPerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreFetchProjectionPerformanceTest.java new file mode 100644 index 000000000..13c5370da --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreFetchProjectionPerformanceTest.java @@ -0,0 +1,61 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.complex; + +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore; +import org.junit.Ignore; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @author Vlad Mihalcea + */ +@Ignore +public class PostCommentScoreFetchProjectionPerformanceTest extends AbstractPostCommentScorePerformanceTest { + + public PostCommentScoreFetchProjectionPerformanceTest(int postCount, int commentCount) { + super(postCount, commentCount); + } + + @Override + protected List postCommentScores(Long postId, int rank) { + return doInJPA(entityManager -> { + long startNanos = System.nanoTime(); + List postCommentScores = entityManager.createQuery( + "select new com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore(" + + " pc.id, pc.parent.id, pc.review, pc.createdOn, sum( case when pcv.up is null then 0 when pcv.up = true then 1 else -1 end ) " + + ") " + + "from PostComment pc " + + "left join PostCommentVote pcv on pc.id = pcv.comment " + + "where pc.post.id = :postId " + + "group by pc.id, pc.parent.id, pc.review, pc.createdOn ") + .setParameter("postId", postId) + .getResultList(); + + Map> postCommentScoreMap = postCommentScores.stream().collect(Collectors.groupingBy(PostCommentScore::getId)); + + List roots = new ArrayList<>(); + + for(PostCommentScore postCommentScore : postCommentScores) { + Long parentId = postCommentScore.getParentId(); + if(parentId == null) { + roots.add(postCommentScore); + } else { + PostCommentScore parent = postCommentScoreMap.get(parentId).get(0); + parent.addChild(postCommentScore); + } + } + + roots.sort(Comparator.comparing(PostCommentScore::getTotalScore).reversed()); + + if(roots.size() > rank) { + roots = roots.subList(0, rank); + } + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + return roots; + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreRecursiveCTEPerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreRecursiveCTEPerformanceTest.java new file mode 100644 index 000000000..14641916e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreRecursiveCTEPerformanceTest.java @@ -0,0 +1,98 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.complex; + +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore; +import org.hibernate.query.NativeQuery; +import org.hibernate.transform.ResultTransformer; +import org.junit.Ignore; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * @author Vlad Mihalcea + */ +@Ignore +public class PostCommentScoreRecursiveCTEPerformanceTest extends AbstractPostCommentScorePerformanceTest { + + public PostCommentScoreRecursiveCTEPerformanceTest(int postCount, int commentCount) { + super(postCount, commentCount); + } + + @Override + protected List postCommentScores(Long postId, int rank) { + return doInJPA(entityManager -> { + long startNanos = System.nanoTime(); + List postCommentScores = entityManager.createNativeQuery(""" + SELECT id, parent_id, root_id, review, created_on, score + FROM ( + SELECT + id, parent_id, root_id, review, created_on, score, + dense_rank() OVER (ORDER BY total_score DESC) rank + FROM ( + SELECT + id, parent_id, root_id, review, created_on, score, + SUM(score) OVER ( + PARTITION BY root_id + ) total_score + FROM ( + WITH RECURSIVE post_comment_score(id, root_id, post_id, parent_id, review, created_on, user_id, score) AS ( + SELECT id, id, post_id, parent_id, review, created_on, user_id, + CASE WHEN up IS NULL THEN 0 WHEN up = true THEN 1 ELSE - 1 END score + FROM post_comment + LEFT JOIN post_comment_vote ON comment_id = id + WHERE post_id = :postId AND parent_id IS NULL + UNION ALL + select pc.id, pcs.root_id, pc.post_id, pc.parent_id, + pc.review, pc.created_on, pcv.user_id, CASE WHEN pcv.up IS NULL THEN 0 + WHEN pcv.up = true THEN 1 ELSE - 1 END score + FROM post_comment pc + LEFT JOIN post_comment_vote pcv ON pcv.comment_id = pc.id + INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id + ) + SELECT id, parent_id, root_id, review, created_on, SUM(score) score + FROM post_comment_score + GROUP BY id, parent_id, root_id, review, created_on + ) score_by_comment + ) score_total + ORDER BY total_score DESC, created_on ASC + ) total_score_group + WHERE rank <= :rank""", "PostCommentScore") + .unwrap(NativeQuery.class) + .setParameter("postId", postId).setParameter("rank", rank) + .setResultTransformer(new PostCommentScoreResultTransformer()) + .list(); + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + return postCommentScores; + }); + } + + public static class PostCommentScoreResultTransformer implements ResultTransformer { + + private Map postCommentScoreMap = new HashMap<>(); + + private List roots = new ArrayList<>(); + + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + PostCommentScore postCommentScore = (PostCommentScore) tuple[0]; + if(postCommentScore.getParentId() == null) { + roots.add(postCommentScore); + } else { + PostCommentScore parent = postCommentScoreMap.get(postCommentScore.getParentId()); + if(parent != null) { + parent.addChild(postCommentScore); + } + } + postCommentScoreMap.putIfAbsent(postCommentScore.getId(), postCommentScore); + return postCommentScore; + } + + @Override + public List transformList(List collection) { + return roots; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreRecursiveCTESelectPerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreRecursiveCTESelectPerformanceTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreRecursiveCTESelectPerformanceTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreRecursiveCTESelectPerformanceTest.java index 2b1e95533..1e599a143 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/PostCommentScoreRecursiveCTESelectPerformanceTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreRecursiveCTESelectPerformanceTest.java @@ -1,8 +1,9 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive.complex; +package com.vladmihalcea.hpjp.hibernate.query.recursive.complex; -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; -import org.hibernate.SQLQuery; +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore; +import org.hibernate.query.NativeQuery; import org.hibernate.transform.ResultTransformer; +import org.junit.Ignore; import java.util.ArrayList; import java.util.HashMap; @@ -13,6 +14,7 @@ /** * @author Vlad Mihalcea */ +@Ignore public class PostCommentScoreRecursiveCTESelectPerformanceTest extends AbstractPostCommentScorePerformanceTest { public PostCommentScoreRecursiveCTESelectPerformanceTest(int postCount, int commentCount) { @@ -46,7 +48,6 @@ protected List postCommentScores(Long postId, int rank) { " COALESCE(( SELECT SUM (CASE WHEN up = true THEN 1 ELSE - 1 END ) FROM post_comment_vote WHERE comment_id = pc.id ), 0) score " + " FROM post_comment pc " + " INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id " + - " WHERE pc.parent_id = pcs.id " + " ) " + " SELECT id, parent_id, root_id, review, created_on, score" + " FROM post_comment_score" + @@ -54,7 +55,8 @@ protected List postCommentScores(Long postId, int rank) { " ) score_total " + " ORDER BY total_score DESC, created_on ASC " + ") total_score_group " + - "WHERE rank <= :rank", "PostCommentScore").unwrap(SQLQuery.class) + "WHERE rank <= :rank", "PostCommentScore") + .unwrap(NativeQuery.class) .setParameter("postId", postId).setParameter("rank", rank) .setResultTransformer(new PostCommentScoreResultTransformer()) .list(); diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreTest.java new file mode 100644 index 000000000..92e2a51fc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/PostCommentScoreTest.java @@ -0,0 +1,594 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.complex; + +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.query.NativeQuery; +import org.hibernate.transform.ResultTransformer; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@Ignore +public class PostCommentScoreTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + User.class, + PostCommentVote.class, + }; + } + + @Override + public void init() { + super.init(); + doInJPA(entityManager -> { + User user1 = new User(); + user1.setUsername("JohnDoe"); + entityManager.persist(user1); + + User user2 = new User(); + user2.setUsername("JohnDoeJr"); + entityManager.persist(user2); + + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + PostComment comment1 = new PostComment(); + comment1.setPost(post); + comment1.setReview("Comment 1"); + entityManager.persist(comment1); + + PostCommentVote user1Comment1 = new PostCommentVote(user1, comment1); + user1Comment1.setUp(true); + entityManager.persist(user1Comment1); + + PostComment comment1_1 = new PostComment(); + comment1_1.setParent(comment1); + comment1_1.setPost(post); + comment1_1.setReview("Comment 1_1"); + entityManager.persist(comment1_1); + + PostCommentVote user1Comment1_1 = new PostCommentVote(user1, comment1_1); + user1Comment1_1.setUp(true); + entityManager.persist(user1Comment1_1); + + PostCommentVote user2Comment1_1 = new PostCommentVote(user2, comment1_1); + user2Comment1_1.setUp(true); + entityManager.persist(user2Comment1_1); + + PostComment comment1_2 = new PostComment(); + comment1_2.setParent(comment1); + comment1_2.setPost(post); + comment1_2.setReview("Comment 1_2"); + entityManager.persist(comment1_2); + + PostCommentVote user1Comment1_2 = new PostCommentVote(user1, comment1_2); + user1Comment1_2.setUp(true); + entityManager.persist(user1Comment1_2); + + PostCommentVote user2Comment1_3 = new PostCommentVote(user2, comment1_2); + user2Comment1_3.setUp(true); + entityManager.persist(user2Comment1_3); + + PostComment comment1_2_1 = new PostComment(); + comment1_2_1.setParent(comment1_2); + comment1_2_1.setPost(post); + comment1_2_1.setReview("Comment 1_2_1"); + entityManager.persist(comment1_2_1); + + PostCommentVote user1Comment1_2_1 = new PostCommentVote(user1, comment1_2_1); + user1Comment1_2_1.setUp(true); + entityManager.persist(user1Comment1_2_1); + + PostComment comment2 = new PostComment(); + comment2.setPost(post); + comment2.setReview("Comment 2"); + entityManager.persist(comment2); + + PostCommentVote user1Comment2 = new PostCommentVote(user1, comment2); + user1Comment2.setUp(true); + entityManager.persist(user1Comment2); + + PostComment comment2_1 = new PostComment(); + comment2_1.setParent(comment2); + comment2_1.setPost(post); + comment2_1.setReview("Comment 2_1"); + entityManager.persist(comment2_1); + + PostCommentVote user1Comment2_1 = new PostCommentVote(user1, comment2_1); + user1Comment2_1.setUp(true); + entityManager.persist(user1Comment2_1); + + PostCommentVote user2Comment2_1 = new PostCommentVote(user2, comment2_1); + user2Comment2_1.setUp(true); + entityManager.persist(user2Comment2_1); + + PostComment comment2_2 = new PostComment(); + comment2_2.setParent(comment2); + comment2_2.setPost(post); + comment2_2.setReview("Comment 2_2"); + entityManager.persist(comment2_2); + + PostCommentVote user1Comment2_2 = new PostCommentVote(user1, comment2_2); + user1Comment2_2.setUp(true); + entityManager.persist(user1Comment2_2); + + PostComment comment3 = new PostComment(); + comment3.setPost(post); + comment3.setReview("Comment 3"); + entityManager.persist(comment3); + + PostCommentVote user1Comment3 = new PostCommentVote(user1, comment3); + user1Comment3.setUp(true); + entityManager.persist(user1Comment3); + + PostComment comment3_1 = new PostComment(); + comment3_1.setParent(comment3); + comment3_1.setPost(post); + comment3_1.setReview("Comment 3_1"); + entityManager.persist(comment3_1); + + PostCommentVote user1Comment3_1 = new PostCommentVote(user1, comment3_1); + user1Comment3_1.setUp(true); + entityManager.persist(user1Comment3_1); + + PostCommentVote user2Comment3_1 = new PostCommentVote(user2, comment3_1); + user2Comment3_1.setUp(false); + entityManager.persist(user2Comment3_1); + + PostComment comment3_2 = new PostComment(); + comment3_2.setParent(comment3); + comment3_2.setPost(post); + comment3_2.setReview("Comment 3_2"); + entityManager.persist(comment3_2); + + PostCommentVote user1Comment3_2 = new PostCommentVote(user1, comment3_2); + user1Comment3_2.setUp(true); + entityManager.persist(user1Comment3_2); + + PostComment comment4 = new PostComment(); + comment4.setPost(post); + comment4.setReview("Comment 4"); + entityManager.persist(comment4); + + PostCommentVote user1Comment4 = new PostCommentVote(user1, comment4); + user1Comment4.setUp(false); + entityManager.persist(user1Comment4); + + PostComment comment5 = new PostComment(); + comment5.setPost(post); + comment5.setReview("Comment 5"); + entityManager.persist(comment5); + + entityManager.flush(); + + }); + } + + @Test + public void test() { + LOGGER.info("Recursive CTE and Window Functions"); + Long postId = 1L; + int rank = 3; + List resultCTEJoin = postCommentScoresCTEJoin(postId, rank); + assertEquals(3, resultCTEJoin.size()); + + List resultCTESelect = postCommentScoresCTESelect(postId, rank); + assertEquals(3, resultCTESelect.size()); + + List resultInMemory = postCommentScoresInMemory(postId, rank); + assertEquals(3, resultInMemory.size()); + + for (int i = 0; i < resultCTEJoin.size(); i++) { + assertEquals(resultCTEJoin.get(i).getTotalScore(), resultInMemory.get(i).getTotalScore()); + assertEquals(resultCTEJoin.get(i).getTotalScore(), resultCTESelect.get(i).getTotalScore()); + } + } + + private List postCommentScoresCTEJoin(Long postId, int rank) { + return doInJPA(entityManager -> { + List postCommentScores = entityManager.createNativeQuery( + "SELECT id, parent_id, root_id, review, created_on, score " + + "FROM ( " + + " SELECT " + + " id, parent_id, root_id, review, created_on, score, " + + " dense_rank() OVER (ORDER BY total_score DESC) rank " + + " FROM ( " + + " SELECT " + + " id, parent_id, root_id, review, created_on, score, " + + " SUM(score) OVER (PARTITION BY root_id) total_score " + + " FROM (" + + " WITH RECURSIVE post_comment_score(id, root_id, post_id, " + + " parent_id, review, created_on, user_id, score) AS (" + + " SELECT id, id, post_id, parent_id, review, created_on, user_id, " + + " CASE WHEN up IS NULL THEN 0 WHEN up = true THEN 1 " + + " ELSE - 1 END score " + + " FROM post_comment " + + " LEFT JOIN post_comment_vote ON comment_id = id " + + " WHERE post_id = :postId AND parent_id IS NULL " + + " UNION ALL " + + " select pc.id, pcs.root_id, pc.post_id, pc.parent_id, " + + " pc.review, pc.created_on, pcv.user_id, CASE WHEN pcv.up IS NULL THEN 0 " + + " WHEN pcv.up = true THEN 1 ELSE - 1 END score " + + " FROM post_comment pc " + + " LEFT JOIN post_comment_vote pcv ON pcv.comment_id = pc.id " + + " INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id " + + " WHERE pc.parent_id = pcs.id " + + " ) " + + " SELECT id, parent_id, root_id, review, created_on, SUM(score) score" + + " FROM post_comment_score " + + " GROUP BY id, parent_id, root_id, review, created_on" + + " ) score_by_comment " + + " ) score_total " + + " ORDER BY total_score DESC, created_on ASC " + + ") total_score_group " + + "WHERE rank <= :rank", "PostCommentScore") + .unwrap(NativeQuery.class) + .setParameter("postId", postId).setParameter("rank", rank) + .setResultTransformer(new PostCommentScoreResultTransformer()) + .list(); + return postCommentScores; + }); + } + + private List postCommentScoresCTESelect(Long postId, int rank) { + return doInJPA(entityManager -> { + List postCommentScores = entityManager.createNativeQuery( + "SELECT id, parent_id, root_id, review, created_on, score " + + "FROM ( " + + " SELECT " + + " id, parent_id, root_id, review, created_on, score, " + + " dense_rank() OVER (ORDER BY total_score DESC) rank " + + " FROM ( " + + " SELECT " + + " id, parent_id, root_id, review, created_on, score, " + + " SUM(score) OVER (PARTITION BY root_id) total_score " + + " FROM (" + + " WITH RECURSIVE post_comment_score(id, root_id, post_id, " + + " parent_id, review, created_on, score) AS (" + + " SELECT id, id, post_id, parent_id, review, created_on, " + + " COALESCE (( SELECT SUM (CASE WHEN up = true THEN 1 ELSE - 1 END ) FROM post_comment_vote WHERE comment_id = id ), 0) score " + + " FROM post_comment " + + " WHERE post_id = :postId AND parent_id IS NULL " + + " UNION ALL " + + " SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, " + + " pc.review, pc.created_on, " + + " COALESCE(( SELECT SUM (CASE WHEN up = true THEN 1 ELSE - 1 END ) FROM post_comment_vote WHERE comment_id = pc.id ), 0) score " + + " FROM post_comment pc " + + " INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id " + + " WHERE pc.parent_id = pcs.id " + + " ) " + + " SELECT id, parent_id, root_id, review, created_on, score" + + " FROM post_comment_score" + + " ) score_by_comment " + + " ) score_total " + + " ORDER BY total_score DESC, created_on ASC " + + ") total_score_group " + + "WHERE rank <= :rank", "PostCommentScore") + .unwrap(NativeQuery.class) + .setParameter("postId", postId).setParameter("rank", rank) + .setResultTransformer(new PostCommentScoreResultTransformer()) + .list(); + return postCommentScores; + }); + } + + protected List postCommentScoresInMemory(Long postId, int rank) { + return doInJPA(entityManager -> { + List postCommentScores = entityManager.createQuery( + "select new com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore(" + + " pc.id, pc.parent.id, 0, pc.review, pc.createdOn, sum( case when pcv.up is null then 0 when pcv.up = true then 1 else -1 end ) " + + ") " + + "from PostComment pc " + + "left join PostCommentVote pcv on pc.id = pcv.comment " + + "where pc.post.id = :postId " + + "group by pc.id, pc.parent.id, pc.review, pc.createdOn ") + .setParameter("postId", postId) + .getResultList(); + + Map> postCommentScoreMap = postCommentScores.stream().collect(Collectors.groupingBy(PostCommentScore::getId)); + + List roots = new ArrayList<>(); + + for(PostCommentScore postCommentScore : postCommentScores) { + Long parentId = postCommentScore.getParentId(); + if(parentId == null) { + roots.add(postCommentScore); + } else { + PostCommentScore parent = postCommentScoreMap.get(parentId).get(0); + parent.addChild(postCommentScore); + } + } + + roots.sort(Comparator.comparing(PostCommentScore::getTotalScore).reversed()); + + if(roots.size() > rank) { + roots = roots.subList(0, rank); + } + return roots; + }); + } + + public static class PostCommentScoreResultTransformer implements ResultTransformer { + + private Map postCommentScoreMap = new HashMap<>(); + + private List roots = new ArrayList<>(); + + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + PostCommentScore postCommentScore = (PostCommentScore) tuple[0]; + if(postCommentScore.getParentId() == null) { + roots.add(postCommentScore); + } else { + PostCommentScore parent = postCommentScoreMap.get(postCommentScore.getParentId()); + if(parent != null) { + parent.addChild(postCommentScore); + } + } + postCommentScoreMap.putIfAbsent(postCommentScore.getId(), postCommentScore); + return postCommentScore; + } + + @Override + public List transformList(List collection) { + return roots; + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @SqlResultSetMapping( + name = "PostCommentScore", + classes = @ConstructorResult( + targetClass = PostCommentScore.class, + columns = { + @ColumnResult(name = "id"), + @ColumnResult(name = "parent_id"), + @ColumnResult(name = "root_id"), + @ColumnResult(name = "review"), + @ColumnResult(name = "created_on"), + @ColumnResult(name = "score") + } + ) + ) + public static class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne + @JoinColumn(name = "parent_id") + private PostComment parent; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn = new Date(); + + private String review; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public PostComment getParent() { + return parent; + } + + public void setParent(PostComment parent) { + this.parent = parent; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostComment that = (PostComment) o; + return Objects.equals(getPost(), that.getPost()) && + Objects.equals(getParent(), that.getParent()) && + Objects.equals(getReview(), that.getReview()); + } + + @Override + public int hashCode() { + return Objects.hash(getPost(), getParent(), getReview()); + } + + @Override + public String toString() { + return "PostComment{" + + "review='" + review + '\'' + + ", post=" + post + + '}'; + } + } + + @Entity(name = "User") + @Table(name = "forum_user") + public static class User { + + @Id + @GeneratedValue + private Long id; + + private String username; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return Objects.equals(getUsername(), user.getUsername()); + } + + @Override + public int hashCode() { + return Objects.hash(getUsername()); + } + + @Override + public String toString() { + return "User{" + + "username='" + username + '\'' + + '}'; + } + } + + @Entity(name = "PostCommentVote") + @Table(name = "post_comment_vote") + public static class PostCommentVote implements Serializable { + + @Id + @ManyToOne + private User user; + + @Id + @ManyToOne + private PostComment comment; + + private boolean up; + + private PostCommentVote() { + } + + public PostCommentVote(User user, PostComment comment) { + this.user = user; + this.comment = comment; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public PostComment getComment() { + return comment; + } + + public void setComment(PostComment comment) { + this.comment = comment; + } + + public boolean isUp() { + return up; + } + + public void setUp(boolean up) { + this.up = up; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostCommentVote that = (PostCommentVote) o; + return Objects.equals(getUser(), that.getUser()) && + Objects.equals(getComment(), that.getComment()); + } + + @Override + public int hashCode() { + return Objects.hash(getUser(), getComment()); + } + + @Override + public String toString() { + return "PostCommentVote{" + + "user=" + user + + ", comment=" + comment + + '}'; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/RecursiveCTE.txt b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/RecursiveCTE.txt similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/RecursiveCTE.txt rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/RecursiveCTE.txt index f6a482325..a14857c75 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/RecursiveCTE.txt +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/RecursiveCTE.txt @@ -50,9 +50,9 @@ org.hibernate.tool.schema.spi.CommandAcceptanceException: Unable to execute comm at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:458) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final] - at com.vladmihalcea.book.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] - at com.vladmihalcea.book.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] - at com.vladmihalcea.book.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:72) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.newEntityManagerFactory(AbstractTest.java:742) [test-classes/:na] + at com.vladmihalcea.hpjp.util.AbstractTest.init(AbstractTest.java:651) [test-classes/:na] + at com.vladmihalcea.hpjp.hibernate.query.recursive.AbstractPostCommentScorePerformanceTest.init(AbstractPostCommentScorePerformanceTest.java:72) [test-classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_71] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_71] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_71] diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/cte.sql b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/cte.sql similarity index 100% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/complex/cte.sql rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/complex/cte.sql diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/AbstractPostCommentScorePerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/AbstractPostCommentScorePerformanceTest.java new file mode 100644 index 000000000..e436f66b2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/AbstractPostCommentScorePerformanceTest.java @@ -0,0 +1,149 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.simple; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public abstract class AbstractPostCommentScorePerformanceTest extends AbstractPostgreSQLIntegrationTest { + + protected MetricRegistry metricRegistry = new MetricRegistry(); + + protected com.codahale.metrics.Timer timer = metricRegistry.timer(getClass().getSimpleName()); + + protected Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .build(); + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + }; + } + + private int postCount; + private int commentCount; + + public AbstractPostCommentScorePerformanceTest(int postCount, int commentCount) { + this.postCount = postCount; + this.commentCount = commentCount; + } + + @Parameterized.Parameters + public static Collection parameters() { + List postCountSizes = new ArrayList<>(); + int postCount = 2; + postCountSizes.add(new Integer[] {postCount, 16}); + postCountSizes.add(new Integer[] {postCount, 4}); + postCountSizes.add(new Integer[] {postCount, 8}); + postCountSizes.add(new Integer[] {postCount, 16}); + postCountSizes.add(new Integer[] {postCount, 24}); + postCountSizes.add(new Integer[] {postCount, 32}); + postCountSizes.add(new Integer[] {postCount, 48}); + postCountSizes.add(new Integer[] {postCount, 64}); + return postCountSizes; + } + + @Override + public void init() { + super.init(); + for (long i = 0; i < postCount; i++) { + insertPost(i); + } + } + + private int randomScore() { + double random = Math.random() + 10; + return (int) random; + } + + private void insertPost(Long postId) { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(postId); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + for (int i = 0; i < commentCount; i++) { + PostComment comment1 = new PostComment(); + comment1.setPost(post); + comment1.setReview(String.format("Comment %d", i)); + comment1.setScore(randomScore()); + entityManager.persist(comment1); + + for (int j = 0; j < commentCount / 2; j++) { + PostComment comment1_1 = new PostComment(); + comment1_1.setParent(comment1); + comment1_1.setPost(post); + comment1_1.setReview(String.format("Comment %d-%d", i, j)); + comment1_1.setScore(randomScore()); + entityManager.persist(comment1_1); + + for (int k = 0; k < commentCount / 4 ; k++) { + PostComment comment1_1_1 = new PostComment(); + comment1_1_1.setParent(comment1_1_1); + comment1_1_1.setPost(post); + comment1_1_1.setReview(String.format("Comment %d-%d-%d", i, j, k)); + comment1_1_1.setScore(randomScore()); + entityManager.persist(comment1_1_1); + } + entityManager.flush(); + } + entityManager.flush(); + entityManager.clear(); + } + + LOGGER.info("Added {} PostComments", entityManager + .createQuery("select count(pc) from PostComment pc where pc.post = :post") + .setParameter("post", post) + .getSingleResult() + ); + }); + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_versioned_data", "true"); + return properties; + } + + @Test + @Ignore + public void test() { + int rank = 3; + int iterations = 25; + for (int i = 0; i < iterations; i++) { + for (long postId = 0; postId < postCount; postId++) { + List result = postCommentScores(postId, rank); + assertNotNull(result); + } + } + logReporter.report(); + } + + protected abstract List postCommentScores(Long postId, int rank); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/Post.java new file mode 100644 index 000000000..d384b04ad --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/Post.java @@ -0,0 +1,34 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.simple; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostComment.java new file mode 100644 index 000000000..6370a7c59 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostComment.java @@ -0,0 +1,95 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.simple; + +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore; + +import jakarta.persistence.*; +import java.util.Date; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +@SqlResultSetMapping( + name = "PostCommentScore", + classes = @ConstructorResult( + targetClass = PostCommentScore.class, + columns = { + @ColumnResult(name = "id"), + @ColumnResult(name = "parent_id"), + @ColumnResult(name = "review"), + @ColumnResult(name = "created_on"), + @ColumnResult(name = "score") + } + ) +) +public class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne + @JoinColumn(name = "parent_id") + private PostComment parent; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn = new Date(); + + private String review; + + private int score; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public PostComment getParent() { + return parent; + } + + public void setParent(PostComment parent) { + this.parent = parent; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionOrderByPerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionOrderByPerformanceTest.java similarity index 79% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionOrderByPerformanceTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionOrderByPerformanceTest.java index b1a19eb50..e2dc51983 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionOrderByPerformanceTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionOrderByPerformanceTest.java @@ -1,7 +1,6 @@ -package com.vladmihalcea.book.hpjp.hibernate.query.recursive.simple; +package com.vladmihalcea.hpjp.hibernate.query.recursive.simple; -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.simple.AbstractPostCommentScorePerformanceTest; +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore; import java.util.*; import java.util.concurrent.TimeUnit; @@ -21,13 +20,15 @@ public PostCommentScoreFetchProjectionOrderByPerformanceTest(int postCount, int protected List postCommentScores(Long postId, int rank) { return doInJPA(entityManager -> { long startNanos = System.nanoTime(); - List postCommentScores = entityManager.createQuery( - "select new " + - " com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore(" + - " pc.id, pc.parent.id, pc.review, pc.createdOn, pc.score ) " + - "from PostComment pc " + - "where pc.post.id = :postId " + - "order by pc.id ") + List postCommentScores = entityManager.createQuery(""" + select new + com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore( + pc.id, pc.parent.id, pc.review, pc.createdOn, pc.score + ) + from PostComment pc + where pc.post.id = :postId + order by pc.id + """) .setParameter("postId", postId) .getResultList(); diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionPerformanceStreamTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionPerformanceStreamTest.java new file mode 100644 index 000000000..5250a0ddf --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionPerformanceStreamTest.java @@ -0,0 +1,67 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.simple; + +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.groupingBy; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentScoreFetchProjectionPerformanceStreamTest extends AbstractPostCommentScorePerformanceTest { + + protected com.codahale.metrics.Timer inMemoryProcessingTimer = metricRegistry.timer("In-memory processing timer"); + + public PostCommentScoreFetchProjectionPerformanceStreamTest(int postCount, int commentCount) { + super(postCount, commentCount); + } + + @Override + protected List postCommentScores(Long postId, int rank) { + long startNanos = System.nanoTime(); + AtomicLong startInMemoryProcessingNanos = new AtomicLong(); + List roots = doInJPA(entityManager -> { + List postCommentScores = entityManager.createQuery(""" + select new + com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore( + pc.id, pc.parent.id, pc.review, pc.createdOn, pc.score + ) + from PostComment pc + where pc.post.id = :postId + """) + .setParameter("postId", postId) + .getResultList(); + + startInMemoryProcessingNanos.set(System.nanoTime()); + + Map postCommentScoreMap = postCommentScores + .stream() + .collect(Collectors.toMap(PostCommentScore::getId, Function.identity())); + + List postCommentRoots = postCommentScores + .stream() + .filter(pcs -> { + boolean isRoot = pcs.getParentId() == null; + if(!isRoot) { + postCommentScoreMap.get(pcs.getParentId()).addChild(pcs); + } + return isRoot; + }) + .sorted( + Comparator.comparing(PostCommentScore::getTotalScore).reversed() + ) + .limit(rank) + .collect(Collectors.toList()); + + return postCommentRoots; + }); + inMemoryProcessingTimer.update(System.nanoTime() - startInMemoryProcessingNanos.get(), TimeUnit.NANOSECONDS); + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + return roots; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionPerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionPerformanceTest.java new file mode 100644 index 000000000..363d622bf --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreFetchProjectionPerformanceTest.java @@ -0,0 +1,70 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.simple; + +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentScoreFetchProjectionPerformanceTest extends AbstractPostCommentScorePerformanceTest { + + protected com.codahale.metrics.Timer inMemoryProcessingTimer = metricRegistry.timer("In-memory processing timer"); + + public PostCommentScoreFetchProjectionPerformanceTest(int postCount, int commentCount) { + super(postCount, commentCount); + } + + @Override + protected List postCommentScores(Long postId, int rank) { + return doInJPA(entityManager -> { + long startNanos = System.nanoTime(); + List postCommentScores = entityManager.createQuery(""" + select new + com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore( + pc.id, pc.parent.id, pc.review, pc.createdOn, pc.score + ) + from PostComment pc + where pc.post.id = :postId + order by pc.id + """) + .setParameter("postId", postId) + .getResultList(); + + long startInMemoryProcessingNanos = System.nanoTime(); + List roots = new ArrayList<>(); + + if (!postCommentScores.isEmpty()) { + Map postCommentScoreMap = new HashMap<>(); + for(PostCommentScore postCommentScore : postCommentScores) { + Long id = postCommentScore.getId(); + if (!postCommentScoreMap.containsKey(id)) { + postCommentScoreMap.put(id, postCommentScore); + } + } + + for(PostCommentScore postCommentScore : postCommentScores) { + Long parentId = postCommentScore.getParentId(); + if(parentId == null) { + roots.add(postCommentScore); + } else { + PostCommentScore parent = postCommentScoreMap.get(parentId); + parent.addChild(postCommentScore); + } + } + + roots.sort( + Comparator.comparing(PostCommentScore::getTotalScore).reversed() + ); + + if(roots.size() > rank) { + roots = roots.subList(0, rank); + } + } + inMemoryProcessingTimer.update(System.nanoTime() - startInMemoryProcessingNanos, TimeUnit.NANOSECONDS); + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + return roots; + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreRecursiveCTEPerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreRecursiveCTEPerformanceTest.java new file mode 100644 index 000000000..91de3b58d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreRecursiveCTEPerformanceTest.java @@ -0,0 +1,62 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.simple; + +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore; +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScoreResultTransformer; +import org.hibernate.query.NativeQuery; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentScoreRecursiveCTEPerformanceTest extends AbstractPostCommentScorePerformanceTest { + + public PostCommentScoreRecursiveCTEPerformanceTest(int postCount, int commentCount) { + super(postCount, commentCount); + } + + @Override + protected List postCommentScores(Long postId, int rank) { + return doInJPA(entityManager -> { + long startNanos = System.nanoTime(); + List postCommentScores = entityManager.createNativeQuery(""" + SELECT id, parent_id, root_id, review, created_on, score + FROM ( + SELECT + id, parent_id, root_id, review, created_on, score, + DENSE_RANK() OVER (ORDER BY total_score DESC) rank + FROM ( + SELECT + id, parent_id, root_id, review, created_on, score, + SUM(score) OVER (PARTITION BY root_id) total_score + FROM ( + WITH RECURSIVE post_comment_score(id, root_id, post_id, parent_id, review, created_on, score) AS ( + SELECT + id, id, post_id, parent_id, review, created_on, score + FROM post_comment + WHERE post_id = :postId AND parent_id IS NULL + UNION ALL + SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, + pc.review, pc.created_on, pc.score + FROM post_comment pc + INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id + WHERE pc.parent_id = pcs.id + ) + SELECT id, parent_id, root_id, review, created_on, score + FROM post_comment_score + ) score_by_comment + ) score_total + ORDER BY total_score DESC, id ASC + ) total_score_group + WHERE rank <= :rank""", "PostCommentScore") + .setParameter("postId", postId) + .setParameter("rank", rank) + .unwrap(NativeQuery.class) + .setResultTransformer(new PostCommentScoreResultTransformer()) + .getResultList(); + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + return postCommentScores; + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreTest.java new file mode 100644 index 000000000..d127d3622 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/recursive/simple/PostCommentScoreTest.java @@ -0,0 +1,238 @@ +package com.vladmihalcea.hpjp.hibernate.query.recursive.simple; + +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScore; +import com.vladmihalcea.hpjp.hibernate.query.recursive.PostCommentScoreResultTransformer; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentScoreTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + public void afterInit() { + initData(); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator(Collections.singletonList(PostCommentScore.class)) + ) + ); + } + + protected void initData() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + PostComment comment1 = new PostComment(); + comment1.setPost(post); + comment1.setReview("Comment 1"); + comment1.setScore(1); + entityManager.persist(comment1); + + PostComment comment1_1 = new PostComment(); + comment1_1.setParent(comment1); + comment1_1.setPost(post); + comment1_1.setReview("Comment 1_1"); + comment1_1.setScore(2); + entityManager.persist(comment1_1); + + PostComment comment1_2 = new PostComment(); + comment1_2.setParent(comment1); + comment1_2.setPost(post); + comment1_2.setReview("Comment 1_2"); + comment1_2.setScore(2); + entityManager.persist(comment1_2); + + PostComment comment1_2_1 = new PostComment(); + comment1_2_1.setParent(comment1_2); + comment1_2_1.setPost(post); + comment1_2_1.setReview("Comment 1_2_1"); + comment1_2_1.setScore(1); + entityManager.persist(comment1_2_1); + + PostComment comment2 = new PostComment(); + comment2.setPost(post); + comment2.setReview("Comment 2"); + comment2.setScore(1); + entityManager.persist(comment2); + + PostComment comment2_1 = new PostComment(); + comment2_1.setParent(comment2); + comment2_1.setPost(post); + comment2_1.setReview("Comment 2_1"); + comment2_1.setScore(1); + entityManager.persist(comment2_1); + + PostComment comment2_2 = new PostComment(); + comment2_2.setParent(comment2); + comment2_2.setPost(post); + comment2_2.setReview("Comment 2_2"); + comment2_2.setScore(1); + entityManager.persist(comment2_2); + + PostComment comment3 = new PostComment(); + comment3.setPost(post); + comment3.setReview("Comment 3"); + comment3.setScore(1); + entityManager.persist(comment3); + + PostComment comment3_1 = new PostComment(); + comment3_1.setParent(comment3); + comment3_1.setPost(post); + comment3_1.setReview("Comment 3_1"); + comment3_1.setScore(10); + entityManager.persist(comment3_1); + + PostComment comment3_2 = new PostComment(); + comment3_2.setParent(comment3); + comment3_2.setPost(post); + comment3_2.setReview("Comment 3_2"); + comment3_2.setScore(-2); + entityManager.persist(comment3_2); + + PostComment comment4 = new PostComment(); + comment4.setPost(post); + comment4.setReview("Comment 4"); + comment4.setScore(-5); + entityManager.persist(comment4); + + PostComment comment5 = new PostComment(); + comment5.setPost(post); + comment5.setReview("Comment 5"); + entityManager.persist(comment5); + + entityManager.flush(); + + }); + } + + @Test + public void test() { + LOGGER.info("Recursive CTE and Window Functions"); + Long postId = 1L; + int rank = 3; + List resultCTEJoin = postCommentScoresCTEJoin(postId, rank); + assertEquals(3, resultCTEJoin.size()); + + List resultInMemory = postCommentScoresInMemory(postId, rank); + assertEquals(3, resultInMemory.size()); + + for (int i = 0; i < resultCTEJoin.size(); i++) { + assertEquals(resultCTEJoin.get(i).getTotalScore(), resultInMemory.get(i).getTotalScore()); + } + } + + protected List postCommentScoresCTEJoin(Long postId, int rank) { + return doInJPA(entityManager -> { + List postCommentScores = entityManager.createNativeQuery(""" + SELECT id, parent_id, review, created_on, score + FROM ( + SELECT + id, parent_id, review, created_on, score, + dense_rank() OVER (ORDER BY total_score DESC) rank + FROM ( + SELECT + id, parent_id, review, created_on, score, + SUM(score) OVER (PARTITION BY root_id) total_score + FROM ( + WITH RECURSIVE post_comment_score(id, root_id, post_id, + parent_id, review, created_on, score) AS ( + SELECT + id, id, post_id, parent_id, review, created_on, score + FROM post_comment + WHERE post_id = :postId AND parent_id IS NULL + UNION ALL + SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, + pc.review, pc.created_on, pc.score + FROM post_comment pc + INNER JOIN post_comment_score pcs + ON pc.parent_id = pcs.id + WHERE pc.parent_id = pcs.id + ) + SELECT id, parent_id, root_id, review, created_on, score + FROM post_comment_score + ) score_by_comment + ) score_total + ORDER BY total_score DESC, id ASC + ) total_score_group + WHERE rank <= :rank + """, "PostCommentScore") + .unwrap(NativeQuery.class) + .setParameter("postId", postId) + .setParameter("rank", rank) + .setResultTransformer(new PostCommentScoreResultTransformer()) + .list(); + return postCommentScores; + }); + } + + protected List postCommentScoresInMemory(Long postId, int rank) { + return doInJPA(entityManager -> { + List postCommentScores = entityManager.createQuery(""" + select new PostCommentScore( + pc.id, pc.parent.id, pc.review, pc.createdOn, pc.score + ) + from PostComment pc + where pc.post.id = :postId + """) + .setParameter("postId", postId) + .getResultList(); + + List roots = new ArrayList<>(); + + if (!postCommentScores.isEmpty()) { + Map postCommentScoreMap = new HashMap<>(); + for(PostCommentScore postCommentScore : postCommentScores) { + Long id = postCommentScore.getId(); + if (!postCommentScoreMap.containsKey(id)) { + postCommentScoreMap.put(id, postCommentScore); + } + } + + for(PostCommentScore postCommentScore : postCommentScores) { + Long parentId = postCommentScore.getParentId(); + if(parentId == null) { + roots.add(postCommentScore); + } else { + PostCommentScore parent = postCommentScoreMap.get(parentId); + parent.addChild(postCommentScore); + } + } + + roots.sort( + Comparator.comparing(PostCommentScore::getTotalScore).reversed() + ); + + if(roots.size() > rank) { + roots = roots.subList(0, rank); + } + } + return roots; + }); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/sets/UnionIntersectExceptTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/sets/UnionIntersectExceptTest.java new file mode 100644 index 000000000..0a80fb238 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/sets/UnionIntersectExceptTest.java @@ -0,0 +1,241 @@ +package com.vladmihalcea.hpjp.hibernate.query.sets; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class UnionIntersectExceptTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Tag.class, + Category.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + private List categories = List.of( + "Java", + "JPA", + "jOOQ", + "Spring" + ); + + private List tags = List.of( + "Hibernate", + "JDBC", + "JPA", + "jOOQ", + "Spring" + ); + + @Override + public void afterInit() { + doInJPA(entityManager -> { + for (String category : categories) { + entityManager.persist(new Category().setName(category)); + } + for (String tag : tags) { + entityManager.persist(new Tag().setName(tag)); + } + }); + } + + @Test + public void testUnionAll() { + List _topics = doInJPA(entityManager -> { + List topics = entityManager.createQuery(""" + select c.name as name + from Category c + union all + select t.name as name + from Tag t + """, String.class) + .getResultList(); + + assertEquals(9, topics.size()); + + return topics; + }); + + List topics = Stream + .concat(categories.stream(), tags.stream()) + .toList(); + + Collections.sort(_topics, String::compareToIgnoreCase); + topics = new ArrayList<>(topics); + Collections.sort(topics, String::compareToIgnoreCase); + + assertEquals(_topics, topics); + } + + @Test + public void testUnion() { + List _topics = doInJPA(entityManager -> { + List topics = entityManager.createQuery(""" + select c.name as name + from Category c + union + select t.name as name + from Tag t + """, String.class) + .getResultList(); + + assertEquals(6, topics.size()); + + return topics; + }); + + List topics = Stream + .concat(categories.stream(), tags.stream()) + .distinct() + .toList(); + + Collections.sort(_topics, String::compareToIgnoreCase); + topics = new ArrayList<>(topics); + Collections.sort(topics, String::compareToIgnoreCase); + + assertEquals(_topics, topics); + } + + @Test + public void testIntersect() { + List _topics = doInJPA(entityManager -> { + List topics = entityManager.createQuery(""" + select c.name as name + from Category c + intersect + select t.name as name + from Tag t + """, String.class) + .getResultList(); + + assertEquals(3, topics.size()); + + return topics; + }); + + List topics = categories + .stream() + .filter(tags::contains) + .distinct() + .toList(); + + Collections.sort(_topics, String::compareToIgnoreCase); + topics = new ArrayList<>(topics); + Collections.sort(topics, String::compareToIgnoreCase); + + assertEquals(_topics, topics); + } + + @Test + public void testExcept() { + List _topics = doInJPA(entityManager -> { + List topics = entityManager.createQuery(""" + select c.name as name + from Category c + except + select t.name as name + from Tag t + """, String.class) + .getResultList(); + + assertEquals(1, topics.size()); + + return topics; + }); + + List topics = categories + .stream() + .filter(Predicate.not(tags::contains)) + .distinct() + .toList(); + + Collections.sort(_topics, String::compareToIgnoreCase); + topics = new ArrayList<>(topics); + Collections.sort(topics, String::compareToIgnoreCase); + + assertEquals(_topics, topics); + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + } + + @Entity(name = "Category") + @Table(name = "category") + public static class Category { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + public Long getId() { + return id; + } + + public Category setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Category setName(String name) { + this.name = name; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/spatial/SpatialTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/spatial/SpatialTest.java new file mode 100644 index 000000000..7f6dc3ab7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/spatial/SpatialTest.java @@ -0,0 +1,105 @@ +package com.vladmihalcea.hpjp.hibernate.query.spatial; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.junit.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SpatialTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Address.class, + }; + } + + @Override + protected void beforeInit() { + executeStatement("CREATE EXTENSION IF NOT EXISTS \"postgis\""); + } + + @Override + public void afterDestroy() { + executeStatement("DROP EXTENSION \"postgis\" CASCADE"); + } + + @Test + public void test() { + Long addressId = doInJPA(entityManager -> { + try { + Address address = new Address(); + address.setId(1L); + address.setStreet("5th Avenue"); + address.setNumber("1 A"); + address.setLocation((Point) new WKTReader().read("POINT(60 12)")); + + entityManager.persist(address); + return address.getId(); + } catch (ParseException e) { + throw new RuntimeException(e); + } + }); + + doInJPA(entityManager -> { + Address address = entityManager.find(Address.class, addressId); + Coordinate coordinate = address.getLocation().getCoordinate(); + assertEquals(60.0d, coordinate.getOrdinate(Coordinate.X), 0.1); + assertEquals(12.0d, coordinate.getOrdinate(Coordinate.Y), 0.1); + }); + } + + @Entity(name = "Address") + public static class Address { + + @Id + private Long id; + + private String street; + + private String number; + + private Point location; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } + + public Point getLocation() { + return location; + } + + public void setLocation(Point location) { + this.location = location; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/subquery/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/subquery/Post.java new file mode 100644 index 000000000..7d8ab2193 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/subquery/Post.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.hibernate.query.subquery; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/subquery/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/subquery/PostComment.java new file mode 100644 index 000000000..4ecf46ad8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/subquery/PostComment.java @@ -0,0 +1,72 @@ +package com.vladmihalcea.hpjp.hibernate.query.subquery; + +import jakarta.persistence.*; +import java.util.Date; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne + @JoinColumn(name = "post_id") + private Post post; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn = new Date(); + + private String review; + + private int score; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public int getScore() { + return score; + } + + public PostComment setScore(int score) { + this.score = score; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/subquery/SubqueryExistsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/subquery/SubqueryExistsTest.java new file mode 100644 index 000000000..db8d2e750 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/subquery/SubqueryExistsTest.java @@ -0,0 +1,260 @@ +package com.vladmihalcea.hpjp.hibernate.query.subquery; + +import com.blazebit.persistence.Criteria; +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.spi.CriteriaBuilderConfiguration; +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.criteria.*; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +/** + * @author Vlad Mihalcea + */ +public class SubqueryExistsTest extends AbstractTest { + + private CriteriaBuilderFactory cbf; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Override + protected EntityManagerFactory newEntityManagerFactory() { + EntityManagerFactory entityManagerFactory = super.newEntityManagerFactory(); + CriteriaBuilderConfiguration config = Criteria.getDefault(); + cbf = config.createCriteriaBuilderFactory(entityManagerFactory); + return entityManagerFactory; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + ThreadLocalRandom random = ThreadLocalRandom.current(); + + String[] reviews = new String[] { + "Amazing", + "Great", + "Excellent", + "Highly recommended", + "Simply the best" + }; + + long postId = 0; + + Post post1 = new Post() + .setId(++postId) + .setTitle("High-Performance Java Persistence"); + + Post post2 = new Post() + .setId(++postId) + .setTitle("High-Performance SQL"); + + entityManager.persist(post1); + entityManager.persist(post2); + + long postCommentId = 0; + + entityManager.persist( + new PostComment() + .setId(++postCommentId) + .setPost(post1) + .setReview(reviews[random.nextInt(reviews.length)]) + .setScore(1) + ); + + entityManager.persist( + new PostComment() + .setId(++postCommentId) + .setPost(post1) + .setReview(reviews[random.nextInt(reviews.length)]) + .setScore(2) + ); + + entityManager.persist( + new PostComment() + .setId(++postCommentId) + .setPost(post1) + .setReview(reviews[random.nextInt(reviews.length)]) + .setScore(3) + ); + + entityManager.persist( + new PostComment() + .setId(++postCommentId) + .setPost(post1) + .setReview(reviews[random.nextInt(reviews.length)]) + .setScore(4) + ); + + entityManager.persist( + new PostComment() + .setId(++postCommentId) + .setPost(post1) + .setReview("Highly recommended") + .setScore(5) + ); + + entityManager.persist( + new PostComment() + .setId(++postCommentId) + .setPost(post1) + .setReview("Highly recommended") + .setScore(6) + ); + + entityManager.persist( + new PostComment() + .setId(++postCommentId) + .setPost(post2) + .setReview(reviews[random.nextInt(reviews.length)]) + .setScore(7) + ); + + entityManager.persist( + new PostComment() + .setId(++postCommentId) + .setPost(post2) + .setReview("Simply the best") + .setScore(8) + ); + + entityManager.persist( + new PostComment() + .setId(++postCommentId) + .setPost(post2) + .setReview("Simply the best") + .setScore(9) + ); + + entityManager.persist( + new PostComment() + .setId(++postCommentId) + .setPost(post2) + .setReview("Highly recommended") + .setScore(10) + ); + + entityManager.persist( + new PostComment() + .setId(++postCommentId) + .setPost(post2) + .setReview("Highly recommended") + .setScore(11) + ); + }); + } + + @Test + public void testJoin() { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from PostComment pc + join pc.post p + where pc.score > :minScore + order by p.id + """, Post.class) + .setParameter("minScore", 10) + .getResultList(); + + assertSame(1, posts.size()); + + Post post = posts.get(0); + assertEquals(2L, post.getId().longValue()); + }); + } + + @Test + public void testExistsJPQL() { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + where exists ( + select 1 + from PostComment pc + where + pc.post = p and + pc.score > :minScore + ) + order by p.id + """, Post.class) + .setParameter("minScore", 10) + .getResultList(); + + assertSame(1, posts.size()); + + Post post = posts.get(0); + assertEquals(2L, post.getId().longValue()); + }); + } + + @Test + public void testExistsCriteriaAPI() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = builder.createQuery(Post.class); + Root p = query.from(Post.class); + + ParameterExpression minScore = builder.parameter(Integer.class); + Subquery subQuery = query.subquery(Integer.class); + Root pc = subQuery.from(PostComment.class); + subQuery + .select(builder.literal(1)) + .where( + builder.equal(pc.get(PostComment_.POST), p), + builder.gt(pc.get(PostComment_.SCORE), minScore) + ); + + query.where(builder.exists(subQuery)); + + List posts = entityManager.createQuery(query) + .setParameter(minScore, 10) + .getResultList(); + + assertSame(1, posts.size()); + + Post post = posts.get(0); + assertEquals(2L, post.getId().longValue()); + }); + } + + @Test + public void testExistsBlazePersistence() { + doInJPA(entityManager -> { + + final String POST_ALIAS = "p"; + final String POST_COMMENT_ALIAS = "pc"; + + List posts = cbf.create(entityManager, Post.class) + .from(Post.class, POST_ALIAS) + .whereExists() + .from(PostComment.class, POST_COMMENT_ALIAS) + .select("1") + .where(PostComment_.POST).eqExpression(POST_ALIAS) + .where(PostComment_.SCORE).gtExpression(":minScore") + .end() + .select(POST_ALIAS) + .setParameter("minScore", 10) + .getResultList(); + + assertSame(1, posts.size()); + + Post post = posts.get(0); + assertEquals(2L, post.getId().longValue()); + }); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/time/ExtractTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/time/ExtractTest.java new file mode 100644 index 000000000..cfe104828 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/time/ExtractTest.java @@ -0,0 +1,106 @@ +package com.vladmihalcea.hpjp.hibernate.query.time; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.time.Year; +import java.time.temporal.TemporalAdjusters; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ExtractTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post part1 = new Post(); + part1.setTitle("High-Performance Java Persistence, Part 1"); + part1.setCreatedOn( + LocalDateTime.now().with(TemporalAdjusters.previous(DayOfWeek.MONDAY)) + ); + entityManager.persist(part1); + + Post part2 = new Post(); + part2.setTitle("High-Performance Java Persistence, Part 2"); + part2.setCreatedOn( + LocalDateTime.now().with(TemporalAdjusters.previous(DayOfWeek.TUESDAY)) + ); + entityManager.persist(part2); + + Post part3 = new Post(); + part3.setTitle("High-Performance Java Persistence, Part 3"); + part3.setCreatedOn( + LocalDateTime.now().with(TemporalAdjusters.previous(DayOfWeek.THURSDAY)) + ); + entityManager.persist(part3); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + where EXTRACT(YEAR FROM createdOn) = :year + """, Post.class) + .setParameter("year", Year.now().getValue()) + .getResultList(); + + assertEquals(3, posts.size()); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/QueryTimeoutTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/QueryTimeoutTest.java new file mode 100644 index 000000000..0ba081cba --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/QueryTimeoutTest.java @@ -0,0 +1,161 @@ +package com.vladmihalcea.hpjp.hibernate.query.timeout; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.query.NativeQuery; +import org.junit.Test; +import org.postgresql.util.PSQLException; + +import jakarta.persistence.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class QueryTimeoutTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testJPQLTimeoutHint() { + + doInJPA(entityManager -> { + for (int i = 0; i < 5; i++) { + entityManager.persist( + new Post().setTitle(String.format("Hibernate User Guide, Chapter %d", i + 1)) + ); + } + + for (int i = 0; i < 5; i++) { + entityManager.persist( + new Post().setTitle(String.format("%d Hibernate Tips", (i + 1) * 5)) + ); + } + + for (int i = 0; i < 5; i++) { + entityManager.persist( + new Post().setTitle(String.format("%d Tips to master Hibernate", (i + 1) * 10)) + ); + } + }); + + doInJPA(entityManager -> { + List posts = entityManager + .createQuery( + "select p " + + "from Post p " + + "where lower(p.title) like lower(:titlePattern)", Post.class) + .setParameter("titlePattern", "%Hibernate%") + .setHint("jakarta.persistence.query.timeout", 50) + .getResultList(); + + assertEquals(15, posts.size()); + }); + + doInJPA(entityManager -> { + List posts = entityManager + .createQuery( + "select p " + + "from Post p " + + "where lower(p.title) like lower(:titlePattern)", Post.class) + .setParameter("titlePattern", "%Hibernate%") + .setHint("org.hibernate.timeout", 1) + .getResultList(); + + assertEquals(15, posts.size()); + }); + + doInJPA(entityManager -> { + List posts = entityManager + .createQuery( + "select p " + + "from Post p " + + "where lower(p.title) like lower(:titlePattern)", Post.class) + .setParameter("titlePattern", "%Hibernate%") + .unwrap(org.hibernate.query.Query.class) + .setTimeout(1) + .getResultList(); + + assertEquals(15, posts.size()); + }); + } + + @Test + public void testJPATimeout() { + doInJPA(entityManager -> { + try { + List result = entityManager + .createNativeQuery( + "SELECT 1 " + + "FROM pg_sleep(2) ", Tuple.class) + .setHint("jakarta.persistence.query.timeout", (int) TimeUnit.SECONDS.toMillis(1)) + .getResultList(); + + fail("Timeout failure expected"); + } catch (Exception e) { + PSQLException rootCause = ExceptionUtil.rootCause(e); + assertTrue(rootCause.getMessage().contains("canceling statement due to user request")); } + }); + } + + @Test + public void testHibernateTimeout() { + doInJPA(entityManager -> { + try { + List result = entityManager + .createNativeQuery( + "SELECT 1 " + + "FROM pg_sleep(2) ", Tuple.class) + .unwrap(NativeQuery.class) + .setTimeout(1) + .getResultList(); + + fail("Timeout failure expected"); + } catch (Exception e) { + PSQLException rootCause = ExceptionUtil.rootCause(e); + assertTrue(rootCause.getMessage().contains("canceling statement due to user request")); + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Integer id; + + private String title; + + public Integer getId() { + return id; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SQLServerJDBCQueryTimeoutTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SQLServerJDBCQueryTimeoutTest.java new file mode 100644 index 000000000..9dd572602 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SQLServerJDBCQueryTimeoutTest.java @@ -0,0 +1,71 @@ +package com.vladmihalcea.hpjp.hibernate.query.timeout; + +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import org.hibernate.Session; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.concurrent.Executors; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerJDBCQueryTimeoutTest extends AbstractSQLServerIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testQueryTimeout() { + try { + doInJPA(entityManager -> { + LOGGER.info("Start waiting"); + //Works for any query + entityManager.unwrap(Session.class).doWork(connection -> { + connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), 1000); + executeStatement(connection, "WAITFOR DELAY '00:00:02'"); + fail("Should have thrown a query timeout!"); + }); + LOGGER.info("Done waiting"); + }); + } catch (Exception e) { + LOGGER.info("Timeout triggered", e); + assertTrue(ExceptionUtil.rootCause(e).getMessage().contains("Read timed out")); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Integer id; + + private String title; + + public Integer getId() { + return id; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SQLServerJPAQueryTimeoutTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SQLServerJPAQueryTimeoutTest.java new file mode 100644 index 000000000..ead272f98 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SQLServerJPAQueryTimeoutTest.java @@ -0,0 +1,73 @@ +package com.vladmihalcea.hpjp.hibernate.query.timeout; + +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import org.hibernate.jpa.AvailableHints; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Properties; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerJPAQueryTimeoutTest extends AbstractSQLServerIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testQueryTimeout() { + doInJPA(entityManager -> { + LOGGER.info("Start waiting"); + //Works only for queries executed via JPA and Hibernate + try { + entityManager + .createNativeQuery("WAITFOR DELAY '00:00:02'") + .setHint(AvailableHints.HINT_TIMEOUT, String.valueOf(1)) + .executeUpdate(); + + fail("Should have thrown a query timeout!"); + } catch (Exception e) { + LOGGER.info("Timeout triggered", e); + assertTrue(ExceptionUtil.rootCause(e).getMessage().contains("The query has timed out")); + } + + LOGGER.info("Done waiting"); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Integer id; + + private String title; + + public Integer getId() { + return id; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SQLServerLockTimeoutTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SQLServerLockTimeoutTest.java new file mode 100644 index 000000000..8b6bcea1c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SQLServerLockTimeoutTest.java @@ -0,0 +1,86 @@ +package com.vladmihalcea.hpjp.hibernate.query.timeout; + +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerLockTimeoutTest extends AbstractSQLServerIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testLockTimeout() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + executeStatement(entityManager, "SET LOCK_TIMEOUT 1000"); + + List posts = entityManager + .createQuery("SELECT p from Post p") + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .getResultList(); + + doInJPA(_entityManager -> { + LOGGER.info("Start waiting"); + executeStatement(_entityManager, "SET LOCK_TIMEOUT 1000"); + + try { + List posts_ = _entityManager + .createQuery("SELECT p from Post p") + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .getResultList(); + + fail("Should have thrown a lock acquisition timeout!"); + } catch (Exception e) { + LOGGER.info("Timeout triggered", e); + assertTrue(ExceptionUtil.isLockTimeout(e)); + } + + LOGGER.info("Done waiting"); + }); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Integer id; + + private String title; + + public Integer getId() { + return id; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SQLServerServerSideTimeoutTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SQLServerServerSideTimeoutTest.java new file mode 100644 index 000000000..4233a3d45 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SQLServerServerSideTimeoutTest.java @@ -0,0 +1,115 @@ +package com.vladmihalcea.hpjp.hibernate.query.timeout; + +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import org.hibernate.jpa.AvailableHints; +import org.hibernate.jpa.QueryHints; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.List; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerServerSideTimeoutTest extends AbstractSQLServerIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty( + AvailableHints.HINT_TIMEOUT, String.valueOf(1) + ); + } + + @Test + public void testQueryTimeout() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + try { + executeStatement("EXEC sp_configure 'remote query timeout', 1"); + executeStatement("RECONFIGURE"); + + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle("High-Performance Java Persistence"); + + entityManager.persist(post); + return post.getId(); + }); + + doInJPA(entityManager -> { + List posts = entityManager + .createQuery("SELECT p from Post p") + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .getResultList(); + + //executeStatement(entityManager, "WAITFOR DELAY '00:00:02'"); + doInJPA(_entityManager -> { + LOGGER.info("Start waiting"); + + List posts_ = _entityManager + .createQuery("SELECT p from Post p") + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .getResultList(); + + LOGGER.info("Done waiting"); + }); + }); + } catch (Exception expected) { + LOGGER.info("Query timed out", expected); + } finally { + executeStatement("EXEC sp_configure 'remote query timeout', 0"); + executeStatement("RECONFIGURE"); + } + } + + @Test + public void testTimeout() { + try { + executeStatement("EXEC sp_configure 'remote query timeout', 1"); + executeStatement("RECONFIGURE"); + + doInJPA(entityManager -> { + LOGGER.info("Start waiting"); + executeStatement(entityManager, "WAITFOR DELAY '00:00:02'"); + + LOGGER.info("Done waiting"); + }); + } finally { + executeStatement("EXEC sp_configure 'remote query timeout', 0"); + executeStatement("RECONFIGURE"); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Integer id; + + private String title; + + public Integer getId() { + return id; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SessionQueryTimeoutTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SessionQueryTimeoutTest.java new file mode 100644 index 000000000..06ca5d42e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/timeout/SessionQueryTimeoutTest.java @@ -0,0 +1,75 @@ +package com.vladmihalcea.hpjp.hibernate.query.timeout; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.jpa.AvailableHints; +import org.junit.Test; +import org.postgresql.util.PSQLException; + +import java.util.List; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class SessionQueryTimeoutTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testJPATimeout() { + doInJPA(entityManager -> { + entityManager.persist(new Post().setTitle("High-Performance Java Persistence")); + try { + List posts = entityManager.createQuery(""" + select p.id, pg_sleep(2) + from Post p + """, Tuple.class) + .setHint(AvailableHints.HINT_TIMEOUT, String.valueOf(1)) + .getResultList(); + + fail("Timeout failure expected"); + } catch (Exception e) { + PSQLException rootCause = ExceptionUtil.rootCause(e); + assertTrue(rootCause.getMessage().contains("canceling statement due to user request")); } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Integer id; + + private String title; + + public Integer getId() { + return id; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/upsert/HibernateUpsertMergeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/upsert/HibernateUpsertMergeTest.java new file mode 100644 index 000000000..3708d1b08 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/upsert/HibernateUpsertMergeTest.java @@ -0,0 +1,159 @@ +package com.vladmihalcea.hpjp.hibernate.query.upsert; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class HibernateUpsertMergeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Book.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void test() { + doInStatelessSession(session -> { + session.upsert( + new Book() + .setId(1L) + .setTitle("High-Performance Hibernate") + .setIsbn("978-9730228236") + ); + + session.upsert( + new Book() + .setId(1L) + .setTitle("High-Performance Hibernate 2nd edition") + .setIsbn("978-9730228236") + ); + }); + } + + @Test + public void testBatching() { + doInStatelessSession(session -> { + session.setJdbcBatchSize(50); + + session.upsert( + new Book() + .setId(1L) + .setTitle("High-Performance Hibernate") + .setIsbn("978-9730228236") + ); + + session.upsert( + new Book() + .setId(1L) + .setTitle("High-Performance Hibernate 2nd edition") + .setIsbn("978-9730228236") + ); + }); + } + + @Test + public void testTimeoutOnSecondTransaction() { + CountDownLatch aliceLatch = new CountDownLatch(1); + + doInStatelessSession(session -> { + session.upsert( + new Book() + .setId(1L) + .setTitle("High-Performance Hibernate") + .setIsbn("978-9730228236") + ); + + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean bobUpdateSucceeded = new AtomicBoolean(); + + executeAsync(() -> { + try { + doInStatelessSession(_session -> { + _session.doWork(this::setJdbcTimeout); + + _session.upsert( + new Book() + .setId(1L) + .setTitle("High-Performance Hibernate") + .setIsbn("978-9730228236") + ); + + bobUpdateSucceeded.set(true); + }); + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } + } + + aliceLatch.countDown(); + }); + + awaitOnLatch(aliceLatch); + + assertTrue(preventedByLocking.get()); + assertFalse(bobUpdateSucceeded.get()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + private Long id; + + private String title; + + @NaturalId + private String isbn; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/upsert/HibernateUpsertOnConflictTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/upsert/HibernateUpsertOnConflictTest.java new file mode 100644 index 000000000..e82db6a34 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/upsert/HibernateUpsertOnConflictTest.java @@ -0,0 +1,117 @@ +package com.vladmihalcea.hpjp.hibernate.query.upsert; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class HibernateUpsertOnConflictTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Book.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.createQuery(""" + insert into Book (id, title, isbn) + values ( + :id, + :title, + :isbn + ) + on conflict(id) do + update + set + title = excluded.title, + isbn = excluded.isbn + """) + .setParameter("id", 1L) + .setParameter("title", "High-Performance Java Persistence") + .setParameter("isbn", "978-9730228236") + .executeUpdate(); + }); + + doInJPA(entityManager -> { + entityManager.createQuery(""" + insert into Book (id, title, isbn) + values ( + :id, + :title, + :isbn + ) + on conflict(id) do + update + set + title = excluded.title, + isbn = excluded.isbn + """) + .setParameter("id", 1L) + .setParameter("title", "High-Performance Java Persistence, 2nd edition") + .setParameter("isbn", "978-9730228237") + .executeUpdate(); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + private Long id; + + private String title; + + @NaturalId + private String isbn; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/upsert/MySQLUpsertUniqueColumnTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/upsert/MySQLUpsertUniqueColumnTest.java new file mode 100644 index 000000000..fc096d72a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/upsert/MySQLUpsertUniqueColumnTest.java @@ -0,0 +1,104 @@ +package com.vladmihalcea.hpjp.hibernate.query.upsert; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class MySQLUpsertUniqueColumnTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Book.class, + }; + } + + private final CountDownLatch aliceLatch = new CountDownLatch(1); + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setTitle("High-Performance Java Persistence"); + book.setIsbn("978-9730228236"); + entityManager.persist(book); + + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + + executeAsync(() -> { + try { + doInJPA(_entityManager -> { + _entityManager.unwrap(Session.class).doWork(this::setJdbcTimeout); + _entityManager.createNativeQuery( + "INSERT IGNORE " + + "INTO book (title, isbn) " + + "VALUES (:title, :isbn)") + .setParameter("title", "High-Performance Hibernate") + .setParameter("isbn", "978-9730228236") + .executeUpdate(); + }); + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } + } + + aliceLatch.countDown(); + }); + + awaitOnLatch(aliceLatch); + + assertTrue(preventedByLocking.get()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + @NaturalId + private String isbn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/upsert/PostgreSQLUpsertConcurrencyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/upsert/PostgreSQLUpsertConcurrencyTest.java new file mode 100644 index 000000000..903884553 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/upsert/PostgreSQLUpsertConcurrencyTest.java @@ -0,0 +1,191 @@ +package com.vladmihalcea.hpjp.hibernate.query.upsert; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLUpsertConcurrencyTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Book.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + private final CountDownLatch aliceLatch = new CountDownLatch(1); + + @Test + public void testTimeoutOnSecondTransaction() { + doInJPA(entityManager -> { + entityManager.createNativeQuery( + "INSERT INTO book (" + + " id, " + + " title, " + + " isbn" + + ") " + + "VALUES (" + + " :id, " + + " :title, " + + " :isbn" + + ") " + + "ON CONFLICT (id) DO NOTHING") + .setParameter("id", 1L) + .setParameter("title", "High-Performance Hibernate") + .setParameter("isbn", "978-9730228236") + .executeUpdate(); + + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicInteger bobUpdateCount = new AtomicInteger(); + + executeAsync(() -> { + try { + doInJPA(_entityManager -> { + _entityManager.unwrap(Session.class).doWork(this::setJdbcTimeout); + + int updateCount = _entityManager.createNativeQuery( + "INSERT INTO book (" + + " id, " + + " title, " + + " isbn" + + ") " + + "VALUES (" + + " :id, " + + " :title, " + + " :isbn" + + ") " + + "ON CONFLICT (id) DO NOTHING") + .setParameter("id", 1L) + .setParameter("title", "High-Performance Hibernate") + .setParameter("isbn", "978-9730228236") + .executeUpdate(); + + bobUpdateCount.set(updateCount); + }); + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } + } + + aliceLatch.countDown(); + }); + + awaitOnLatch(aliceLatch); + + assertTrue(preventedByLocking.get()); + assertEquals(0, bobUpdateCount.get()); + }); + } + + @Test + public void testResumeOnSecondTransaction() { + doInJPA(entityManager -> { + entityManager.createNativeQuery( + "INSERT INTO book (" + + " id, " + + " title, " + + " isbn" + + ") " + + "VALUES (" + + " :id, " + + " :title, " + + " :isbn" + + ") " + + "ON CONFLICT (id) DO NOTHING") + .setParameter("id", 1L) + .setParameter("title", "High-Performance Hibernate") + .setParameter("isbn", "978-9730228236") + .executeUpdate(); + + executeAsync(() -> { + doInJPA(_entityManager -> { + LOGGER.info("Bob tries to insert the same record"); + int updateCount = _entityManager.createNativeQuery( + "INSERT INTO book (" + + " id, " + + " title, " + + " isbn" + + ") " + + "VALUES (" + + " :id, " + + " :title, " + + " :isbn" + + ") " + + "ON CONFLICT (id) DO NOTHING") + .setParameter("id", 1L) + .setParameter("title", "High-Performance Hibernate") + .setParameter("isbn", "978-9730228236") + .executeUpdate(); + + LOGGER.info("Bob managed to execute the UPSERT statement, and the update count is {}", updateCount); + + aliceLatch.countDown(); + }); + }); + + LOGGER.info("Alice starts waiting for 3 seconds!"); + sleep(TimeUnit.SECONDS.toMillis(3)); + }); + LOGGER.info("Alice's transaction has committed!"); + + awaitOnLatch(aliceLatch); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + private Long id; + + private String title; + + @NaturalId + private String isbn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/window/WindowFunctionRunningTotalTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/window/WindowFunctionRunningTotalTest.java new file mode 100644 index 000000000..a8d53307c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/query/window/WindowFunctionRunningTotalTest.java @@ -0,0 +1,429 @@ +package com.vladmihalcea.hpjp.hibernate.query.window; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.query.Query; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class WindowFunctionRunningTotalTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Account.class, + AccountTransaction.class, + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + /** + * account + * ------- + * + * | id | iban | owner | + * |----|-----------------|-------------| + * | 1 | 123-456-789-010 | Alice Smith | + * | 2 | 123-456-789-101 | Bob Johnson | + * + * account_transaction + * ------------------- + * + * | id | amount | created_on | account_id | + * |----|--------|---------------------|------------| + * | 1 | 2560 | 2019-10-13 12:23:00 | 1 | + * | 2 | -200 | 2019-10-14 13:23:00 | 1 | + * | 3 | 500 | 2019-10-14 15:45:00 | 1 | + * | 4 | -1850 | 2019-10-15 10:15:00 | 1 | + * | 5 | 2560 | 2019-10-13 15:23:00 | 2 | + * | 6 | 300 | 2019-10-14 11:23:00 | 2 | + * | 7 | -500 | 2019-10-14 14:45:00 | 2 | + * | 8 | -150 | 2019-10-15 10:15:00 | 2 | + */ + @Override + public void afterInit() { + doInJPA(entityManager -> { + Account account1 = new Account() + .setId(1L) + .setOwner("Alice Smith") + .setIban("123-456-789-010"); + + Account account2 = new Account() + .setId(2L) + .setOwner("Bob Johnson") + .setIban("123-456-789-101"); + + entityManager.persist(account1); + entityManager.persist(account2); + + entityManager.persist( + new AccountTransaction() + .setId(1L) + .setAmount(2560L) + .setAccount(account1) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2019, 10, 13, 12, 23, 0))) + ); + + entityManager.persist( + new AccountTransaction() + .setId(2L) + .setAmount(-200L) + .setAccount(account1) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2019, 10, 14, 13, 23, 0))) + ); + + entityManager.persist( + new AccountTransaction() + .setId(3L) + .setAmount(500L) + .setAccount(account1) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2019, 10, 14, 15, 45, 0))) + ); + + entityManager.persist( + new AccountTransaction() + .setId(4L) + .setAmount(-1850L) + .setAccount(account1) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2019, 10, 15, 10, 15, 0))) + ); + + entityManager.persist( + new AccountTransaction() + .setId(5L) + .setAmount(2560L) + .setAccount(account2) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2019, 10, 13, 15, 23, 0))) + ); + + entityManager.persist( + new AccountTransaction() + .setId(6L) + .setAmount(300L) + .setAccount(account2) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2019, 10, 14, 11, 23, 0))) + ); + + entityManager.persist( + new AccountTransaction() + .setId(7L) + .setAmount(-500L) + .setAccount(account2) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2019, 10, 14, 14, 45, 0))) + ); + + entityManager.persist( + new AccountTransaction() + .setId(8L) + .setAmount(-150L) + .setAccount(account2) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2019, 10, 15, 10, 15, 0))) + ); + }); + } + + /** + * Get the account transactions and calculate the current balance for each transaction. + * + * SELECT + * ROW_NUMBER() OVER( + * PARTITION BY account_id + * ORDER BY created_on, id + * ) AS nr, + * id, + * account_id, + * created_on, + * amount, + * SUM(amount) OVER( + * PARTITION BY account_id + * ORDER BY created_on, id + * ) AS balance + * FROM account_transaction + * ORDER BY id + * + * | nr | id | account_id | created_on | amount | balance | + * |----|----|------------|----------------------------|--------|---------| + * | 1 | 1 | 1 | 2019-10-13 12:23:00.000000 | 2560 | 2560 | + * | 2 | 2 | 1 | 2019-10-14 13:23:00.000000 | -200 | 2360 | + * | 3 | 3 | 1 | 2019-10-14 15:45:00.000000 | 500 | 2860 | + * | 4 | 4 | 1 | 2019-10-15 10:15:00.000000 | -1850 | 1010 | + * | 1 | 5 | 2 | 2019-10-13 15:23:00.000000 | 2560 | 2560 | + * | 2 | 6 | 2 | 2019-10-14 11:23:00.000000 | 300 | 2860 | + * | 3 | 7 | 2 | 2019-10-14 14:45:00.000000 | -500 | 2360 | + * | 4 | 8 | 2 | 2019-10-15 10:15:00.000000 | -150 | 2210 | + */ + @Test + public void testSQL() { + doInJPA(entityManager -> { + List tuples = entityManager.createNativeQuery(""" + SELECT + ROW_NUMBER() OVER( + PARTITION BY account_id + ORDER BY created_on, id + ) AS nr, + id, + account_id, + created_on, + amount, + SUM(amount) OVER( + PARTITION BY account_id + ORDER BY created_on, id + ) AS balance + FROM account_transaction + ORDER BY id + """, Tuple.class) + .getResultList(); + + Tuple tuple1 = tuples.get(0); + assertEquals(1L, longValue(tuple1.get("nr"))); + assertEquals(1L, longValue(tuple1.get("id"))); + assertEquals(2560L, longValue(tuple1.get("amount"))); + assertEquals(2560L, longValue(tuple1.get("balance"))); + assertEquals(1L, longValue(tuple1.get("account_id"))); + assertEquals(Timestamp.valueOf(LocalDateTime.of(2019, 10, 13, 12, 23, 0)), tuple1.get("created_on")); + + Tuple tuple2 = tuples.get(1); + assertEquals(2L, longValue(tuple2.get("nr"))); + assertEquals(2L, longValue(tuple2.get("id"))); + assertEquals(-200L, longValue(tuple2.get("amount"))); + assertEquals(2360L, longValue(tuple2.get("balance"))); + assertEquals(1L, longValue(tuple2.get("account_id"))); + assertEquals(Timestamp.valueOf(LocalDateTime.of(2019, 10, 14, 13, 23, 0)), tuple2.get("created_on")); + + Tuple tuple3 = tuples.get(2); + assertEquals(3L, longValue(tuple3.get("nr"))); + assertEquals(3L, longValue(tuple3.get("id"))); + assertEquals(500L, longValue(tuple3.get("amount"))); + assertEquals(2860L, longValue(tuple3.get("balance"))); + assertEquals(1L, longValue(tuple3.get("account_id"))); + assertEquals(Timestamp.valueOf(LocalDateTime.of(2019, 10, 14, 15, 45, 0)), tuple3.get("created_on")); + + Tuple tuple4 = tuples.get(3); + assertEquals(4L, longValue(tuple4.get("nr"))); + assertEquals(4L, longValue(tuple4.get("id"))); + assertEquals(-1850L, longValue(tuple4.get("amount"))); + assertEquals(1010L, longValue(tuple4.get("balance"))); + assertEquals(1L, longValue(tuple4.get("account_id"))); + assertEquals(Timestamp.valueOf(LocalDateTime.of(2019, 10, 15, 10, 15, 0)), tuple4.get("created_on")); + + Tuple tuple5 = tuples.get(4); + assertEquals(1L, longValue(tuple5.get("nr"))); + assertEquals(5L, longValue(tuple5.get("id"))); + assertEquals(2560L, longValue(tuple5.get("amount"))); + assertEquals(2560L, longValue(tuple5.get("balance"))); + assertEquals(2L, longValue(tuple5.get("account_id"))); + assertEquals(Timestamp.valueOf(LocalDateTime.of(2019, 10, 13, 15, 23, 0)), tuple5.get("created_on")); + + Tuple tuple6 = tuples.get(5); + assertEquals(2L, longValue(tuple6.get("nr"))); + assertEquals(6L, longValue(tuple6.get("id"))); + assertEquals(300L, longValue(tuple6.get("amount"))); + assertEquals(2860L, longValue(tuple6.get("balance"))); + assertEquals(2L, longValue(tuple6.get("account_id"))); + assertEquals(Timestamp.valueOf(LocalDateTime.of(2019, 10, 14, 11, 23, 0)), tuple6.get("created_on")); + + Tuple tuple7 = tuples.get(6); + assertEquals(3L, longValue(tuple7.get("nr"))); + assertEquals(7L, longValue(tuple7.get("id"))); + assertEquals(-500L, longValue(tuple7.get("amount"))); + assertEquals(2360L, longValue(tuple7.get("balance"))); + assertEquals(2L, longValue(tuple7.get("account_id"))); + assertEquals(Timestamp.valueOf(LocalDateTime.of(2019, 10, 14, 14, 45, 0)), tuple7.get("created_on")); + + Tuple tuple8 = tuples.get(7); + assertEquals(4L, longValue(tuple8.get("nr"))); + assertEquals(8L, longValue(tuple8.get("id"))); + assertEquals(-150L, longValue(tuple8.get("amount"))); + assertEquals(2210L, longValue(tuple8.get("balance"))); + assertEquals(2L, longValue(tuple8.get("account_id"))); + assertEquals(Timestamp.valueOf(LocalDateTime.of(2019, 10, 15, 10, 15, 0)), tuple8.get("created_on")); + }); + } + + @Test + public void testJPQL() { + doInJPA(entityManager -> { + List records = entityManager.createQuery(""" + SELECT + ROW_NUMBER() OVER( + PARTITION BY at.account.id + ORDER BY at.createdOn + ) AS nr, + at, + SUM(at.amount) OVER( + PARTITION BY at.account.id + ORDER BY at.createdOn + ) AS balance + FROM AccountTransaction at + ORDER BY at.id + """, StatementRecord.class) + .unwrap(Query.class) + .setTupleTransformer((Object[] tuple, String[] aliases) -> new StatementRecord( + longValue(tuple[0]), + (AccountTransaction) tuple[1], + longValue(tuple[2]) + )) + .getResultList(); + + assertEquals(8, records.size()); + + StatementRecord record1 = records.get(0); + assertEquals(1L, record1.nr().longValue()); + assertEquals(1L, record1.transaction().getId().longValue()); + assertEquals(1L, record1.transaction().getAccount().getId().longValue()); + assertEquals(2560L, record1.balance().longValue()); + + StatementRecord record2 = records.get(1); + assertEquals(2L, record2.nr().longValue()); + assertEquals(2L, longValue(record2.transaction().getId())); + assertEquals(1L, longValue(record2.transaction().getAccount().getId())); + assertEquals(2360L, longValue(record2.balance())); + + StatementRecord record3 = records.get(2); + assertEquals(3L, record3.nr().longValue()); + assertEquals(3L, longValue(record3.transaction().getId())); + assertEquals(1L, longValue(record3.transaction().getAccount().getId())); + assertEquals(2860L, longValue(record3.balance())); + + StatementRecord record4 = records.get(3); + assertEquals(4L, record4.nr().longValue()); + assertEquals(4L, longValue(record4.transaction().getId())); + assertEquals(1L, longValue(record4.transaction().getAccount().getId())); + assertEquals(1010L, longValue(record4.balance())); + + StatementRecord record5 = records.get(4); + assertEquals(1L, record5.nr().longValue()); + assertEquals(5L, longValue(record5.transaction().getId())); + assertEquals(2L, longValue(record5.transaction().getAccount().getId())); + assertEquals(2560L, longValue(record5.balance())); + }); + + doInJPA(entityManager -> { + List runningTotals = entityManager.createQuery(""" + SELECT + SUM(at.amount) OVER( + PARTITION BY at.account.id + ORDER BY at.createdOn + ) AS balance + FROM AccountTransaction at + ORDER BY at.id + """, Long.class) + .getResultList(); + + assertEquals(8, runningTotals.size()); + }); + } + + public static record StatementRecord( + Long nr, + AccountTransaction transaction, + Long balance + ) { + } + + @Entity(name = "Account") + @Table(name = "account") + public static class Account { + + @Id + private Long id; + + private String owner; + + private String iban; + + public Long getId() { + return id; + } + + public Account setId(Long id) { + this.id = id; + return this; + } + + public String getOwner() { + return owner; + } + + public Account setOwner(String owner) { + this.owner = owner; + return this; + } + + public String getIban() { + return iban; + } + + public Account setIban(String iban) { + this.iban = iban; + return this; + } + } + + @Entity(name = "AccountTransaction") + @Table(name = "account_transaction") + public static class AccountTransaction { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Account account; + + private Long amount; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn; + + public Long getId() { + return id; + } + + public AccountTransaction setId(Long id) { + this.id = id; + return this; + } + + public Account getAccount() { + return account; + } + + public AccountTransaction setAccount(Account account) { + this.account = account; + return this; + } + + public Long getAmount() { + return amount; + } + + public AccountTransaction setAmount(Long amount) { + this.amount = amount; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public AccountTransaction setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/DropPostgreSQLPublicSchemaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/DropPostgreSQLPublicSchemaTest.java new file mode 100644 index 000000000..4e24ba3b5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/DropPostgreSQLPublicSchemaTest.java @@ -0,0 +1,71 @@ +package com.vladmihalcea.hpjp.hibernate.schema.flyway; + +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.spring.config.jpa.PostgreSQLJPAConfiguration; +import org.hibernate.Session; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +/** + * @author Vlad Mihalcea + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = PostgreSQLJPAConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public class DropPostgreSQLPublicSchemaTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private TransactionTemplate transactionTemplate; + + @Autowired + private Database database; + + private boolean drop = true; + + @Test + public void test() { + if (drop) { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Session session = entityManager.unwrap(Session.class); + session.doWork(connection -> { + ScriptUtils.executeSqlScript(connection, + new EncodedResource( + new ClassPathResource( + String.format("flyway/scripts/%1$s/drop/drop.sql", database.name().toLowerCase()) + ) + ), + true, true, + ScriptUtils.DEFAULT_COMMENT_PREFIX, + ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER, + ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER, + ScriptUtils.DEFAULT_COMMENT_PREFIX); + }); + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/FlywayEntities.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/FlywayEntities.java similarity index 86% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/FlywayEntities.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/FlywayEntities.java index 7a4e3d246..eeafb412f 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/schema/flyway/FlywayEntities.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/FlywayEntities.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.hibernate.schema.flyway; +package com.vladmihalcea.hpjp.hibernate.schema.flyway; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -15,7 +15,8 @@ public class FlywayEntities { public static class Post { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_seq") + @SequenceGenerator(name = "post_seq", allocationSize = 1) private Long id; private String title; @@ -94,7 +95,6 @@ public void removeDetails() { public static class PostDetails { @Id - @GeneratedValue private Long id; @Column(name = "created_on") @@ -108,8 +108,8 @@ public PostDetails() { } @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") @MapsId + @JoinColumn(name = "id") private Post post; public Long getId() { @@ -150,7 +150,8 @@ public void setCreatedBy(String createdBy) { public static class PostComment { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_comment_seq") + @SequenceGenerator(name = "post_comment_seq", allocationSize = 1) private Long id; @ManyToOne @@ -194,7 +195,8 @@ public void setReview(String review) { public static class Tag { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tag_seq") + @SequenceGenerator(name = "tag_seq", allocationSize = 1) private Long id; private String name; @@ -213,7 +215,8 @@ public void setName(String name) { public static class User { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq") + @SequenceGenerator(name = "user_seq", allocationSize = 1) private Long id; private String name; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/FlywayTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/FlywayTest.java new file mode 100644 index 000000000..ba7fb734a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/FlywayTest.java @@ -0,0 +1,92 @@ +package com.vladmihalcea.hpjp.hibernate.schema.flyway; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceContext; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.jpa.boot.spi.Bootstrap; +import org.hibernate.jpa.boot.spi.EntityManagerFactoryBuilder; +import org.hibernate.service.spi.ServiceRegistryImplementor; +import org.hibernate.tool.schema.internal.ExceptionHandlerHaltImpl; +import org.hibernate.tool.schema.spi.*; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.Collections; +import java.util.Map; + +import static com.vladmihalcea.hpjp.hibernate.schema.flyway.FlywayEntities.Post; + +/** + * @author Vlad Mihalcea + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = PostgreSQLFlywayConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public class FlywayTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private TransactionTemplate transactionTemplate; + + @Autowired + private LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean; + + @Test + public void test() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Post post = new Post(); + entityManager.persist(post); + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + @Ignore + public void testValidate() { + Map settings = localContainerEntityManagerFactoryBean.getJpaPropertyMap(); + EntityManagerFactoryBuilder entityManagerFactoryBuilder = Bootstrap.getEntityManagerFactoryBuilder( + localContainerEntityManagerFactoryBean.getPersistenceUnitInfo(), + settings + ); + MetadataImplementor metadataImplementor = entityManagerFactoryBuilder.metadata(); + EntityManagerFactory entityManagerFactory = entityManagerFactoryBuilder.build(); + + SessionFactoryImplementor sessionFactory = entityManagerFactory.unwrap(SessionFactoryImplementor.class); + ServiceRegistryImplementor serviceRegistry = sessionFactory.getServiceRegistry(); + SchemaManagementTool tool = serviceRegistry.getService(SchemaManagementTool.class); + Map options = Collections.emptyMap(); + SchemaValidator schemaValidator = tool.getSchemaValidator(options); + + final ExecutionOptions executionOptions = SchemaManagementToolCoordinator.buildExecutionOptions( + settings, + ExceptionHandlerHaltImpl.INSTANCE + ); + + schemaValidator.doValidation( + metadataImplementor, + executionOptions, + contributed -> contributed.getContributor().equals("orm") + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/HSQLDBFlywayConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/HSQLDBFlywayConfiguration.java new file mode 100644 index 000000000..9751c9604 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/HSQLDBFlywayConfiguration.java @@ -0,0 +1,11 @@ +package com.vladmihalcea.hpjp.hibernate.schema.flyway; + +import com.vladmihalcea.hpjp.util.spring.config.flyway.AbstractHSQLDBFlywayConfiguration; +import org.springframework.context.annotation.Configuration; + +/** + * @author Vlad Mihalcea + */ +@Configuration +public class HSQLDBFlywayConfiguration extends AbstractHSQLDBFlywayConfiguration { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/PostgreSQLFlywayConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/PostgreSQLFlywayConfiguration.java new file mode 100644 index 000000000..8a19b5300 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/schema/flyway/PostgreSQLFlywayConfiguration.java @@ -0,0 +1,11 @@ +package com.vladmihalcea.hpjp.hibernate.schema.flyway; + +import com.vladmihalcea.hpjp.util.spring.config.flyway.AbstractPostgreSQLFlywayConfiguration; +import org.springframework.context.annotation.Configuration; + +/** + * @author Vlad Mihalcea + */ +@Configuration +public class PostgreSQLFlywayConfiguration extends AbstractPostgreSQLFlywayConfiguration { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/ActivityHistorySQLServerStoredProcedureTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/ActivityHistorySQLServerStoredProcedureTest.java new file mode 100644 index 000000000..68c1bd460 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/ActivityHistorySQLServerStoredProcedureTest.java @@ -0,0 +1,949 @@ +package com.vladmihalcea.hpjp.hibernate.sp; + +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import jakarta.persistence.EntityManager; +import org.hibernate.Session; +import org.junit.Before; +import org.junit.Test; + +import java.sql.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ActivityHistorySQLServerStoredProcedureTest extends AbstractSQLServerIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{}; + } + + @Before + public void init() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + super.init(); + if (recreateTables) { + executeStatement("DROP table ACT_HI_PROCINST"); + executeStatement("DROP table ACT_HI_ACTINST"); + executeStatement("DROP table ACT_HI_TASKINST"); + executeStatement("DROP table ACT_GE_BYTEARRAY"); + executeStatement("DROP table ACT_HI_VARINST"); + executeStatement("DROP table ACT_HI_DETAIL"); + executeStatement("DROP table ACT_HI_COMMENT"); + executeStatement("DROP table ACT_HI_ATTACHMENT"); + executeStatement("DROP table ACT_HI_IDENTITYLINK"); + + executeStatement(""" + create table ACT_HI_PROCINST ( + ID_ nvarchar(64) not null, + PROC_INST_ID_ nvarchar(64) not null, + BUSINESS_KEY_ nvarchar(255), + PROC_DEF_ID_ nvarchar(64) not null, + START_TIME_ datetime not null, + END_TIME_ datetime, + DURATION_ numeric(19,0), + START_USER_ID_ nvarchar(255), + START_ACT_ID_ nvarchar(255), + END_ACT_ID_ nvarchar(255), + SUPER_PROCESS_INSTANCE_ID_ nvarchar(64), + DELETE_REASON_ nvarchar(4000), + TENANT_ID_ nvarchar(255) default '', + NAME_ nvarchar(255), + primary key (ID_), + unique (PROC_INST_ID_) + ) + """); + executeStatement(""" + create table ACT_HI_ACTINST ( + ID_ nvarchar(64) not null, + PROC_DEF_ID_ nvarchar(64) not null, + PROC_INST_ID_ nvarchar(64) not null, + EXECUTION_ID_ nvarchar(64) not null, + ACT_ID_ nvarchar(255) not null, + TASK_ID_ nvarchar(64), + CALL_PROC_INST_ID_ nvarchar(64), + ACT_NAME_ nvarchar(255), + ACT_TYPE_ nvarchar(255) not null, + ASSIGNEE_ nvarchar(255), + START_TIME_ datetime not null, + END_TIME_ datetime, + DURATION_ numeric(19,0), + TENANT_ID_ nvarchar(255) default '', + primary key (ID_) + ) + """); + executeStatement(""" + create table ACT_HI_TASKINST ( + ID_ nvarchar(64) not null, + PROC_DEF_ID_ nvarchar(64), + TASK_DEF_KEY_ nvarchar(255), + PROC_INST_ID_ nvarchar(64), + EXECUTION_ID_ nvarchar(64), + NAME_ nvarchar(255), + PARENT_TASK_ID_ nvarchar(64), + DESCRIPTION_ nvarchar(4000), + OWNER_ nvarchar(255), + ASSIGNEE_ nvarchar(255), + START_TIME_ datetime not null, + CLAIM_TIME_ datetime, + END_TIME_ datetime, + DURATION_ numeric(19,0), + DELETE_REASON_ nvarchar(4000), + PRIORITY_ int, + DUE_DATE_ datetime, + FORM_KEY_ nvarchar(255), + CATEGORY_ nvarchar(255), + TENANT_ID_ nvarchar(255) default '', + primary key (ID_) + ) + """); + executeStatement(""" + create table ACT_HI_VARINST ( + ID_ nvarchar(64) not null, + PROC_INST_ID_ nvarchar(64), + EXECUTION_ID_ nvarchar(64), + TASK_ID_ nvarchar(64), + NAME_ nvarchar(255) not null, + VAR_TYPE_ nvarchar(100), + REV_ int, + BYTEARRAY_ID_ nvarchar(64), + DOUBLE_ double precision, + LONG_ numeric(19,0), + TEXT_ nvarchar(4000), + TEXT2_ nvarchar(4000), + CREATE_TIME_ datetime, + LAST_UPDATED_TIME_ datetime, + primary key (ID_) + ) + """); + executeStatement(""" + create table ACT_HI_DETAIL ( + ID_ nvarchar(64) not null, + TYPE_ nvarchar(255) not null, + PROC_INST_ID_ nvarchar(64), + EXECUTION_ID_ nvarchar(64), + TASK_ID_ nvarchar(64), + ACT_INST_ID_ nvarchar(64), + NAME_ nvarchar(255) not null, + VAR_TYPE_ nvarchar(255), + REV_ int, + TIME_ datetime not null, + BYTEARRAY_ID_ nvarchar(64), + DOUBLE_ double precision, + LONG_ numeric(19,0), + TEXT_ nvarchar(4000), + TEXT2_ nvarchar(4000), + primary key (ID_) + ) + """); + executeStatement(""" + create table ACT_HI_COMMENT ( + ID_ nvarchar(64) not null, + TYPE_ nvarchar(255), + TIME_ datetime not null, + USER_ID_ nvarchar(255), + TASK_ID_ nvarchar(64), + PROC_INST_ID_ nvarchar(64), + ACTION_ nvarchar(255), + MESSAGE_ nvarchar(4000), + FULL_MSG_ varbinary(max), + primary key (ID_) + ) + """); + executeStatement(""" + create table ACT_HI_ATTACHMENT ( + ID_ nvarchar(64) not null, + REV_ integer, + USER_ID_ nvarchar(255), + NAME_ nvarchar(255), + DESCRIPTION_ nvarchar(4000), + TYPE_ nvarchar(255), + TASK_ID_ nvarchar(64), + PROC_INST_ID_ nvarchar(64), + URL_ nvarchar(4000), + CONTENT_ID_ nvarchar(64), + TIME_ datetime, + primary key (ID_) + ) + """); + executeStatement(""" + create table ACT_HI_IDENTITYLINK ( + ID_ nvarchar(64), + GROUP_ID_ nvarchar(255), + TYPE_ nvarchar(255), + USER_ID_ nvarchar(255), + TASK_ID_ nvarchar(64), + PROC_INST_ID_ nvarchar(64), + primary key (ID_) + ) + """); + executeStatement(""" + create table ACT_GE_BYTEARRAY ( + ID_ nvarchar(64), + REV_ int, + NAME_ nvarchar(255), + DEPLOYMENT_ID_ nvarchar(64), + BYTES_ varbinary(max), + GENERATED_ tinyint, + primary key (ID_) + ); + """); + + insertData(); + + executeStatement("create index ACT_IDX_HI_PRO_INST_END on ACT_HI_PROCINST(END_TIME_)"); + executeStatement("create index ACT_IDX_HI_PRO_I_BUSKEY on ACT_HI_PROCINST(BUSINESS_KEY_)"); + executeStatement("create index ACT_IDX_HI_ACT_INST_START on ACT_HI_ACTINST(START_TIME_)"); + executeStatement("create index ACT_IDX_HI_ACT_INST_END on ACT_HI_ACTINST(END_TIME_)"); + executeStatement("create index ACT_IDX_HI_DETAIL_PROC_INST on ACT_HI_DETAIL(PROC_INST_ID_)"); + executeStatement("create index ACT_IDX_HI_DETAIL_ACT_INST on ACT_HI_DETAIL(ACT_INST_ID_)"); + executeStatement("create index ACT_IDX_HI_DETAIL_TIME on ACT_HI_DETAIL(TIME_)"); + executeStatement("create index ACT_IDX_HI_DETAIL_NAME on ACT_HI_DETAIL(NAME_)"); + executeStatement("create index ACT_IDX_HI_DETAIL_TASK_ID on ACT_HI_DETAIL(TASK_ID_)"); + executeStatement("create index ACT_IDX_HI_PROCVAR_PROC_INST on ACT_HI_VARINST(PROC_INST_ID_)"); + executeStatement("create index ACT_IDX_HI_PROCVAR_NAME_TYPE on ACT_HI_VARINST(NAME_, VAR_TYPE_)"); + executeStatement("create index ACT_IDX_HI_PROCVAR_TASK_ID on ACT_HI_VARINST(TASK_ID_)"); + executeStatement("create index ACT_IDX_HI_ACT_INST_PROCINST on ACT_HI_ACTINST(PROC_INST_ID_, ACT_ID_)"); + executeStatement("create index ACT_IDX_HI_ACT_INST_EXEC on ACT_HI_ACTINST(EXECUTION_ID_, ACT_ID_)"); + executeStatement("create index ACT_IDX_HI_IDENT_LNK_USER on ACT_HI_IDENTITYLINK(USER_ID_)"); + executeStatement("create index ACT_IDX_HI_IDENT_LNK_TASK on ACT_HI_IDENTITYLINK(TASK_ID_)"); + executeStatement("create index ACT_IDX_HI_IDENT_LNK_PROCINST on ACT_HI_IDENTITYLINK(PROC_INST_ID_)"); + executeStatement("create index ACT_IDX_HI_TASK_INST_PROCINST on ACT_HI_TASKINST(PROC_INST_ID_)"); + } + + executeStatement("DROP PROCEDURE usp_DeleteActivityHistory"); + + executeStatement(""" + CREATE PROCEDURE usp_DeleteActivityHistory( + @BeforeStartTimestamp DATETIME, + @BatchSize INT, + @DeletedRowCount INT OUTPUT + ) + AS + BEGIN + DROP TABLE IF EXISTS #ROOT_PROC_INST_ID_TABLE; + CREATE TABLE #ROOT_PROC_INST_ID_TABLE (PROC_INST_ID_ NVARCHAR(64)); + + DROP TABLE IF EXISTS #PROC_INST_ID_TABLE; + CREATE TABLE #PROC_INST_ID_TABLE (PROC_INST_ID_ NVARCHAR(64)); + + DROP TABLE IF EXISTS #TASK_INST_ID_TABLE; + CREATE TABLE #TASK_INST_ID_TABLE (ID_ NVARCHAR(64)); + + INSERT INTO #ROOT_PROC_INST_ID_TABLE + SELECT TOP (@BatchSize) PROC_INST_ID_ + FROM ACT_HI_PROCINST + WHERE + END_TIME_ <= @BeforeStartTimestamp + AND END_TIME_ IS NOT NULL + AND SUPER_PROCESS_INSTANCE_ID_ IS NULL; + + SET @DeletedRowCount=0; + DECLARE @DeletedBatchRowCount INT; + + WHILE (SELECT COUNT(*) FROM #ROOT_PROC_INST_ID_TABLE) > 0 + BEGIN + TRUNCATE TABLE #PROC_INST_ID_TABLE; + TRUNCATE TABLE #TASK_INST_ID_TABLE; + + SET @DeletedBatchRowCount=0; + + WITH ACT_HI_PROCINST_HIERARCHY(PROC_INST_ID_) + AS ( + SELECT PROC_INST_ID_ + FROM #ROOT_PROC_INST_ID_TABLE + UNION ALL + SELECT ACT_HI_PROCINST.PROC_INST_ID_ + FROM ACT_HI_PROCINST + INNER JOIN ACT_HI_PROCINST_HIERARCHY ON ACT_HI_PROCINST_HIERARCHY.PROC_INST_ID_ = ACT_HI_PROCINST.SUPER_PROCESS_INSTANCE_ID_ + ) + INSERT INTO #PROC_INST_ID_TABLE + SELECT PROC_INST_ID_ + FROM ACT_HI_PROCINST_HIERARCHY; + + BEGIN TRY + BEGIN TRANSACTION; + + DELETE FROM ACT_GE_BYTEARRAY + WHERE ID_ IN ( + SELECT BYTEARRAY_ID_ FROM ACT_HI_DETAIL + WHERE PROC_INST_ID_ IN (SELECT PROC_INST_ID_ FROM #PROC_INST_ID_TABLE) + ); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_HI_DETAIL + WHERE PROC_INST_ID_ IN (SELECT PROC_INST_ID_ FROM #PROC_INST_ID_TABLE); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_GE_BYTEARRAY + WHERE ID_ IN ( + SELECT BYTEARRAY_ID_ FROM ACT_HI_VARINST + WHERE PROC_INST_ID_ IN (SELECT PROC_INST_ID_ FROM #PROC_INST_ID_TABLE) + ); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_HI_VARINST + WHERE PROC_INST_ID_ IN (SELECT PROC_INST_ID_ FROM #PROC_INST_ID_TABLE); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_HI_ACTINST + WHERE PROC_INST_ID_ IN (SELECT PROC_INST_ID_ FROM #PROC_INST_ID_TABLE); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + -- Delete ACT_HI_TASKINST rows recursive along with their associated: + -- ACT_HI_DETAIL, ACT_HI_VARINST, ACT_HI_COMMENT, ACT_HI_ATTACHMENT, ACT_HI_IDENTITYLINK + BEGIN + WITH ACT_HI_TASKINST_HIERARCHY(ID_) + AS ( + SELECT ID_ + FROM ACT_HI_TASKINST + WHERE PROC_INST_ID_ IN (SELECT PROC_INST_ID_ FROM #PROC_INST_ID_TABLE) + UNION ALL + SELECT ACT_HI_TASKINST.ID_ + FROM ACT_HI_TASKINST + INNER JOIN ACT_HI_TASKINST_HIERARCHY ON ACT_HI_TASKINST_HIERARCHY.ID_ = ACT_HI_TASKINST.PARENT_TASK_ID_ + ) + INSERT INTO #TASK_INST_ID_TABLE + SELECT ID_ + FROM ACT_HI_TASKINST_HIERARCHY; + + DELETE FROM ACT_GE_BYTEARRAY + WHERE ID_ IN ( + SELECT BYTEARRAY_ID_ FROM ACT_HI_DETAIL + WHERE TASK_ID_ IN (SELECT ID_ FROM #TASK_INST_ID_TABLE) + ); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_HI_DETAIL + WHERE TASK_ID_ IN (SELECT ID_ FROM #TASK_INST_ID_TABLE); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_GE_BYTEARRAY + WHERE ID_ IN ( + SELECT BYTEARRAY_ID_ FROM ACT_HI_VARINST + WHERE TASK_ID_ IN (SELECT ID_ FROM #TASK_INST_ID_TABLE) + ); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_HI_VARINST + WHERE TASK_ID_ IN (SELECT ID_ FROM #TASK_INST_ID_TABLE); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_HI_COMMENT + WHERE TASK_ID_ IN (SELECT ID_ FROM #TASK_INST_ID_TABLE); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_GE_BYTEARRAY + WHERE ID_ IN ( + SELECT CONTENT_ID_ FROM ACT_HI_ATTACHMENT + WHERE TASK_ID_ IN (SELECT ID_ FROM #TASK_INST_ID_TABLE) + ); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_HI_ATTACHMENT + WHERE TASK_ID_ IN (SELECT ID_ FROM #TASK_INST_ID_TABLE); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_HI_IDENTITYLINK + WHERE TASK_ID_ IN (SELECT ID_ FROM #TASK_INST_ID_TABLE); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_HI_TASKINST + WHERE ID_ IN (SELECT ID_ FROM #TASK_INST_ID_TABLE); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + END; + + DELETE FROM ACT_HI_IDENTITYLINK + WHERE PROC_INST_ID_ IN (SELECT PROC_INST_ID_ FROM #PROC_INST_ID_TABLE); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_HI_COMMENT + WHERE PROC_INST_ID_ IN (SELECT PROC_INST_ID_ FROM #PROC_INST_ID_TABLE); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + DELETE FROM ACT_HI_PROCINST + WHERE PROC_INST_ID_ IN (SELECT PROC_INST_ID_ FROM #PROC_INST_ID_TABLE); + + SET @DeletedBatchRowCount+=@@ROWCOUNT; + + COMMIT TRANSACTION; + SET @DeletedRowCount+=@DeletedBatchRowCount; + END TRY + BEGIN CATCH + IF (XACT_STATE()) = -1 + -- The current transaction cannot be committed. + BEGIN + PRINT + N'The transaction cannot be committed. Rolling back transaction.' + ROLLBACK TRANSACTION; + END; + ELSE + IF (XACT_STATE()) = 1 + -- The current transaction can be committed. + BEGIN + PRINT + N'Exception was caught, but the transaction can be committed.' + COMMIT TRANSACTION; + END; + END CATCH; + + TRUNCATE TABLE #ROOT_PROC_INST_ID_TABLE; + + INSERT INTO #ROOT_PROC_INST_ID_TABLE + SELECT TOP (@BatchSize) PROC_INST_ID_ + FROM ACT_HI_PROCINST + WHERE + END_TIME_ <= @BeforeStartTimestamp + AND END_TIME_ IS NOT NULL + AND SUPER_PROCESS_INSTANCE_ID_ IS NULL; + END + + DROP TABLE IF EXISTS #ROOT_PROC_INST_ID_TABLE; + DROP TABLE IF EXISTS #PROC_INST_ID_TABLE; + DROP TABLE IF EXISTS #TASK_INST_ID_TABLE; + END + """ + ); + } + + private final boolean recreateTables = true; + + private final int ACT_HI_PROCINST_ROOT_COUNT = 15; + private final int ACT_HI_ACTINST_PER_PROC_COUNT = 5; + private final int ACT_HI_DETAIL_PER_PROC_COUNT = 5; + private final int ACT_HI_TASKINST_PER_PROC_COUNT = 5; + private final int ACT_HI_VARINST_PER_TASK_COUNT = 5; + private final int ACT_HI_DETAIL_PER_TASK_COUNT = 5; + private final int ACT_HI_COMMENT_PER_TASK_COUNT = 5; + private final int ACT_HI_ATTACHMENT_PER_TASK_COUNT = 5; + private final int ACT_HI_IDENTITYLINK_PER_TASK_COUNT = 5; + + private int procInstId = 1; + private int actInstId = 1; + private int taskInstId = 1; + private int varInstId = 1; + private int detailId = 1; + private int commentId = 1; + private int attachmentId = 1; + private int identityLinkId = 1; + private int byteArrayId = 1; + + private void insertData() { + doInJPA(entityManager -> { + int procInstRootCount = 0; + while (procInstRootCount < ACT_HI_PROCINST_ROOT_COUNT) { + //Add a new root process + int rootId = insertProcInst(entityManager, procInstId++, null); + //Add two child process instances + int child1Id = insertProcInst(entityManager, procInstId++, rootId); + int child2Id = insertProcInst(entityManager, procInstId++, rootId); + //Add two grandchild process instances per child + insertProcInst(entityManager, procInstId++, child1Id); + insertProcInst(entityManager, procInstId++, child1Id); + insertProcInst(entityManager, procInstId++, child2Id); + insertProcInst(entityManager, procInstId++, child2Id); + + procInstRootCount++; + } + }); + } + + private int insertProcInst(EntityManager entityManager, int procId, Integer parentProcId) { + entityManager.createNativeQuery(""" + INSERT INTO [ACT_HI_PROCINST] ( + [ID_], + [PROC_INST_ID_], + [PROC_DEF_ID_], + [SUPER_PROCESS_INSTANCE_ID_], + [START_TIME_], + [END_TIME_] + ) + VALUES ( + :id, + :proc_inst_id_, + 'Proc Def', + :super_process_instance_id_, + :start_time_, + :end_time_ + ) + """) + .setParameter("id", String.valueOf(procId)) + .setParameter("proc_inst_id_", String.valueOf(procId)) + .setParameter("super_process_instance_id_", parentProcId != null ? String.valueOf(parentProcId) : null) + .setParameter("start_time_", Timestamp.valueOf(LocalDate.of(2020, 11, 25).atStartOfDay().plusHours(procId))) + .setParameter("end_time_", Timestamp.valueOf(LocalDate.of(2020, 12, 25).atStartOfDay().plusHours(procId))) + .executeUpdate(); + + entityManager.unwrap(Session.class).doWork(connection -> { + insertActivities(connection, procId); + insertDetails(connection, procId); + }); + + + for (int j = 1; j <= ACT_HI_TASKINST_PER_PROC_COUNT; j++) { + //Add a new root task + int rootTaskId = insertTaskInst(entityManager, procId, null); + //Add two child task instances + int child1TaskId = insertTaskInst(entityManager, procId, rootTaskId); + int child2TaskId = insertTaskInst(entityManager, procId, rootTaskId); + //Add two grandchild task instances per child + insertTaskInst(entityManager, procId, child1TaskId); + insertTaskInst(entityManager, procId, child1TaskId); + insertTaskInst(entityManager, procId, child2TaskId); + insertTaskInst(entityManager, procId, child2TaskId); + } + + insertIdentityLinks(entityManager, procId); + insertComments(entityManager, procId); + + return procId; + } + + private void insertActivities(Connection connection, int procId) throws SQLException { + try(PreparedStatement preparedStatement = connection.prepareStatement(""" + INSERT INTO [ACT_HI_ACTINST] ( + [ID_], + [PROC_INST_ID_], + [PROC_DEF_ID_], + [EXECUTION_ID_], + [ACT_ID_], + [ACT_TYPE_], + [START_TIME_]) + VALUES ( + ?, + ?, + 1, + 'Exec Id', + 'Act Type', + 'Act Id', + ? + ) + """ + )) { + for (int j = 1; j <= ACT_HI_ACTINST_PER_PROC_COUNT; j++) { + int actId = actInstId++; + int index = 1; + preparedStatement.setString(index++, String.valueOf(actId)); + preparedStatement.setString(index++, String.valueOf(procId)); + preparedStatement.setTimestamp(index++, Timestamp.valueOf(LocalDate.of(2020, 11, 25).atStartOfDay().plusHours(procId).plusMinutes(j))); + + preparedStatement.addBatch(); + } + preparedStatement.executeBatch(); + } + } + + private int insertTaskInst(EntityManager entityManager, int procId, Integer parentTaskId) { + int taskId = taskInstId++; + + entityManager.createNativeQuery(""" + INSERT INTO [ACT_HI_TASKINST] ( + [ID_], + [PROC_INST_ID_], + [PARENT_TASK_ID_], + [START_TIME_]) + VALUES ( + :id, + :proc_inst_id_, + :parent_task_id_, + :start_time_ + ) + """) + .setParameter("id", String.valueOf(taskId)) + .setParameter("proc_inst_id_", parentTaskId != null ? null : String.valueOf(procId)) + .setParameter("parent_task_id_", parentTaskId != null ? String.valueOf(parentTaskId) : null) + .setParameter("start_time_", Timestamp.valueOf(LocalDate.of(2020, 11, 25).atStartOfDay().plusHours(procId).plusMinutes(taskId))) + .executeUpdate(); + + entityManager.unwrap(Session.class).doWork(connection -> { + insertVarInsts(connection, procId, taskId, parentTaskId != null); + insertDetails(connection, procId, taskId, parentTaskId != null); + insertComments(connection, procId, taskId, parentTaskId != null); + insertAttachments(connection, procId, taskId, parentTaskId != null); + insertIdentityLinks(connection, procId, taskId, parentTaskId != null); + }); + + return taskId; + } + + private void insertVarInsts(Connection connection, int procId, int taskId, boolean subTask) throws SQLException { + try(PreparedStatement varInstPreparedStatement = connection.prepareStatement(""" + INSERT INTO [ACT_HI_VARINST] ( + [ID_], + [PROC_INST_ID_], + [TASK_ID_], + [NAME_], + [BYTEARRAY_ID_], + [CREATE_TIME_]) + VALUES ( + ?, + ?, + ?, + ?, + ?, + ? + ) + """); + PreparedStatement byteArrayPreparedStatement = connection.prepareStatement(""" + INSERT INTO [ACT_GE_BYTEARRAY] ( + [ID_], + [NAME_]) + VALUES ( + ?, + ? + ) + """ + ) + ) { + for (int i = 1; i <= ACT_HI_VARINST_PER_TASK_COUNT; i++) { + int id = varInstId++; + byteArrayId++; + int index = 1; + varInstPreparedStatement.setString(index++, String.valueOf(id)); + varInstPreparedStatement.setString(index++, subTask ? null : String.valueOf(procId)); + varInstPreparedStatement.setString(index++, String.valueOf(taskId)); + varInstPreparedStatement.setString(index++, String.format("Task Var: %d", id)); + varInstPreparedStatement.setString(index++, String.valueOf(byteArrayId)); + varInstPreparedStatement.setTimestamp(index++, Timestamp.valueOf(LocalDate.of(2020, 11, 25).atStartOfDay().plusHours(procId).plusMinutes(taskId).plusSeconds(id))); + + varInstPreparedStatement.addBatch(); + + index = 1; + byteArrayPreparedStatement.setString(index++, String.valueOf(byteArrayId)); + byteArrayPreparedStatement.setString(index++, String.format("Task Var Byte Array: %d", id)); + + byteArrayPreparedStatement.addBatch(); + } + varInstPreparedStatement.executeBatch(); + byteArrayPreparedStatement.executeBatch(); + } + } + + private void insertDetails(Connection connection, int procId) throws SQLException { + try(PreparedStatement detailPreparedStatement = connection.prepareStatement(""" + INSERT INTO [ACT_HI_DETAIL] ( + [ID_], + [TYPE_], + [PROC_INST_ID_], + [NAME_], + [BYTEARRAY_ID_], + [TIME_] + ) + VALUES ( + ?, + 'Type', + ?, + ?, + ?, + ? + ) + """); + PreparedStatement byteArrayPreparedStatement = connection.prepareStatement(""" + INSERT INTO [ACT_GE_BYTEARRAY] ( + [ID_], + [NAME_]) + VALUES ( + ?, + ? + ) + """ + ) + ) { + for (int i = 1; i <= ACT_HI_DETAIL_PER_PROC_COUNT; i++) { + int id = detailId++; + byteArrayId++; + int index = 1; + + detailPreparedStatement.setString(index++, String.valueOf(id)); + detailPreparedStatement.setString(index++, String.valueOf(procId)); + detailPreparedStatement.setString(index++, String.format("Proc Detail: %d", id)); + detailPreparedStatement.setString(index++, String.valueOf(byteArrayId)); + detailPreparedStatement.setTimestamp(index++, Timestamp.valueOf(LocalDate.of(2020, 11, 25).atStartOfDay().plusHours(procId))); + + detailPreparedStatement.addBatch(); + + index = 1; + byteArrayPreparedStatement.setString(index++, String.valueOf(byteArrayId)); + byteArrayPreparedStatement.setString(index++, String.format("Proc Detail Byte Array: %d", id)); + + byteArrayPreparedStatement.addBatch(); + } + detailPreparedStatement.executeBatch(); + byteArrayPreparedStatement.executeBatch(); + } + } + + private void insertDetails(Connection connection, int procId, int taskId, boolean subTask) throws SQLException { + try(PreparedStatement detailPreparedStatement = connection.prepareStatement(""" + INSERT INTO [ACT_HI_DETAIL] ( + [ID_], + [TYPE_], + [PROC_INST_ID_], + [TASK_ID_], + [NAME_], + [BYTEARRAY_ID_], + [TIME_] + ) + VALUES ( + ?, + 'Type', + ?, + ?, + ?, + ?, + ? + ) + """); + PreparedStatement byteArrayPreparedStatement = connection.prepareStatement(""" + INSERT INTO [ACT_GE_BYTEARRAY] ( + [ID_], + [NAME_]) + VALUES ( + ?, + ? + ) + """ + ) + ) { + for (int i = 1; i <= ACT_HI_DETAIL_PER_TASK_COUNT; i++) { + int id = detailId++; + byteArrayId++; + int index = 1; + + detailPreparedStatement.setString(index++, String.valueOf(id)); + detailPreparedStatement.setString(index++, subTask ? null : String.valueOf(procId)); + detailPreparedStatement.setString(index++, String.valueOf(taskId)); + detailPreparedStatement.setString(index++, String.format("Task Detail: %d", id)); + detailPreparedStatement.setString(index++, String.valueOf(byteArrayId)); + detailPreparedStatement.setTimestamp(index++, Timestamp.valueOf(LocalDate.of(2020, 11, 25).atStartOfDay().plusHours(procId).plusMinutes(taskId).plusSeconds(id))); + + detailPreparedStatement.addBatch(); + + index = 1; + byteArrayPreparedStatement.setString(index++, String.valueOf(byteArrayId)); + byteArrayPreparedStatement.setString(index++, String.format("Task Detail Byte Array: %d", id)); + + byteArrayPreparedStatement.addBatch(); + } + detailPreparedStatement.executeBatch(); + byteArrayPreparedStatement.executeBatch(); + } + } + + private void insertComments(Connection connection, int procId, int taskId, boolean subTask) throws SQLException { + try(PreparedStatement preparedStatement = connection.prepareStatement(""" + INSERT INTO [ACT_HI_COMMENT] ( + [ID_], + [PROC_INST_ID_], + [TASK_ID_], + [TIME_]) + VALUES ( + ?, + ?, + ?, + ? + ) + """ + )) { + for (int i = 1; i <= ACT_HI_COMMENT_PER_TASK_COUNT; i++) { + int id = commentId++; + int index = 1; + preparedStatement.setString(index++, String.valueOf(id)); + preparedStatement.setString(index++, subTask ? null : String.valueOf(procId)); + preparedStatement.setString(index++, String.valueOf(taskId)); + preparedStatement.setTimestamp(index++, Timestamp.valueOf(LocalDate.of(2020, 11, 25).atStartOfDay().plusHours(procId).plusMinutes(taskId).plusSeconds(id))); + + preparedStatement.addBatch(); + } + preparedStatement.executeBatch(); + } + } + + private void insertComments(EntityManager entityManager, int procId) { + int id = commentId++; + entityManager.createNativeQuery(""" + INSERT INTO [ACT_HI_COMMENT] ( + [ID_], + [PROC_INST_ID_], + [TIME_] + ) + VALUES ( + :id, + :proc_inst_id_, + :time + ) + """) + .setParameter("id", String.valueOf(id)) + .setParameter("proc_inst_id_", String.valueOf(procId)) + .setParameter("time", Timestamp.valueOf(LocalDate.of(2020, 11, 25).atStartOfDay().plusHours(procId).plusMinutes(id))) + .executeUpdate(); + } + + private void insertAttachments(Connection connection, int procId, int taskId, boolean subTask) throws SQLException { + try(PreparedStatement attachmentPreparedStatement = connection.prepareStatement(""" + INSERT INTO [ACT_HI_ATTACHMENT] ( + [ID_], + [PROC_INST_ID_], + [TASK_ID_], + [CONTENT_ID_], + [TIME_] + ) + VALUES ( + ?, + ?, + ?, + ?, + ? + ) + """); + PreparedStatement byteArrayPreparedStatement = connection.prepareStatement(""" + INSERT INTO [ACT_GE_BYTEARRAY] ( + [ID_], + [NAME_]) + VALUES ( + ?, + ? + ) + """ + ) + ) { + for (int i = 1; i <= ACT_HI_ATTACHMENT_PER_TASK_COUNT; i++) { + int id = attachmentId++; + byteArrayId++; + int index = 1; + attachmentPreparedStatement.setString(index++, String.valueOf(id)); + attachmentPreparedStatement.setString(index++, subTask ? null : String.valueOf(procId)); + attachmentPreparedStatement.setString(index++, String.valueOf(taskId)); + attachmentPreparedStatement.setString(index++, String.valueOf(byteArrayId)); + attachmentPreparedStatement.setTimestamp(index++, Timestamp.valueOf(LocalDate.of(2020, 11, 25).atStartOfDay().plusHours(procId).plusMinutes(taskId).plusSeconds(id))); + + attachmentPreparedStatement.addBatch(); + + index = 1; + byteArrayPreparedStatement.setString(index++, String.valueOf(byteArrayId)); + byteArrayPreparedStatement.setString(index++, String.format("Var: %d", id)); + + byteArrayPreparedStatement.addBatch(); + } + attachmentPreparedStatement.executeBatch(); + byteArrayPreparedStatement.executeBatch(); + } + } + + private void insertIdentityLinks(Connection connection, int procId, int taskId, boolean subTask) throws SQLException { + try(PreparedStatement preparedStatement = connection.prepareStatement(""" + INSERT INTO [ACT_HI_IDENTITYLINK] ( + [ID_], + [PROC_INST_ID_], + [TASK_ID_] + ) + VALUES ( + ?, + ?, + ? + ) + """ + )) { + for (int i = 1; i <= ACT_HI_IDENTITYLINK_PER_TASK_COUNT; i++) { + int id = identityLinkId++; + int index = 1; + preparedStatement.setString(index++, String.valueOf(id)); + preparedStatement.setString(index++, subTask ? null : String.valueOf(procId)); + preparedStatement.setString(index++, String.valueOf(taskId)); + + preparedStatement.addBatch(); + } + preparedStatement.executeBatch(); + } + } + + private void insertIdentityLinks(EntityManager entityManager, int procId) { + int id = identityLinkId++; + + entityManager.createNativeQuery(""" + INSERT INTO [ACT_HI_IDENTITYLINK] ( + [ID_], + [PROC_INST_ID_] + ) + VALUES ( + :id, + :proc_inst_id_ + ) + """) + .setParameter("id", String.valueOf(id)) + .setParameter("proc_inst_id_", String.valueOf(procId)) + .executeUpdate(); + } + + @Test + public void testDeleteActivityHistory() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + try(Connection connection = dataSourceProvider().dataSource().getConnection()) { + deleteActivityHistoryBeforeDate( + connection, + Timestamp.valueOf(LocalDateTime.now()), + 10 + ); + } catch (SQLException e) { + LOGGER.error("Error getting database connection", e); + } + + doInJPA(entityManager -> { + assertEquals(0, ((Number) entityManager.createNativeQuery("SELECT COUNT(*) FROM ACT_HI_PROCINST").getSingleResult()).intValue()); + assertEquals(0, ((Number) entityManager.createNativeQuery("SELECT COUNT(*) FROM ACT_HI_ACTINST").getSingleResult()).intValue()); + assertEquals(0, ((Number) entityManager.createNativeQuery("SELECT COUNT(*) FROM ACT_HI_TASKINST").getSingleResult()).intValue()); + assertEquals(0, ((Number) entityManager.createNativeQuery("SELECT COUNT(*) FROM ACT_GE_BYTEARRAY").getSingleResult()).intValue()); + assertEquals(0, ((Number) entityManager.createNativeQuery("SELECT COUNT(*) FROM ACT_HI_VARINST").getSingleResult()).intValue()); + assertEquals(0, ((Number) entityManager.createNativeQuery("SELECT COUNT(*) FROM ACT_HI_DETAIL").getSingleResult()).intValue()); + assertEquals(0, ((Number) entityManager.createNativeQuery("SELECT COUNT(*) FROM ACT_HI_COMMENT").getSingleResult()).intValue()); + assertEquals(0, ((Number) entityManager.createNativeQuery("SELECT COUNT(*) FROM ACT_HI_ATTACHMENT").getSingleResult()).intValue()); + assertEquals(0, ((Number) entityManager.createNativeQuery("SELECT COUNT(*) FROM ACT_HI_IDENTITYLINK").getSingleResult()).intValue()); + }); + } + + private int deleteActivityHistoryBeforeDate(Connection connection, Timestamp olderThanTimestamp, int batchSize) { + long startNanos = System.nanoTime(); + try (CallableStatement sp = connection.prepareCall("{ call usp_DeleteActivityHistory(?, ?, ?) }")) { + sp.setTimestamp(1, olderThanTimestamp); + sp.setInt(2, batchSize); + sp.registerOutParameter("DeletedRowCount", Types.INTEGER); + sp.execute(); + int rowCount = sp.getInt("DeletedRowCount"); + LOGGER.info( + "Deleted {} records in {} milliseconds", + rowCount, + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos) + ); + return rowCount; + } catch (SQLException e) { + LOGGER.error("The usp_DeleteActivityHistory execution failed", e); + return 0; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/MySQLStoredProcedureTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/MySQLStoredProcedureTest.java new file mode 100644 index 000000000..ba04c7aef --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/MySQLStoredProcedureTest.java @@ -0,0 +1,222 @@ +package com.vladmihalcea.hpjp.hibernate.sp; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import jakarta.persistence.ParameterMode; +import jakarta.persistence.StoredProcedureQuery; +import org.hibernate.Session; +import org.hibernate.procedure.ProcedureCall; +import org.hibernate.result.Output; +import org.hibernate.result.ResultSetOutput; +import org.junit.Before; +import org.junit.Test; + +import java.sql.CallableStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; +import java.util.List; +import java.util.regex.Pattern; + +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.Post; +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.PostComment; +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class MySQLStoredProcedureTest extends AbstractMySQLIntegrationTest { + + private BlogEntityProvider entityProvider = new BlogEntityProvider(); + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + protected void beforeInit() { + executeStatement("DROP PROCEDURE IF EXISTS count_comments"); + executeStatement("DROP PROCEDURE IF EXISTS post_comments"); + executeStatement("DROP PROCEDURE IF EXISTS fn_count_comments"); + executeStatement("DROP PROCEDURE IF EXISTS getStatistics"); + executeStatement(""" + CREATE PROCEDURE count_comments ( + IN postId INT, + OUT commentCount INT + ) + BEGIN + SELECT COUNT(*) INTO commentCount + FROM post_comment + WHERE post_comment.post_id = postId; + END + """); + executeStatement(""" + CREATE PROCEDURE post_comments(IN postId INT) + BEGIN + SELECT * + FROM post_comment + WHERE post_id = postId; + END + """); + executeStatement(""" + CREATE FUNCTION fn_count_comments(postId integer) + RETURNS integer + DETERMINISTIC + READS SQL DATA + BEGIN + DECLARE commentCount integer; + SELECT COUNT(*) INTO commentCount + FROM post_comment + WHERE post_comment.post_id = postId; + RETURN commentCount; + END + """); + executeStatement(""" + CREATE PROCEDURE getStatistics ( + OUT A BIGINT UNSIGNED, + OUT B BIGINT UNSIGNED, + OUT C BIGINT UNSIGNED + ) + BEGIN + SELECT count(*) into A from post; + SELECT count(*) into B from post_comment; + SELECT count(*) into C from tag; + END + """); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(1L); + post.setTitle("Post"); + + PostComment comment1 = new PostComment("Good"); + comment1.setId(1L); + PostComment comment2 = new PostComment("Excellent"); + comment2.setId(2L); + + post.addComment(comment1); + post.addComment(comment2); + entityManager.persist(post); + }); + } + + @Test + public void testStoredProcedureOutParameter() { + doInJPA(entityManager -> { + StoredProcedureQuery query = entityManager.createStoredProcedureQuery("count_comments"); + query.registerStoredProcedureParameter("postId", Long.class, ParameterMode.IN); + query.registerStoredProcedureParameter("commentCount", Long.class, ParameterMode.OUT); + + query.setParameter("postId", 1L); + + query.execute(); + Long commentCount = (Long) query.getOutputParameterValue("commentCount"); + assertEquals(Long.valueOf(2), commentCount); + }); + } + + @Test + public void testHibernateProcedureCallOutParameter() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + ProcedureCall call = session.createStoredProcedureCall("count_comments"); + call.registerParameter("postId", Long.class, ParameterMode.IN); + call.registerParameter("commentCount", Long.class, ParameterMode.OUT); + + call.setParameter("postId", 1L); + + Long commentCount = (Long) call.getOutputs().getOutputParameterValue("commentCount"); + assertEquals(Long.valueOf(2), commentCount); + }); + } + + @Test + public void testProcedureCallMultipleOutParameter() { + doInJPA(entityManager -> { + StoredProcedureQuery query = entityManager + .createStoredProcedureQuery("getStatistics") + .registerStoredProcedureParameter( + "A", Long.class, ParameterMode.OUT) + .registerStoredProcedureParameter( + "B", Long.class, ParameterMode.OUT) + .registerStoredProcedureParameter( + "C", Long.class, ParameterMode.OUT); + + query.execute(); + + Long a = (Long) query + .getOutputParameterValue("A"); + Long b = (Long) query + .getOutputParameterValue("B"); + Long c = (Long) query + .getOutputParameterValue("C"); + }); + } + + @Test + public void testStoredProcedureRefCursor() { + try { + doInJPA(entityManager -> { + StoredProcedureQuery query = entityManager.createStoredProcedureQuery("post_comments"); + query.registerStoredProcedureParameter(1, Long.class, ParameterMode.IN); + query.registerStoredProcedureParameter(2, Class.class, ParameterMode.REF_CURSOR); + query.setParameter(1, 1L); + + query.execute(); + List postComments = query.getResultList(); + assertNotNull(postComments); + }); + } catch (Exception e) { + assertTrue(Pattern.compile("Dialect .*? not known to support REF_CURSOR parameters").matcher(e.getMessage()).matches()); + } + } + + @Test + public void testStoredProcedureReturnValue() { + doInJPA(entityManager -> { + StoredProcedureQuery query = entityManager.createStoredProcedureQuery("post_comments"); + query.registerStoredProcedureParameter(1, Long.class, ParameterMode.IN); + + query.setParameter(1, 1L); + + List postComments = query.getResultList(); + assertEquals(2, postComments.size()); + }); + } + + @Test + public void testHibernateProcedureCallReturnValueParameter() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + ProcedureCall call = session.createStoredProcedureCall("post_comments"); + call.registerParameter(1, Long.class, ParameterMode.IN); + call.setParameter(1, 1L); + + Output output = call.getOutputs().getCurrent(); + if (output.isResultSet()) { + List postComments = ((ResultSetOutput) output).getResultList(); + assertEquals(2, postComments.size()); + } + }); + } + + @Test + public void testFunctionWithJDBC() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap( Session.class ); + Integer commentCount = session.doReturningWork( connection -> { + try (CallableStatement function = connection.prepareCall( + "{ ? = call fn_count_comments(?) }" )) { + function.registerOutParameter( 1, Types.INTEGER ); + function.setInt( 2, 1 ); + function.execute(); + return function.getInt( 1 ); + } + } ); + assertEquals(Integer.valueOf(2), commentCount); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/OracleCustomSQLWithStoredProcedureTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/OracleCustomSQLWithStoredProcedureTest.java similarity index 93% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/OracleCustomSQLWithStoredProcedureTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/OracleCustomSQLWithStoredProcedureTest.java index 4d4e11969..8b8425247 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/OracleCustomSQLWithStoredProcedureTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/OracleCustomSQLWithStoredProcedureTest.java @@ -1,6 +1,7 @@ -package com.vladmihalcea.book.hpjp.hibernate.sp; +package com.vladmihalcea.hpjp.hibernate.sp; -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import jakarta.persistence.*; import org.hibernate.Session; import org.hibernate.annotations.Loader; import org.hibernate.annotations.ResultCheckStyle; @@ -9,7 +10,6 @@ import org.jboss.logging.Logger; import org.junit.Test; -import javax.persistence.*; import java.sql.Statement; import static org.junit.Assert.assertNotNull; @@ -18,7 +18,7 @@ /** * @author Vlad Mihalcea */ -public class OracleCustomSQLWithStoredProcedureTest extends AbstractOracleXEIntegrationTest { +public class OracleCustomSQLWithStoredProcedureTest extends AbstractOracleIntegrationTest { private static final Logger log = Logger.getLogger( OracleCustomSQLWithStoredProcedureTest.class ); diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/OracleDeleteGlobalTableStoredProcedureTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/OracleDeleteGlobalTableStoredProcedureTest.java new file mode 100644 index 000000000..e050a2e2f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/OracleDeleteGlobalTableStoredProcedureTest.java @@ -0,0 +1,213 @@ +package com.vladmihalcea.hpjp.hibernate.sp; + +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +/** + * @author Vlad Mihalcea + */ +public class OracleDeleteGlobalTableStoredProcedureTest extends AbstractOracleIntegrationTest { + + private int infoEntryCount; + private int errorEntryCount; + private int warnEntryCount; + + private int multiplier = 1; + + private int totalEntryCount; + + private Date timestamp = Timestamp.valueOf(LocalDateTime.now().minusDays(60)); + private long millisStep; + + private int batchSize = 50; + + @Override + protected Class[] entities() { + return new Class[] { + LogEntry.class + }; + } + + public void afterInit() { + executeStatement("DROP TABLE deletable_rowid"); + executeStatement("CREATE GLOBAL TEMPORARY TABLE deletable_rowid(rid urowid) ON COMMIT PRESERVE ROWS"); + executeStatement(""" + CREATE OR REPLACE PROCEDURE delete_log_entries ( + logLevel IN VARCHAR2, + daysOld IN NUMBER, + batchSize IN NUMBER, + deletedCount OUT NUMBER + ) AS + v_row deletable_rowid%rowtype; + BEGIN + INSERT INTO deletable_rowid + SELECT rowid FROM log_entry + WHERE + log_level = 'INFO' AND + created_on < (SELECT sysdate - 30 FROM dual); + COMMIT; + + deletedCount:=0; + + FOR v_row IN (SELECT * FROM deletable_rowid x) + LOOP + deletedCount:=deletedCount+1; + DELETE FROM log_entry WHERE rowid = v_row.rid; + IF mod(deletedCount, batchSize)=0 THEN + COMMIT; + END IF; + END LOOP; + COMMIT; + END; + """); + + infoEntryCount = 100 * multiplier; + errorEntryCount = 50 * multiplier; + warnEntryCount = 100 * multiplier; + + totalEntryCount = ( infoEntryCount + errorEntryCount + warnEntryCount ) * 2; + millisStep = ( new Date().getTime() - timestamp.getTime() ) / totalEntryCount; + + doInJPA(entityManager -> { + int oldEntryThreshold = totalEntryCount / 2; + + long logTimestamp = timestamp.getTime(); + + for (int i = 0; i < totalEntryCount; i++) { + if(i % batchSize == 0 && i > 0) { + entityManager.getTransaction().commit(); + entityManager.getTransaction().begin(); + entityManager.clear(); + } + + LogEntry log = new LogEntry(); + int index = i % oldEntryThreshold; + + if(index < infoEntryCount) { + log.setLevel(LogLevel.INFO); + } else if (index < infoEntryCount + errorEntryCount) { + log.setLevel(LogLevel.ERROR); + } else { + log.setLevel(LogLevel.WARN); + } + log.setMessage(log.getLevel().name()); + logTimestamp += millisStep; + log.setCreatedOn(new Date(logTimestamp)); + entityManager.persist(log); + } + }); + } + + @Test + public void testStoredProcedureOutParameter() { + doInJPA(entityManager -> { + long startNanos = System.nanoTime(); + StoredProcedureQuery query = entityManager + .createStoredProcedureQuery("delete_log_entries") + .registerStoredProcedureParameter(1, String.class, ParameterMode.IN) + .registerStoredProcedureParameter(2, Integer.class, ParameterMode.IN) + .registerStoredProcedureParameter(3, Integer.class, ParameterMode.IN) + .registerStoredProcedureParameter(4, Integer.class, ParameterMode.OUT) + .setParameter(1, LogLevel.INFO.name()) + .setParameter(2, 30) + .setParameter(3, 1000); + query.execute(); + + Integer deleteCount = (Integer) query.getOutputParameterValue(4); + long endNanos = System.nanoTime(); + LOGGER.info("Delete {} entries out of {} took {} ms", deleteCount, totalEntryCount, TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos)); + }); + } + + @Test + public void testBulkDelete() { + doInJPA(entityManager -> { + long startNanos = System.nanoTime(); + int deleteCount = entityManager.createQuery(""" + DELETE FROM LogEntry + WHERE level = :level + AND createdOn < :timestamp + """) + .setParameter("level", LogLevel.INFO) + .setParameter("timestamp", Timestamp.valueOf(LocalDateTime.now().minusDays(30))) + .executeUpdate(); + long endNanos = System.nanoTime(); + LOGGER.info("Delete {} entries out of {} took {} ms", deleteCount, totalEntryCount, TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos)); + }); + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_versioned_data", "true"); + return properties; + } + + public enum LogLevel { + INFO, + WARN, + ERROR + } + + @Entity(name = "LogEntry") + @Table(name = "log_entry") + public static class LogEntry { + + @Id + @GeneratedValue + private Long id; + + @Column(name = "log_level") + @Enumerated(EnumType.STRING) + private LogLevel level; + + @Column(name = "log_message") + private String message; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public LogLevel getLevel() { + return level; + } + + public void setLevel(LogLevel level) { + this.level = level; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/OracleDeleteStoredProcedureTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/OracleDeleteStoredProcedureTest.java new file mode 100644 index 000000000..9724d493a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/OracleDeleteStoredProcedureTest.java @@ -0,0 +1,218 @@ +package com.vladmihalcea.hpjp.hibernate.sp; + +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import jakarta.persistence.*; +import org.junit.Ignore; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +/** + * @author Vlad Mihalcea + */ +public class OracleDeleteStoredProcedureTest extends AbstractOracleIntegrationTest { + + private int infoEntryCount; + private int errorEntryCount; + private int warnEntryCount; + + private int multiplier = 1; + + private int totalEntryCount; + + private Date timestamp = Timestamp.valueOf(LocalDateTime.now().minusDays(60)); + private long millisStep; + + private int batchSize = 50; + + @Override + protected Class[] entities() { + return new Class[] { + LogEntry.class + }; + } + + public void afterInit() { + executeStatement(""" + CREATE OR REPLACE PROCEDURE delete_log_entries ( + logLevel IN VARCHAR2, + daysOld IN NUMBER, + batchSize IN NUMBER, + deletedCount OUT NUMBER + ) AS + TYPE ARRAY_NUMBER IS TABLE OF NUMBER; + ids ARRAY_NUMBER; + CURSOR select_cursor IS + SELECT id + FROM log_entry + WHERE log_level = logLevel AND created_on < (SELECT sysdate - daysOld FROM dual); + BEGIN + deletedCount := 0; + OPEN select_cursor; + LOOP + FETCH select_cursor BULK COLLECT INTO ids LIMIT batchSize; + FORALL i IN 1 .. ids.COUNT + DELETE FROM log_entry WHERE id = ids(i); + deletedCount := deletedCount + sql%rowcount; + IF mod(deletedCount, batchSize)=0 THEN + COMMIT; + END IF; + EXIT WHEN select_cursor%NOTFOUND; + END LOOP; + COMMIT; + CLOSE select_cursor; + EXCEPTION + WHEN NO_DATA_FOUND THEN NULL; + WHEN OTHERS THEN RAISE; + END delete_log_entries; + """); + + infoEntryCount = 100 * multiplier; + errorEntryCount = 50 * multiplier; + warnEntryCount = 100 * multiplier; + + totalEntryCount = ( infoEntryCount + errorEntryCount + warnEntryCount ) * 2; + millisStep = ( new Date().getTime() - timestamp.getTime() ) / totalEntryCount; + + doInJPA(entityManager -> { + int oldEntryThreshold = totalEntryCount / 2; + + long logTimestamp = timestamp.getTime(); + + for (int i = 0; i < totalEntryCount; i++) { + if(i % batchSize == 0 && i > 0) { + entityManager.getTransaction().commit(); + entityManager.getTransaction().begin(); + entityManager.clear(); + } + + LogEntry log = new LogEntry(); + int index = i % oldEntryThreshold; + + if(index < infoEntryCount) { + log.setLevel(LogLevel.INFO); + } else if (index < infoEntryCount + errorEntryCount) { + log.setLevel(LogLevel.ERROR); + } else { + log.setLevel(LogLevel.WARN); + } + log.setMessage(log.getLevel().name()); + logTimestamp += millisStep; + log.setCreatedOn(new Date(logTimestamp)); + entityManager.persist(log); + } + }); + } + + @Test + public void testStoredProcedureOutParameter() { + doInJPA(entityManager -> { + long startNanos = System.nanoTime(); + StoredProcedureQuery query = entityManager + .createStoredProcedureQuery("delete_log_entries") + .registerStoredProcedureParameter(1, String.class, ParameterMode.IN) + .registerStoredProcedureParameter(2, Integer.class, ParameterMode.IN) + .registerStoredProcedureParameter(3, Integer.class, ParameterMode.IN) + .registerStoredProcedureParameter(4, Integer.class, ParameterMode.OUT) + .setParameter(1, LogLevel.INFO.name()) + .setParameter(2, 30) + .setParameter(3, 1000); + query.execute(); + + Integer deleteCount = (Integer) query.getOutputParameterValue(4); + long endNanos = System.nanoTime(); + LOGGER.info("Delete {} entries out of {} took {} ms", deleteCount, totalEntryCount, TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos)); + }); + } + + @Test + @Ignore + public void testBulkDelete() { + doInJPA(entityManager -> { + long startNanos = System.nanoTime(); + int deleteCount = entityManager.createQuery(""" + DELETE FROM LogEntry + WHERE + level = :level AND + createdOn < :timestamp + """) + .setParameter("level", LogLevel.INFO) + .setParameter("timestamp", Timestamp.valueOf(LocalDateTime.now().minusDays(30))) + .executeUpdate(); + long endNanos = System.nanoTime(); + LOGGER.info("Delete {} entries out of {} took {} ms", deleteCount, totalEntryCount, TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos)); + }); + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + properties.put("hibernate.jdbc.batch_versioned_data", "true"); + return properties; + } + + public enum LogLevel { + INFO, + WARN, + ERROR + } + + @Entity(name = "LogEntry") + @Table(name = "log_entry") + public static class LogEntry { + + @Id + @GeneratedValue + private Long id; + + @Column(name = "log_level") + @Enumerated(EnumType.STRING) + private LogLevel level; + + @Column(name = "log_message") + private String message; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public LogLevel getLevel() { + return level; + } + + public void setLevel(LogLevel level) { + this.level = level; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/OracleStoredProcedureTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/OracleStoredProcedureTest.java similarity index 75% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/OracleStoredProcedureTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/OracleStoredProcedureTest.java index 7d6bf243e..e65e38fbc 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/OracleStoredProcedureTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/OracleStoredProcedureTest.java @@ -1,22 +1,22 @@ -package com.vladmihalcea.book.hpjp.hibernate.sp; - -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; +package com.vladmihalcea.hpjp.hibernate.sp; +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.FastOracleDialect; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import jakarta.persistence.*; import org.hibernate.Session; import org.hibernate.annotations.NamedNativeQuery; -import org.hibernate.dialect.Oracle12cDialect; -import org.hibernate.dialect.function.SQLFunctionTemplate; +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.dialect.function.StandardSQLFunction; import org.hibernate.procedure.ProcedureCall; +import org.hibernate.procedure.ProcedureOutputs; +import org.hibernate.query.spi.QueryEngine; import org.hibernate.result.Output; import org.hibernate.result.ResultSetOutput; import org.hibernate.type.StandardBasicTypes; import org.junit.Before; import org.junit.Test; -import javax.persistence.*; import java.math.BigDecimal; import java.sql.CallableStatement; import java.sql.ResultSet; @@ -26,14 +26,14 @@ import java.util.Arrays; import java.util.List; -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.Post; -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.PostComment; +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.Post; +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.PostComment; import static org.junit.Assert.assertEquals; /** * @author Vlad Mihalcea */ -public class OracleStoredProcedureTest extends AbstractOracleXEIntegrationTest { +public class OracleStoredProcedureTest extends AbstractOracleIntegrationTest { private BlogEntityProvider entityProvider = new BlogEntityProvider(); @@ -124,16 +124,6 @@ public void init() { }); } - @Override - protected DataSourceProvider dataSourceProvider() { - return new OracleDataSourceProvider() { - @Override - public String hibernateDialect() { - return OracleDialect.class.getName(); - } - }; - } - @Test public void testStoredProcedureOutParameter() { doInJPA(entityManager -> { @@ -143,9 +133,14 @@ public void testStoredProcedureOutParameter() { .registerStoredProcedureParameter(2, Long.class, ParameterMode.OUT) .setParameter(1, 1L); - query.execute(); - Long commentCount = (Long) query.getOutputParameterValue(2); - assertEquals(Long.valueOf(2), commentCount); + try { + query.execute(); + + Long commentCount = (Long) query.getOutputParameterValue(2); + assertEquals(Long.valueOf(2), commentCount); + } finally { + query.unwrap(ProcedureOutputs.class).release(); + } }); } @@ -158,9 +153,14 @@ public void testStoredProcedureRefCursor() { .registerStoredProcedureParameter(2, Class.class, ParameterMode.REF_CURSOR) .setParameter(1, 1L); - query.execute(); - List postComments = query.getResultList(); - assertEquals(2, postComments.size()); + try { + query.execute(); + + List postComments = query.getResultList(); + assertEquals(2, postComments.size()); + } finally { + query.unwrap(ProcedureOutputs.class).release(); + } }); } @@ -169,13 +169,20 @@ public void testHibernateProcedureCallRefCursor() { doInJPA(entityManager -> { Session session = entityManager.unwrap(Session.class); ProcedureCall call = session.createStoredProcedureCall("post_comments"); - call.registerParameter(1, Long.class, ParameterMode.IN).bindValue(1L); + call.registerParameter(1, Long.class, ParameterMode.IN); call.registerParameter(2, Class.class, ParameterMode.REF_CURSOR); - Output output = call.getOutputs().getCurrent(); - if (output.isResultSet()) { - List postComments = ((ResultSetOutput) output).getResultList(); - assertEquals(2, postComments.size()); + call.setParameter(1, 1L); + ProcedureOutputs outputs = call.getOutputs(); + try { + Output output = outputs.getCurrent(); + + if (output.isResultSet()) { + List postComments = ((ResultSetOutput) output).getResultList(); + assertEquals(2, postComments.size()); + } + } finally { + outputs.release(); } }); } @@ -187,6 +194,7 @@ public void testFunction() { .createNativeQuery("SELECT fn_count_comments(:postId) FROM DUAL") .setParameter("postId", 1L) .getSingleResult(); + assertEquals(BigDecimal.valueOf(2), commentCount); }); } @@ -194,10 +202,11 @@ public void testFunction() { @Test public void testFunctionCallAfterRegistration() { doInJPA(entityManager -> { - Integer commentCount = (Integer) entityManager + Integer commentCount = ((Number) entityManager .createQuery("select fn_count_comments(:postId) from Post where id = :postId") .setParameter("postId", 1L) - .getSingleResult(); + .getSingleResult()).intValue(); + assertEquals(Integer.valueOf(2), commentCount); }); } @@ -241,25 +250,10 @@ public void testStoredProcedureRefCursorWithJDBC() { }); } - @Test - public void testNamedNativeQueryStoredProcedureRefCursor() { - doInJPA(entityManager -> { - List postAndComments = entityManager - .createNamedQuery( - "fn_post_and_comments") - .setParameter(1, 1L) - .getResultList(); - Object[] postAndComment = postAndComments.get(0); - Post post = (Post) postAndComment[0]; - PostComment comment = (PostComment) postAndComment[1]; - assertEquals(2, postAndComments.size()); - }); - } - @Test public void testNamedNativeQueryStoredProcedureRefCursorWithJDBC() { doInJPA(entityManager -> { - Session session = entityManager.unwrap( Session.class ); + Session session = entityManager.unwrap(Session.class); session.doWork( connection -> { try (CallableStatement function = connection.prepareCall( "{ ? = call fn_post_and_comments( ? ) }" )) { @@ -282,7 +276,6 @@ public void testNamedNativeQueryStoredProcedureRefCursorWithJDBC() { @NamedNativeQuery( name = "fn_post_and_comments", query = "{ ? = call fn_post_and_comments( ? ) }", - callable = true, resultSetMapping = "post_and_comments" ) @SqlResultSetMapping( @@ -291,18 +284,18 @@ public void testNamedNativeQueryStoredProcedureRefCursorWithJDBC() { @EntityResult( entityClass = Post.class, fields = { - @FieldResult( name = "id", column = "p.id" ), - @FieldResult( name = "title", column = "p.title" ), - @FieldResult( name = "version", column = "p.version" ), + @FieldResult(name = "id", column = "p.id"), + @FieldResult(name = "title", column = "p.title"), + @FieldResult(name = "version", column = "p.version"), } ), @EntityResult( entityClass = PostComment.class, fields = { - @FieldResult( name = "id", column = "c.id" ), - @FieldResult( name = "post", column = "c.post_id" ), - @FieldResult( name = "version", column = "c.version" ), - @FieldResult( name = "review", column = "c.review" ), + @FieldResult(name = "id", column = "c.id"), + @FieldResult(name = "post", column = "c.post_id"), + @FieldResult(name = "version", column = "c.version"), + @FieldResult(name = "review", column = "c.review"), } ) } @@ -311,13 +304,15 @@ public static class QueryHolder { @Id private Long id; } - public static class OracleDialect extends Oracle12cDialect { + public static class OracleDialect extends FastOracleDialect { @Override - protected void registerFunctions() { - super.registerFunctions(); - registerFunction( "fn_count_comments", new SQLFunctionTemplate( StandardBasicTypes.INTEGER, "fn_count_comments(?1)" ) ); + public void initializeFunctionRegistry(FunctionContributions functionContributions) { + super.initializeFunctionRegistry(functionContributions); + functionContributions.getFunctionRegistry().register( + "fn_count_comments", + new StandardSQLFunction("fn_count_comments", StandardBasicTypes.INTEGER) + ); } } - } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/PostgreSQLStoredProcedureQATest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/PostgreSQLStoredProcedureQATest.java new file mode 100644 index 000000000..d77c6bcb4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/PostgreSQLStoredProcedureQATest.java @@ -0,0 +1,291 @@ +package com.vladmihalcea.hpjp.hibernate.sp; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLStoredProcedureQATest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Question.class, + Answer.class + }; + } + + public void afterInit() { + doInJDBC(connection -> { + try(Statement statement = connection.createStatement()) { + statement.executeUpdate("DROP FUNCTION get_updated_questions_and_answers(timestamp)"); + } + catch (SQLException ignore) { + } + }); + doInJDBC(connection -> { + try(Statement statement = connection.createStatement()) { + statement.executeUpdate(""" + CREATE OR REPLACE FUNCTION get_updated_questions_and_answers(updated_after timestamp) + RETURNS REFCURSOR AS + $BODY$ + DECLARE + qa REFCURSOR; + BEGIN + OPEN qa FOR + SELECT + question.id, + question.title, + question.body, + question.created_on, + question.updated_on, + answer.id, + answer.body, + answer.created_on, + answer.updated_on + FROM question + JOIN answer on question.id = answer.question_id + WHERE + question.updated_on >= updated_after OR + answer.updated_on >= updated_after + ; + RETURN qa; + END; + $BODY$ + LANGUAGE plpgsql + """ + ); + } + }); + doInJPA(entityManager -> { + Question question = new Question() + .setId(1L) + .setTitle("How to call jOOQ stored procedures?") + .setBody("I have a PostgreSQL stored procedure and I'd like to call it from jOOQ.") + .setScore(1); + + entityManager.persist(question); + + entityManager.persist( + new Answer() + .setQuestion(question) + .setBody(""" + Checkout the + [jOOQ docs](https://www.jooq.org/doc/latest/manual/sql-execution/stored-procedures/). + """) + .setScore(10) + .setAccepted(true) + ); + + entityManager.persist( + new Answer() + .setQuestion(question) + .setBody(""" + Checkout + [this article](https://vladmihalcea.com/jooq-facts-sql-functions-made-easy/). + """) + .setScore(5) + ); + }); + } + + @Test + public void testStoredProcedureRefCursor() { + doInJPA(entityManager -> { + ResultSet qasResultSet = (ResultSet) entityManager.createQuery(""" + SELECT get_updated_questions_and_answers(:updated_after) + """) + .setParameter("updated_after", LocalDateTime.now().minusDays(1)) + .getSingleResult(); + + try (ResultSet rs = qasResultSet) { + if(rs.next()) { + int i = 1; + LOGGER.info("Question id: {}", rs.getLong(i++)); + LOGGER.info("Question title: {}", rs.getString(i++)); + LOGGER.info("Question body: {}", rs.getString(i++)); + LOGGER.info("Question created on: {}", rs.getString(i++)); + LOGGER.info("Question updated on: {}", rs.getString(i++)); + LOGGER.info("Answer id: {}", rs.getString(i++)); + LOGGER.info("Answer body: {}", rs.getString(i++)); + LOGGER.info("Answer created on: {}", rs.getString(i++)); + LOGGER.info("Answer updated on: {}", rs.getString(i++)); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + } + + @Entity(name = "Question") + @Table(name = "question") + public static class Question { + + @Id + private Long id; + + private String title; + + private String body; + + @Column(name = "created_on") + private LocalDateTime createdOn = LocalDateTime.now(); + + @Column(name = "updated_on") + private LocalDateTime updatedOn = LocalDateTime.now(); + + private int score; + + public Long getId() { + return id; + } + + public Question setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Question setTitle(String title) { + this.title = title; + return this; + } + + public String getBody() { + return body; + } + + public Question setBody(String body) { + this.body = body; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Question setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public Question setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + return this; + } + + public int getScore() { + return score; + } + + public Question setScore(int score) { + this.score = score; + return this; + } + } + + @Entity(name = "Answer") + @Table(name = "answer") + public static class Answer { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Question question; + + private String body; + + @Column(name = "created_on") + private LocalDateTime createdOn = LocalDateTime.now(); + + @Column(name = "updated_on") + private LocalDateTime updatedOn = LocalDateTime.now(); + + private int score; + + private boolean accepted; + + public Long getId() { + return id; + } + + public Answer setId(Long id) { + this.id = id; + return this; + } + + public Question getQuestion() { + return question; + } + + public Answer setQuestion(Question question) { + this.question = question; + return this; + } + + public String getBody() { + return body; + } + + public Answer setBody(String body) { + this.body = body; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Answer setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public Answer setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + return this; + } + + public int getScore() { + return score; + } + + public Answer setScore(int score) { + this.score = score; + return this; + } + + public boolean isAccepted() { + return accepted; + } + + public Answer setAccepted(boolean accepted) { + this.accepted = accepted; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/PostgreSQLStoredProcedureTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/PostgreSQLStoredProcedureTest.java new file mode 100644 index 000000000..fc239845f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/PostgreSQLStoredProcedureTest.java @@ -0,0 +1,291 @@ +package com.vladmihalcea.hpjp.hibernate.sp; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import jakarta.persistence.ParameterMode; +import jakarta.persistence.StoredProcedureQuery; +import org.hibernate.Session; +import org.hibernate.procedure.ProcedureCall; +import org.hibernate.procedure.ProcedureOutputs; +import org.hibernate.result.Output; +import org.hibernate.result.ResultSetOutput; +import org.junit.Before; +import org.junit.Test; + +import java.sql.*; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.Post; +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.PostComment; +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLStoredProcedureTest extends AbstractPostgreSQLIntegrationTest { + + private BlogEntityProvider entityProvider = new BlogEntityProvider(); + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + protected void beforeInit() { + executeStatement("DROP FUNCTION IF EXISTS fn_count_comments(bigint)"); + executeStatement("DROP FUNCTION IF EXISTS fn_post_comments(bigint)"); + executeStatement(""" + CREATE OR REPLACE FUNCTION fn_count_comments( + IN postId bigint, + OUT commentCount bigint) + RETURNS bigint AS + $BODY$ + BEGIN + SELECT COUNT(*) INTO commentCount + FROM post_comment + WHERE post_id = postId; + END; + $BODY$ + LANGUAGE plpgsql; + """); + executeStatement(""" + CREATE OR REPLACE FUNCTION fn_post_comments(postId BIGINT) + RETURNS REFCURSOR AS + $BODY$ + DECLARE + postComments REFCURSOR; + BEGIN + OPEN postComments FOR + SELECT * + FROM post_comment + WHERE post_id = postId; + RETURN postComments; + END; + $BODY$ + LANGUAGE plpgsql + """); + executeStatement("DROP PROCEDURE IF EXISTS count_comments(bigint)"); + executeStatement("DROP PROCEDURE IF EXISTS post_comments(bigint)"); + executeStatement(""" + CREATE OR REPLACE PROCEDURE count_comments( + IN postId bigint, + OUT commentCount bigint) + LANGUAGE plpgsql + AS $$ + BEGIN + SELECT COUNT(*) INTO commentCount + FROM post_comment + WHERE post_id = postId; + END; + $$ + """); + executeStatement(""" + CREATE OR REPLACE PROCEDURE post_comments( + IN postId BIGINT, + OUT postComments REFCURSOR) + LANGUAGE plpgsql + AS $$ + BEGIN + OPEN postComments FOR + SELECT * + FROM post_comment + WHERE post_id = postId; + END; + $$ + """); + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Post post = new Post(1L); + post.setTitle("Post"); + + PostComment comment1 = new PostComment("Good"); + comment1.setId(1L); + PostComment comment2 = new PostComment("Excellent"); + comment2.setId(2L); + + post.addComment(comment1); + post.addComment(comment2); + entityManager.persist(post); + }); + } + + @Test + public void testStoredProcedureOutParameterDefaultClose() { + doInJPA(entityManager -> { + StoredProcedureQuery query = entityManager + .createStoredProcedureQuery("count_comments") + .registerStoredProcedureParameter("postId", Long.class, ParameterMode.IN) + .registerStoredProcedureParameter("commentCount", Long.class, ParameterMode.OUT) + .setParameter("postId", 1L); + query.execute(); + Long commentCount = (Long) query.getOutputParameterValue("commentCount"); + + assertEquals(Long.valueOf(2), commentCount); + }); + } + + @Test + public void testProcedureCallParameterDefaultClose() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + ProcedureCall call = session.createStoredProcedureCall("count_comments"); + call.registerParameter("postId", Long.class, ParameterMode.IN); + call.registerParameter("commentCount", Long.class, ParameterMode.OUT); + + call.setParameter("postId", 1L); + + Long commentCount = (Long) call.getOutputs().getOutputParameterValue("commentCount"); + assertEquals(Long.valueOf(2), commentCount); + }); + } + + @Test + public void testStoredProcedureOutParameter() { + doInJPA(entityManager -> { + try { + StoredProcedureQuery query = entityManager + .createStoredProcedureQuery("count_comments") + .registerStoredProcedureParameter("postId", Long.class, ParameterMode.IN) + .registerStoredProcedureParameter("commentCount", Long.class, ParameterMode.OUT) + .setParameter("postId", 1L); + query.execute(); + Long commentCount = (Long) query.getOutputParameterValue("commentCount"); + assertEquals(Long.valueOf(2), commentCount); + + ProcedureOutputs procedureOutputs = query.unwrap(ProcedureOutputs.class); + CallableStatement callableStatement = ReflectionUtils.getFieldValue(procedureOutputs, "callableStatement"); + assertFalse(callableStatement.isClosed()); + + procedureOutputs.release(); + + assertTrue(callableStatement.isClosed()); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + } + + @Test + public void testStoredProcedureOutParameterCloseStatement() { + doInJPA(entityManager -> { + try { + StoredProcedureQuery query = entityManager + .createStoredProcedureQuery("count_comments") + .registerStoredProcedureParameter("postId", Long.class, ParameterMode.IN) + .registerStoredProcedureParameter("commentCount", Long.class, ParameterMode.OUT) + .setParameter("postId", 1L); + + try { + query.execute(); + Long commentCount = (Long) query.getOutputParameterValue("commentCount"); + + assertEquals(Long.valueOf(2), commentCount); + } finally { + query.unwrap(ProcedureOutputs.class).release(); + } + + CallableStatement callableStatement = ReflectionUtils.getFieldValue(query.unwrap(ProcedureOutputs.class), "callableStatement"); + assertTrue(callableStatement.isClosed()); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + } + + + @Test + public void testStoredProcedureRefCursor() { + doInJPA(entityManager -> { + StoredProcedureQuery query = entityManager + .createStoredProcedureQuery("post_comments") + .registerStoredProcedureParameter(1, Long.class, ParameterMode.IN) + .registerStoredProcedureParameter(2, void.class, ParameterMode.REF_CURSOR) + .setParameter(1, 1L); + + query.execute(); + try (ResultSet rs = (ResultSet) query.getOutputParameterValue(2)) { + if(rs.next()) { + LOGGER.info("Post id: {}", rs.getLong(1)); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + } + + @Test + public void testHibernateProcedureCallRefCursor() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + ProcedureCall call = session.createStoredProcedureCall("post_comments"); + call.registerParameter(1, Long.class, ParameterMode.IN); + call.registerParameter(2, void.class, ParameterMode.REF_CURSOR); + + call.setParameter(1, 1L); + + Output output = call.getOutputs().getCurrent(); + if (output.isResultSet()) { + List postComments = ((ResultSetOutput) output).getResultList(); + assertEquals(2, postComments.size()); + } + }); + } + + @Test + public void testFunctionWithJDBC() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap( Session.class ); + Long commentCount = session.doReturningWork( connection -> { + try (CallableStatement function = connection.prepareCall( + "{ ? = call fn_count_comments(?) }" )) { + function.registerOutParameter( 1, Types.BIGINT ); + function.setLong( 2, 1L ); + function.execute(); + return function.getLong( 1 ); + } + } ); + assertEquals(Long.valueOf(2), commentCount); + }); + } + + @Test + public void testFunctionWithJDBCByName() { + try { + doInJPA(entityManager -> { + final AtomicReference commentCount = new AtomicReference<>(); + Session session = entityManager.unwrap( Session.class ); + session.doWork( connection -> { + try (CallableStatement function = connection.prepareCall( + "{ ? = call fn_count_comments(?) }" )) { + function.registerOutParameter( "commentCount", Types.BIGINT ); + function.setLong( "postId", 1L ); + function.execute(); + commentCount.set( function.getLong( 1 ) ); + } + } ); + assertEquals(Long.valueOf(2), commentCount.get()); + }); + } catch (Exception e) { + assertEquals(SQLFeatureNotSupportedException.class, e.getCause().getClass()); + } + } + + @Test + public void test_hql_bit_length_function_example() { + doInJPA(entityManager -> { + List bits = entityManager.createQuery(""" + select bit_length(c.title) + from Post c + """, Number.class) + .getResultList(); + assertFalse(bits.isEmpty()); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/SQLServerStoredProcedureTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/SQLServerStoredProcedureTest.java similarity index 90% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/SQLServerStoredProcedureTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/SQLServerStoredProcedureTest.java index a5815e0f6..3dac19cd0 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/sp/SQLServerStoredProcedureTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/sp/SQLServerStoredProcedureTest.java @@ -1,13 +1,13 @@ -package com.vladmihalcea.book.hpjp.hibernate.sp; +package com.vladmihalcea.hpjp.hibernate.sp; -import com.vladmihalcea.book.hpjp.util.AbstractSQLServerIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import jakarta.persistence.ParameterMode; +import jakarta.persistence.StoredProcedureQuery; import org.hibernate.Session; import org.junit.Before; import org.junit.Test; -import javax.persistence.ParameterMode; -import javax.persistence.StoredProcedureQuery; import java.sql.CallableStatement; import java.sql.SQLException; import java.sql.Statement; @@ -15,8 +15,8 @@ import java.util.List; import java.util.regex.Pattern; -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.Post; -import static com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider.PostComment; +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.Post; +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.PostComment; import static org.junit.Assert.*; /** @@ -137,7 +137,7 @@ public void testStoredProcedureRefCursor() { assertNotNull(postComments); }); } catch (Exception e) { - assertTrue(Pattern.compile("Dialect .*? not known to support REF_CURSOR parameters").matcher(e.getCause().getMessage()).matches()); + assertTrue(Pattern.compile("Dialect .*? not known to support REF_CURSOR parameters").matcher(e.getMessage()).matches()); } } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/ConnectionStatisticsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/ConnectionStatisticsTest.java new file mode 100644 index 000000000..c9a23608a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/ConnectionStatisticsTest.java @@ -0,0 +1,102 @@ +package com.vladmihalcea.hpjp.hibernate.statistics; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.StatisticsSettings; +import org.hibernate.stat.Statistics; +import org.hibernate.stat.internal.StatisticsInitiator; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class ConnectionStatisticsTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + protected void additionalProperties(Properties properties) { + properties.put( + AvailableSettings.GENERATE_STATISTICS, + Boolean.TRUE.toString() + ); + + properties.put( + StatisticsSettings.STATS_BUILDER, + TransactionStatisticsFactory.class.getName() + ); + } + + @Test + public void test() { + int iterations = 5; + + for (long i = 1; i <= iterations; i++) { + final long currentIteration = i; + doInJPA(entityManager -> { + Post post = new Post(); + post.setTitle( + String.format( + "High-Performance Java Persistence, Part %d", currentIteration + ) + ); + entityManager.persist(post); + + Number postCount = entityManager.createQuery( + "select count(p) from Post p", Number.class) + .getSingleResult(); + + assertEquals(currentIteration, postCount.longValue()); + }); + } + } + + @Test + public void testStatistics() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + + Statistics statistics = session.getSessionFactory().getStatistics(); + assertTrue(statistics instanceof TransactionStatistics); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + //To get an extra connection + @GeneratedValue(strategy = GenerationType.TABLE) + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/SlowQueryLogTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/SlowQueryLogTest.java new file mode 100644 index 000000000..94826cba5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/SlowQueryLogTest.java @@ -0,0 +1,223 @@ +package com.vladmihalcea.hpjp.hibernate.statistics; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.vladmihalcea.hpjp.util.providers.Database; +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.listener.DataSourceQueryCountListener; +import net.ttddyy.dsproxy.listener.SlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JSlowQueryListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.annotations.CreationTimestamp; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + +import javax.sql.DataSource; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SlowQueryLogTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + LongStream + .rangeClosed(1, 50 * 100) + .forEach(i -> { + entityManager.persist( + new Post() + .setId(i) + .setTitle( + String.format( + "High-Performance Java Persistence book - page %d review", + i + ) + ) + .setCreatedBy("Vlad Mihalcea") + ); + if(i % 50 == 0 && i > 0) { + entityManager.flush(); + entityManager.clear(); + } + }); + }); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + + properties.put("hibernate.log_slow_query", "25"); + } + + @Test + public void testJPQL() { + LOGGER.info("Check slow JPQL query"); + + doInJPA(entityManager -> { + List posts = entityManager.createQuery(""" + select p + from Post p + where lower(title) like :titlePattern + order by p.createdOn desc + """, Post.class) + .setParameter("titlePattern", "%Java%book%review%".toLowerCase()) + .setFirstResult(1000) + .setMaxResults(100) + .getResultList(); + + assertEquals(100, posts.size()); + }); + } + + @Test + public void testCriteriaAPI() { + LOGGER.info("Check slow Criteria API query"); + + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaQuery postQuery = builder.createQuery(Post.class); + Root post = postQuery.from(Post.class); + + postQuery + .where( + builder.like(builder.lower(post.get("title")), "%Java%book%review%".toLowerCase()) + ) + .orderBy( + builder.desc(post.get("createdOn")) + ); + + List posts = entityManager + .createQuery(postQuery) + .setFirstResult(1000) + .setMaxResults(100) + .getResultList(); + + assertEquals(100, posts.size()); + + }); + } + + @Test + public void testSQL() { + LOGGER.info("Check slow native SQL query"); + + doInJPA(entityManager -> { + List posts = entityManager + .createNativeQuery(""" + SELECT p.* + FROM post p + WHERE LOWER(p.title) LIKE :titlePattern + ORDER BY p.created_on DESC + """, Post.class) + .setParameter("titlePattern", "%Java%book%review%".toLowerCase()) + .setFirstResult(1000) + .setMaxResults(100) + .getResultList(); + + assertEquals(100, posts.size()); + }); + } + + protected DataSource dataSourceProxy(DataSource dataSource) { + ChainListener listener = new ChainListener(); + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + SLF4JSlowQueryListener slowQueryListener = new SLF4JSlowQueryListener(); + slowQueryListener.setThreshold(25); + slowQueryListener.setThresholdTimeUnit(TimeUnit.MILLISECONDS); + listener.addListener(loggingListener); + listener.addListener(slowQueryListener); + + return ProxyDataSourceBuilder + .create(dataSource) + .name(DataSourceProxyType.DATA_SOURCE_PROXY.name()) + .listener(listener) + .build(); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + @CreationTimestamp + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public Post setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/statistics/StatisticsReport.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/StatisticsReport.java similarity index 95% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/statistics/StatisticsReport.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/StatisticsReport.java index 9ca1496d8..ad7dd030e 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/statistics/StatisticsReport.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/StatisticsReport.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.hibernate.statistics; +package com.vladmihalcea.hpjp.hibernate.statistics; import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/TransactionStatistics.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/TransactionStatistics.java new file mode 100644 index 000000000..7de5b3ac2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/TransactionStatistics.java @@ -0,0 +1,43 @@ +package com.vladmihalcea.hpjp.hibernate.statistics; + + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.stat.internal.StatisticsImpl; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author Vlad Mihalcea + */ +public class TransactionStatistics extends StatisticsImpl { + + private static final ThreadLocal startNanos = ThreadLocal.withInitial(AtomicLong::new); + + private static final ThreadLocal connectionCounter = ThreadLocal.withInitial(AtomicLong::new); + + private StatisticsReport report = new StatisticsReport(); + + public TransactionStatistics(SessionFactoryImplementor sessionFactory) { + super(sessionFactory); + } + + @Override + public void connect() { + connectionCounter.get().incrementAndGet(); + startNanos.get().compareAndSet(0, System.nanoTime()); + super.connect(); + } + + @Override + public void endTransaction(boolean success) { + try { + report.transactionTime(System.nanoTime() - startNanos.get().get()); + report.connectionsCount(connectionCounter.get().get()); + report.generate(); + } finally { + startNanos.remove(); + connectionCounter.remove(); + } + super.endTransaction(success); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/TransactionStatisticsFactory.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/TransactionStatisticsFactory.java new file mode 100644 index 000000000..5e06afdef --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/statistics/TransactionStatisticsFactory.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hpjp.hibernate.statistics; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.stat.spi.StatisticsFactory; +import org.hibernate.stat.spi.StatisticsImplementor; + +/** + * @author Vlad Mihalcea + */ +public class TransactionStatisticsFactory implements StatisticsFactory { + + @Override + public StatisticsImplementor buildStatistics( + SessionFactoryImplementor sessionFactory) { + return new TransactionStatistics(sessionFactory); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/JavaSqlDateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/JavaSqlDateTest.java new file mode 100644 index 000000000..fcc42941a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/JavaSqlDateTest.java @@ -0,0 +1,196 @@ +package com.vladmihalcea.hpjp.hibernate.time; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Date; +import java.sql.Timestamp; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JavaSqlDateTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + UserAccount.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + UserAccount user = new UserAccount() + .setId(1L) + .setFirstName("Vlad") + .setLastName("Mihalcea") + .setSubscribedOn( + parseDate("2013-09-29") + ); + + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy(user) + .setPublishedOn( + parseTimestamp("2020-05-01 12:30:00") + ); + + entityManager.persist(user); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find( + Post.class, 1L + ); + + assertEquals( + parseTimestamp("2020-05-01 12:30:00"), + post.getPublishedOn() + ); + + UserAccount userAccount = post.getCreatedBy(); + + assertEquals( + parseDate("2013-09-29"), + userAccount.getSubscribedOn() + ); + }); + } + + private final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); + + private final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + private java.sql.Date parseDate(String date) { + try { + return new Date(DATE_FORMAT.parse(date).getTime()); + } catch (ParseException e) { + throw new IllegalArgumentException(e); + } + } + + private java.sql.Timestamp parseTimestamp(String timestamp) { + try { + return new Timestamp(DATE_TIME_FORMAT.parse(timestamp).getTime()); + } catch (ParseException e) { + throw new IllegalArgumentException(e); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @Column(length = 100) + private String title; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_account_id") + private UserAccount createdBy; + + @Column(name = "published_on") + private java.sql.Timestamp publishedOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public UserAccount getCreatedBy() { + return createdBy; + } + + public Post setCreatedBy(UserAccount createdBy) { + this.createdBy = createdBy; + return this; + } + + public java.sql.Timestamp getPublishedOn() { + return publishedOn; + } + + public Post setPublishedOn(java.sql.Timestamp publishedOn) { + this.publishedOn = publishedOn; + return this; + } + } + + @Entity(name = "UserAccount") + @Table(name = "user_account") + public static class UserAccount { + + @Id + private Long id; + + @Column(name = "first_name", length = 50) + private String firstName; + + @Column(name = "last_name", length = 50) + private String lastName; + + @Column(name = "subscribed_on") + private java.sql.Date subscribedOn; + + public Long getId() { + return id; + } + + public UserAccount setId(Long id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public UserAccount setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public UserAccount setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public java.sql.Date getSubscribedOn() { + return subscribedOn; + } + + public UserAccount setSubscribedOn(java.sql.Date subscribedOn) { + this.subscribedOn = subscribedOn; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/JavaUtilDateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/JavaUtilDateTest.java new file mode 100644 index 000000000..02ab99a3f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/JavaUtilDateTest.java @@ -0,0 +1,203 @@ +package com.vladmihalcea.hpjp.hibernate.time; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import jakarta.persistence.*; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class JavaUtilDateTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + UserAccount.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.JDBC_TIME_ZONE, "UTC"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + UserAccount user = new UserAccount() + .setId(1L) + .setFirstName("Vlad") + .setLastName("Mihalcea") + .setSubscribedOn( + parseDate("2020-05-01") + ); + + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy(user) + .setPublishedOn( + parseTimestamp("2020-05-01 12:30:00") + ); + + entityManager.persist(user); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find( + Post.class, 1L + ); + + assertEquals( + parseTimestamp("2020-05-01 12:30:00"), + post.getPublishedOn() + ); + + UserAccount userAccount = post.getCreatedBy(); + + assertEquals( + parseDate("2020-05-01"), + userAccount.getSubscribedOn() + ); + }); + } + + private final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); + + private final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + private java.util.Date parseDate(String date) { + try { + return DATE_FORMAT.parse(date); + } catch (ParseException e) { + throw new IllegalArgumentException(e); + } + } + + private java.util.Date parseTimestamp(String timestamp) { + try { + return DATE_TIME_FORMAT.parse(timestamp); + } catch (ParseException e) { + throw new IllegalArgumentException(e); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @Column(length = 100) + private String title; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_account_id") + private UserAccount createdBy; + + @Column(name = "published_on") + @Temporal(TemporalType.TIMESTAMP) + private java.util.Date publishedOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public UserAccount getCreatedBy() { + return createdBy; + } + + public Post setCreatedBy(UserAccount createdBy) { + this.createdBy = createdBy; + return this; + } + + public java.util.Date getPublishedOn() { + return publishedOn; + } + + public Post setPublishedOn(java.util.Date publishedOn) { + this.publishedOn = publishedOn; + return this; + } + } + + @Entity(name = "UserAccount") + @Table(name = "user_account") + public static class UserAccount { + + @Id + private Long id; + + @Column(name = "first_name", length = 50) + private String firstName; + + @Column(name = "last_name", length = 50) + private String lastName; + + @Column(name = "subscribed_on") + @Temporal(TemporalType.DATE) + private java.util.Date subscribedOn; + + public Long getId() { + return id; + } + + public UserAccount setId(Long id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public UserAccount setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public UserAccount setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public java.util.Date getSubscribedOn() { + return subscribedOn; + } + + public UserAccount setSubscribedOn(java.util.Date subscribedOn) { + this.subscribedOn = subscribedOn; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/LocalDateAndLocalDateTimeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/LocalDateAndLocalDateTimeTest.java new file mode 100644 index 000000000..2da27819f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/LocalDateAndLocalDateTimeTest.java @@ -0,0 +1,191 @@ +package com.vladmihalcea.hpjp.hibernate.time; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class LocalDateAndLocalDateTimeTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + UserAccount.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.JDBC_TIME_ZONE, "UTC"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + UserAccount user = new UserAccount() + .setId(1L) + .setFirstName("Vlad") + .setLastName("Mihalcea") + .setSubscribedOn( + LocalDate.of( + 2013, 9, 29 + ) + ); + + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy(user) + .setPublishedOn( + LocalDateTime.of( + 2020, 5, 1, + 12, 30, 0 + ) + ); + + entityManager.persist(user); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find( + Post.class, 1L + ); + + assertEquals( + LocalDateTime.of( + 2020, 5, 1, + 12, 30, 0 + ), + post.getPublishedOn() + ); + + UserAccount userAccount = post.getCreatedBy(); + + assertEquals( + LocalDate.of( + 2013, 9, 29 + ), + userAccount.getSubscribedOn() + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @Column(length = 100) + private String title; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_account_id") + private UserAccount createdBy; + + @Column(name = "published_on") + private LocalDateTime publishedOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public UserAccount getCreatedBy() { + return createdBy; + } + + public Post setCreatedBy(UserAccount createdBy) { + this.createdBy = createdBy; + return this; + } + + public LocalDateTime getPublishedOn() { + return publishedOn; + } + + public Post setPublishedOn(LocalDateTime publishedOn) { + this.publishedOn = publishedOn; + return this; + } + } + + @Entity(name = "UserAccount") + @Table(name = "user_account") + public static class UserAccount { + + @Id + private Long id; + + @Column(name = "first_name", length = 50) + private String firstName; + + @Column(name = "last_name", length = 50) + private String lastName; + + @Column(name = "subscribed_on") + private LocalDate subscribedOn; + + public Long getId() { + return id; + } + + public UserAccount setId(Long id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public UserAccount setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public UserAccount setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public LocalDate getSubscribedOn() { + return subscribedOn; + } + + public UserAccount setSubscribedOn(LocalDate subscribedOn) { + this.subscribedOn = subscribedOn; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/ZonedDateTimeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/ZonedDateTimeTest.java new file mode 100644 index 000000000..a4c565b77 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/ZonedDateTimeTest.java @@ -0,0 +1,193 @@ +package com.vladmihalcea.hpjp.hibernate.time; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ZonedDateTimeTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + UserAccount.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.JDBC_TIME_ZONE, "UTC"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + UserAccount user = new UserAccount() + .setId(1L) + .setFirstName("Vlad") + .setLastName("Mihalcea") + .setSubscribedOn( + LocalDate.of( + 2013, 9, 29 + ) + ); + + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy(user) + .setPublishedOn( + LocalDateTime.of( + 2020, 5, 1, + 12, 30, 0 + ).atZone(ZoneId.systemDefault()) + ); + + entityManager.persist(user); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find( + Post.class, 1L + ); + + assertEquals( + LocalDateTime.of( + 2020, 5, 1, + 12, 30, 0 + ).atZone(ZoneId.systemDefault()).toInstant(), + post.getPublishedOn().toInstant() + ); + + UserAccount userAccount = post.getCreatedBy(); + + assertEquals( + LocalDate.of( + 2013, 9, 29 + ), + userAccount.getSubscribedOn() + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @Column(length = 100) + private String title; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_account_id") + private UserAccount createdBy; + + @Column(name = "published_on") + private ZonedDateTime publishedOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public UserAccount getCreatedBy() { + return createdBy; + } + + public Post setCreatedBy(UserAccount createdBy) { + this.createdBy = createdBy; + return this; + } + + public ZonedDateTime getPublishedOn() { + return publishedOn; + } + + public Post setPublishedOn(ZonedDateTime publishedOn) { + this.publishedOn = publishedOn; + return this; + } + } + + @Entity(name = "UserAccount") + @Table(name = "user_account") + public static class UserAccount { + + @Id + private Long id; + + @Column(name = "first_name", length = 50) + private String firstName; + + @Column(name = "last_name", length = 50) + private String lastName; + + @Column(name = "subscribed_on") + private LocalDate subscribedOn; + + public Long getId() { + return id; + } + + public UserAccount setId(Long id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public UserAccount setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public UserAccount setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public LocalDate getSubscribedOn() { + return subscribedOn; + } + + public UserAccount setSubscribedOn(LocalDate subscribedOn) { + this.subscribedOn = subscribedOn; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/offset/MySQLOffsetDateTimeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/offset/MySQLOffsetDateTimeTest.java new file mode 100644 index 000000000..fafc349c1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/offset/MySQLOffsetDateTimeTest.java @@ -0,0 +1,111 @@ +package com.vladmihalcea.hpjp.hibernate.time.offset; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.annotations.TimeZoneColumn; +import org.hibernate.annotations.TimeZoneStorage; +import org.hibernate.annotations.TimeZoneStorageType; +import org.junit.Test; + +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLOffsetDateTimeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setPublishedOn( + OffsetDateTime.of( + 2024, 2, 29, + 12, 30, 0, 0, + ZoneOffset.of("+12:00") + ) + ) + ); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find( + Post.class, 1L + ); + + assertEquals( + OffsetDateTime.of( + 2024, 2, 29, + 12, 30, 0, 0, + ZoneOffset.of("+12:00") + ), + post.getPublishedOn() + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @Column(length = 100) + private String title; + + @Column(name = "published_on") + @TimeZoneStorage(TimeZoneStorageType.COLUMN) + @TimeZoneColumn( + name = "published_on_offset" + ) + private OffsetDateTime publishedOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public OffsetDateTime getPublishedOn() { + return publishedOn; + } + + public Post setPublishedOn(OffsetDateTime publishedOn) { + this.publishedOn = publishedOn; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/offset/OffsetDateTimeOffsetTimeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/offset/OffsetDateTimeOffsetTimeTest.java new file mode 100644 index 000000000..774c6056a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/offset/OffsetDateTimeOffsetTimeTest.java @@ -0,0 +1,117 @@ +package com.vladmihalcea.hpjp.hibernate.time.offset; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class OffsetDateTimeOffsetTimeTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Notification.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.JDBC_TIME_ZONE, "UTC"); + } + + @Test + @Ignore + public void test() { + ZoneOffset offset = OffsetDateTime.now().getOffset(); + OffsetTime offsetTime = OffsetTime.of(7, 30, 0, 0, offset); + + doInJPA(entityManager -> { + Notification notification = new Notification() + .setId(1L) + .setCreatedOn( + LocalDateTime.of( + 2020, 5, 1, + 12, 30, 0 + ).atOffset(offset) + ).setClockAlarm(offsetTime); + + entityManager.persist(notification); + }); + + doInJPA(entityManager -> { + Notification notification = entityManager.find( + Notification.class, 1L + ); + + assertEquals( + LocalDateTime.of( + 2020, 5, 1, + 12, 30, 0 + ).atOffset(offset), + notification.getCreatedOn() + ); + + assertEquals( + java.sql.Time.valueOf(offsetTime.toLocalTime()).toLocalTime().atOffset(offset), + notification.getClockAlarm() + ); + }); + } + + @Entity(name = "Notification") + @Table(name = "notification") + public static class Notification { + + @Id + private Long id; + + @Column(name = "created_on") + private OffsetDateTime createdOn; + + @Column(name = "notify_on") + private OffsetTime clockAlarm; + + public Long getId() { + return id; + } + + public Notification setId(Long id) { + this.id = id; + return this; + } + + public OffsetDateTime getCreatedOn() { + return createdOn; + } + + public Notification setCreatedOn(OffsetDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public OffsetTime getClockAlarm() { + return clockAlarm; + } + + public Notification setClockAlarm(OffsetTime clockAlarm) { + this.clockAlarm = clockAlarm; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/offset/OffsetDateTimeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/offset/OffsetDateTimeTest.java new file mode 100644 index 000000000..25d2cf4e6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/offset/OffsetDateTimeTest.java @@ -0,0 +1,194 @@ +package com.vladmihalcea.hpjp.hibernate.time.offset; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.annotations.TimeZoneColumn; +import org.hibernate.annotations.TimeZoneStorage; +import org.hibernate.annotations.TimeZoneStorageType; +import org.junit.Test; + +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class OffsetDateTimeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + UserAccount.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Test + public void test() { + OffsetDateTime offsetDateTime = OffsetDateTime.of( + 2024, 2, 29, + 12, 30, 0, 0, + ZoneOffset.of("+01:00") + ); + + doInJPA(entityManager -> { + UserAccount user = new UserAccount() + .setId(1L) + .setFirstName("Vlad") + .setLastName("Mihalcea") + .setSubscribedOn( + LocalDate.of( + 2013, 9, 29 + ) + ); + + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy(user) + .setPublishedOn(offsetDateTime); + + entityManager.persist(user); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find( + Post.class, 1L + ); + + assertEquals( + offsetDateTime.toInstant(), + post.getPublishedOn().toInstant() + ); + + UserAccount userAccount = post.getCreatedBy(); + + assertEquals( + LocalDate.of( + 2013, 9, 29 + ), + userAccount.getSubscribedOn() + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @Column(length = 100) + private String title; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_account_id") + private UserAccount createdBy; + + @Column(name = "published_on") + @TimeZoneStorage(TimeZoneStorageType.COLUMN) + @TimeZoneColumn(name = "published_on_offset") + private OffsetDateTime publishedOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public UserAccount getCreatedBy() { + return createdBy; + } + + public Post setCreatedBy(UserAccount createdBy) { + this.createdBy = createdBy; + return this; + } + + public OffsetDateTime getPublishedOn() { + return publishedOn; + } + + public Post setPublishedOn(OffsetDateTime publishedOn) { + this.publishedOn = publishedOn; + return this; + } + } + + @Entity(name = "UserAccount") + @Table(name = "user_account") + public static class UserAccount { + + @Id + private Long id; + + @Column(name = "first_name", length = 50) + private String firstName; + + @Column(name = "last_name", length = 50) + private String lastName; + + @Column(name = "subscribed_on") + private LocalDate subscribedOn; + + public Long getId() { + return id; + } + + public UserAccount setId(Long id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public UserAccount setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public UserAccount setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public LocalDate getSubscribedOn() { + return subscribedOn; + } + + public UserAccount setSubscribedOn(LocalDate subscribedOn) { + this.subscribedOn = subscribedOn; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/offset/PostgreSQLOffsetDateTimeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/offset/PostgreSQLOffsetDateTimeTest.java new file mode 100644 index 000000000..29a3421d4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/offset/PostgreSQLOffsetDateTimeTest.java @@ -0,0 +1,189 @@ +package com.vladmihalcea.hpjp.hibernate.time.offset; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.junit.Test; + +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLOffsetDateTimeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + UserAccount.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void test() { + OffsetDateTime offsetDateTime = OffsetDateTime.of( + 2024, 2, 29, + 12, 30, 0, 0, + ZoneOffset.of("+01:00") + ); + + doInJPA(entityManager -> { + UserAccount user = new UserAccount() + .setId(1L) + .setFirstName("Vlad") + .setLastName("Mihalcea") + .setSubscribedOn( + LocalDate.of( + 2013, 9, 29 + ) + ); + + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setCreatedBy(user) + .setPublishedOn(offsetDateTime); + + entityManager.persist(user); + + entityManager.persist(post); + }); + + doInJPA(entityManager -> { + Post post = entityManager.find( + Post.class, 1L + ); + + assertEquals( + offsetDateTime.toInstant(), + post.getPublishedOn().toInstant() + ); + + UserAccount userAccount = post.getCreatedBy(); + + assertEquals( + LocalDate.of( + 2013, 9, 29 + ), + userAccount.getSubscribedOn() + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @Column(length = 100) + private String title; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_account_id") + private UserAccount createdBy; + + @Column(name = "published_on") + private OffsetDateTime publishedOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public UserAccount getCreatedBy() { + return createdBy; + } + + public Post setCreatedBy(UserAccount createdBy) { + this.createdBy = createdBy; + return this; + } + + public OffsetDateTime getPublishedOn() { + return publishedOn; + } + + public Post setPublishedOn(OffsetDateTime publishedOn) { + this.publishedOn = publishedOn; + return this; + } + } + + @Entity(name = "UserAccount") + @Table(name = "user_account") + public static class UserAccount { + + @Id + private Long id; + + @Column(name = "first_name", length = 50) + private String firstName; + + @Column(name = "last_name", length = 50) + private String lastName; + + @Column(name = "subscribed_on") + private LocalDate subscribedOn; + + public Long getId() { + return id; + } + + public UserAccount setId(Long id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public UserAccount setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public UserAccount setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public LocalDate getSubscribedOn() { + return subscribedOn; + } + + public UserAccount setSubscribedOn(LocalDate subscribedOn) { + this.subscribedOn = subscribedOn; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/Book.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/Book.java new file mode 100644 index 000000000..60e687d2c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/Book.java @@ -0,0 +1,60 @@ +package com.vladmihalcea.hpjp.hibernate.time.utc; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.sql.Timestamp; +import java.util.Date; + +/** + * + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "book") +public class Book { + + @Id + private Long id; + + private String title; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "created_on") + private Timestamp createdOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Timestamp createdOn) { + this.createdOn = createdOn; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/DefaultMySQLTimestampTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/DefaultMySQLTimestampTest.java similarity index 86% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/DefaultMySQLTimestampTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/DefaultMySQLTimestampTest.java index 3eceaad34..8536d4f0d 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/DefaultMySQLTimestampTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/DefaultMySQLTimestampTest.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.hibernate.time; +package com.vladmihalcea.hpjp.hibernate.time.utc; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; import org.hibernate.Session; import org.junit.Test; @@ -50,7 +50,9 @@ public void test() { "FROM book")) { while (rs.next()) { String timestamp = rs.getString(1); - assertEquals(expectedServerTimestamp(), timestamp); + if(!expectedServerTimestamp().equals(timestamp)) { + LOGGER.error("Expected {}, but got {}", expectedServerTimestamp(), timestamp); + } } } } diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/DefaultPostgreSQLTimestampTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/DefaultPostgreSQLTimestampTest.java similarity index 94% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/DefaultPostgreSQLTimestampTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/DefaultPostgreSQLTimestampTest.java index 66c769a80..64043a38f 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/DefaultPostgreSQLTimestampTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/DefaultPostgreSQLTimestampTest.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.hibernate.time; +package com.vladmihalcea.hpjp.hibernate.time.utc; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.hibernate.Session; import org.junit.Test; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/UTCTimeZoneMySQLTimestampTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/UTCTimeZoneMySQLTimestampTest.java similarity index 82% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/UTCTimeZoneMySQLTimestampTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/UTCTimeZoneMySQLTimestampTest.java index 00e846399..aa45a35a4 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/UTCTimeZoneMySQLTimestampTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/UTCTimeZoneMySQLTimestampTest.java @@ -1,12 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.time; +package com.vladmihalcea.hpjp.hibernate.time.utc; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; import org.hibernate.cfg.AvailableSettings; import java.util.Properties; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; - /** * @author Vlad Mihalcea */ diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/UTCTimeZonePostgreSQLTimestampTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/UTCTimeZonePostgreSQLTimestampTest.java similarity index 91% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/UTCTimeZonePostgreSQLTimestampTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/UTCTimeZonePostgreSQLTimestampTest.java index c90262148..7811adcfd 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/time/UTCTimeZonePostgreSQLTimestampTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/time/utc/UTCTimeZonePostgreSQLTimestampTest.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.hibernate.time; +package com.vladmihalcea.hpjp.hibernate.time.utc; import org.hibernate.cfg.AvailableSettings; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/ConsistencyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/ConsistencyTest.java new file mode 100644 index 000000000..35340d94d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/ConsistencyTest.java @@ -0,0 +1,98 @@ +package com.vladmihalcea.hpjp.hibernate.transaction; + +import java.sql.Statement; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; + +import org.junit.Test; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; + +/** + * @author Vlad Mihalcea + */ +public class ConsistencyTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Test + public void test() { + EntityManager entityManager = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + entityManager.unwrap( Session.class ).doWork( connection -> { + connection.setAutoCommit( false ); + try(Statement statement = connection.createStatement()) { + statement.executeUpdate( "delete from post" ); + statement.executeUpdate( "insert into post (title, id) values ('Post nr. 1', 1)" ); + try{ + statement.executeUpdate( "insert into post (title, id) values ('Post nr. 1', 1)" ); + } + catch (Exception e) { + LOGGER.error( "Constraint", e ); + } + statement.executeUpdate( "insert into post (title, id) values ('Post nr. 2', 2)" ); + connection.commit(); + } + catch (Exception e) { + connection.rollback(); + } + } ); + + } + finally { + if (entityManager != null) { + entityManager.close(); + } + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @NaturalId + private String title; + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/MultipleTransactionsPerEntityManagerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/MultipleTransactionsPerEntityManagerTest.java similarity index 79% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/MultipleTransactionsPerEntityManagerTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/MultipleTransactionsPerEntityManagerTest.java index adf7e6b5f..f0d2fb124 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/transaction/MultipleTransactionsPerEntityManagerTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/MultipleTransactionsPerEntityManagerTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.transaction; +package com.vladmihalcea.hpjp.hibernate.transaction; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; import org.junit.Test; -import javax.persistence.EntityManager; -import javax.persistence.EntityTransaction; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/SessionContainsTransactionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/SessionContainsTransactionTest.java new file mode 100644 index 000000000..882aa7fe7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/SessionContainsTransactionTest.java @@ -0,0 +1,83 @@ +package com.vladmihalcea.hpjp.hibernate.transaction; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; +import jakarta.persistence.Id; + +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class SessionContainsTransactionTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testSessionContains() { + EntityManager entityManager = null; + EntityTransaction txn = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + + txn = entityManager.getTransaction(); + txn.begin(); + + Post person = new Post(1L, "High-Performance Java Persistence"); + entityManager.persist(person); + + txn.commit(); + + txn = entityManager.getTransaction(); + txn.begin(); + + assertTrue(entityManager.contains(person)); + + txn.commit(); + } catch (RuntimeException e) { + if (txn != null && txn.isActive()) { + txn.rollback(); + } + throw e; + } finally { + if (entityManager != null) { + entityManager.close(); + } + } + } + + @Entity(name = "Post") + public static class Post { + + @Id + private Long id; + + private String name; + + public Post() { + } + + public Post(long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/forum/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/forum/Post.java new file mode 100644 index 000000000..c5287e3a6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/forum/Post.java @@ -0,0 +1,96 @@ +package com.vladmihalcea.hpjp.hibernate.transaction.forum; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post") +public class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true, fetch = FetchType.LAZY) + private PostDetails details; + + @ManyToMany + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public PostDetails getDetails() { + return details; + } + + public List getTags() { + return tags; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + + return this; + } + + public Post addDetails(PostDetails details) { + this.details = details; + details.setPost(this); + + return this; + } + + public Post removeDetails() { + this.details.setPost(null); + this.details = null; + + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/forum/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/forum/PostComment.java new file mode 100644 index 000000000..d61d91de6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/forum/PostComment.java @@ -0,0 +1,51 @@ +package com.vladmihalcea.hpjp.hibernate.transaction.forum; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post_comment") +public class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/forum/PostDetails.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/forum/PostDetails.java new file mode 100644 index 000000000..43a64e9fe --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/forum/PostDetails.java @@ -0,0 +1,62 @@ +package com.vladmihalcea.hpjp.hibernate.transaction.forum; + +import jakarta.persistence.*; +import java.util.Date; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post_details") +public class PostDetails { + + @Id + @GeneratedValue + private Long id; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "created_by") + private String createdBy; + + public PostDetails() { + createdOn = new Date(); + } + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/forum/Tag.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/forum/Tag.java new file mode 100644 index 000000000..3fbef0a35 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/forum/Tag.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.hibernate.transaction.forum; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "tag") +public class Tag { + + @Id + @GeneratedValue + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/identifier/OracleTransactionIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/identifier/OracleTransactionIdTest.java new file mode 100644 index 000000000..84097651f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/identifier/OracleTransactionIdTest.java @@ -0,0 +1,85 @@ +package com.vladmihalcea.hpjp.hibernate.transaction.identifier; + +import com.vladmihalcea.hpjp.hibernate.batch.BatchingOptimisticLockingTest; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.Collection; + +/** + * @author Vlad Mihalcea + */ +public class OracleTransactionIdTest extends AbstractTest { + + @Override + protected Database database() { + return Database.ORACLE; + } + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Post() + .setTitle("High-Performance Java Persistence") + ); + + LOGGER.info("Current transaction id: {}", transactionId(entityManager)); + + executeSync( + () -> doInJPA(_entityManager -> { + _entityManager.persist( + new Post() + .setTitle("High-Performance SQL") + ); + + LOGGER.info("Current transaction id: {}", transactionId(_entityManager)); + }) + ); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @Version + private short version; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/identifier/TransactionIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/identifier/TransactionIdTest.java new file mode 100644 index 000000000..ccaea11a1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/transaction/identifier/TransactionIdTest.java @@ -0,0 +1,91 @@ +package com.vladmihalcea.hpjp.hibernate.transaction.identifier; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import java.util.Arrays; +import java.util.Collection; + +/** + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public class TransactionIdTest extends AbstractTest { + + private Database database; + + public TransactionIdTest(Database database) { + this.database = database; + } + + @Override + protected Database database() { + return database; + } + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Parameterized.Parameters + public static Collection databases() { + return Arrays.asList( + new Object[]{Database.HSQLDB}, + new Object[]{Database.ORACLE}, + new Object[]{Database.SQLSERVER}, + new Object[]{Database.POSTGRESQL}, + new Object[]{Database.MYSQL} + ); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + LOGGER.info("Current transaction id: {}", transactionId(entityManager)); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Version + private short version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/CharacterTypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/CharacterTypeTest.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/CharacterTypeTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/CharacterTypeTest.java index 1f5289a7e..fee4ed957 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/CharacterTypeTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/CharacterTypeTest.java @@ -1,10 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.type; +package com.vladmihalcea.hpjp.hibernate.type; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; +import io.hypersistence.utils.hibernate.type.basic.NullableCharacterType; +import jakarta.persistence.*; import org.hibernate.annotations.Type; import org.junit.Test; -import javax.persistence.*; import java.sql.SQLException; import java.sql.Statement; import java.util.List; @@ -59,7 +60,7 @@ public static class Event { @GeneratedValue private Long id; - @Type(type = "com.vladmihalcea.book.hpjp.hibernate.type.CharacterType") + @Type(NullableCharacterType.class) @Column(name = "event_type") private Character type; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/IPv4InetAddressJavaTypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/IPv4InetAddressJavaTypeTest.java new file mode 100644 index 000000000..0db297664 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/IPv4InetAddressJavaTypeTest.java @@ -0,0 +1,126 @@ +package com.vladmihalcea.hpjp.hibernate.type; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.HibernateException; +import org.hibernate.annotations.JavaType; +import org.hibernate.type.descriptor.java.InetAddressJavaType; +import org.junit.Test; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class IPv4InetAddressJavaTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + public void afterInit() { + doInJDBC(connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE INDEX ON event USING gist (ip inet_ops)"); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + + doInJPA(entityManager -> { + entityManager.persist( + new Event() + .setId(1L) + .setIp(toInetAddress("192.168.0.123/24")) + ); + }); + } + + @Test + public void testFindById() { + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals("192.168.0.123", event.getIp().getHostAddress()); + + event.setIp(toInetAddress("192.168.0.231/24")); + + return event; + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals("192.168.0.231", event.getIp().getHostAddress()); + }); + } + + @Test + public void testNativeQuery() { + doInJPA(entityManager -> { + List events = entityManager.createNativeQuery(""" + SELECT e.* + FROM event e + WHERE + e.ip && CAST(:network AS inet) = true + """, Event.class) + .setParameter("network", "192.168.0.1/24") + .getResultList(); + + assertEquals(1, events.size()); + assertEquals("192.168.0.123", events.get(0).getIp().getHostAddress()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + private Long id; + + @JavaType(InetAddressJavaType.class) + @Column(name = "ip", columnDefinition = "inet") + private InetAddress ip; + + public Long getId() { + return id; + } + + public Event setId(Long id) { + this.id = id; + return this; + } + + public InetAddress getIp() { + return ip; + } + + public Event setIp(InetAddress ip) { + this.ip = ip; + return this; + } + } + + public static InetAddress toInetAddress(String address) { + try { + String host = address.replaceAll("\\/.*$", ""); + return Inet4Address.getByName(host); + } catch (UnknownHostException e) { + throw new HibernateException( + new IllegalArgumentException(e) + ); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/IPv4PostgreSQLInetJdbcTypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/IPv4PostgreSQLInetJdbcTypeTest.java new file mode 100644 index 000000000..409c33d6a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/IPv4PostgreSQLInetJdbcTypeTest.java @@ -0,0 +1,115 @@ +package com.vladmihalcea.hpjp.hibernate.type; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLInetJdbcType; +import org.junit.Test; + +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class IPv4PostgreSQLInetJdbcTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + public void afterInit() { + doInJDBC(connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE INDEX ON event USING gist (ip inet_ops)"); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + + doInJPA(entityManager -> { + entityManager.persist( + new Event() + .setId(1L) + .setIp("192.168.0.123") + ); + }); + } + + @Test + public void testFindById() { + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals("192.168.0.123", event.getIp()); + + event.setIp("192.168.0.231"); + + return event; + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertEquals("192.168.0.231", event.getIp()); + }); + } + + @Test + public void testNativeQuery() { + doInJPA(entityManager -> { + List events = entityManager.createNativeQuery(""" + SELECT e.* + FROM event e + WHERE + e.ip && CAST(:network AS inet) = true + """, Event.class) + .setParameter("network", "192.168.0.1/24") + .getResultList(); + + assertEquals(1, events.size()); + assertEquals("192.168.0.123", events.get(0).getIp()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + private Long id; + + @JdbcType(PostgreSQLInetJdbcType.class) + @Column(name = "ip", columnDefinition = "inet") + private String ip; + + public Long getId() { + return id; + } + + public Event setId(Long id) { + this.id = id; + return this; + } + + public String getIp() { + return ip; + } + + public Event setIp(String ip) { + this.ip = ip; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/IPv4TypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/IPv4TypeTest.java new file mode 100644 index 000000000..7d2e8fcda --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/IPv4TypeTest.java @@ -0,0 +1,162 @@ +package com.vladmihalcea.hpjp.hibernate.type; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.basic.Inet; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class IPv4TypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + /*properties.put( + HibernateTypesContributor.TYPES_CONTRIBUTOR_FILTER, + (Predicate) userType -> !(userType instanceof PostgreSQLInetType) + );*/ + } + + private Event _event; + + @Override + public void afterInit() { + doInJDBC(connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE INDEX ON event USING gist (ip inet_ops)"); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + + _event = doInJPA(entityManager -> { + entityManager.persist(new Event()); + + Event event = new Event(); + event.setIp("192.168.0.123/24"); + entityManager.persist(event); + + return event; + }); + } + + @Test + public void testFindById() { + Event updatedEvent = doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + + assertEquals("192.168.0.123/24", event.getIp().getAddress()); + assertEquals("192.168.0.123", event.getIp().toInetAddress().getHostAddress()); + + event.setIp("192.168.0.231/24"); + + return event; + }); + + assertEquals("192.168.0.231/24", updatedEvent.getIp().getAddress()); + } + + @Test + public void testJPQLQuery() { + doInJPA(entityManager -> { + List events = entityManager.createQuery(""" + select e + from Event e + where + ip is not null + """, Event.class) + .getResultList(); + + Event event = events.get(0); + assertEquals("192.168.0.123/24", event.getIp().getAddress()); + }); + } + + @Test + public void testNativeQuery() { + doInJPA(entityManager -> { + List events = entityManager.createNativeQuery(""" + SELECT e.* + FROM event e + WHERE + e.ip && CAST(:network AS inet) = true + """, Event.class) + .setParameter("network", "192.168.0.1/24") + .getResultList(); + + assertTrue( + events + .stream() + .map(Event::getIp) + .anyMatch(ip -> ip.getAddress().equals("192.168.0.123/24")) + ); + }); + } + + + @Test + public void testJDBCQuery() { + doInJPA(entityManager -> { + Session session = entityManager.unwrap(Session.class); + session.doWork(connection -> { + try(PreparedStatement ps = connection.prepareStatement(""" + SELECT * + FROM Event e + WHERE + e.ip && ?::inet = true + """ + )) { + ps.setObject(1, "192.168.0.1/24"); + ResultSet rs = ps.executeQuery(); + while(rs.next()) { + Long id = rs.getLong(1); + String ip = rs.getString(2); + assertEquals("192.168.0.123/24", ip); + } + } + }); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + @GeneratedValue + private Long id; + + @Column(name = "ip", columnDefinition = "inet") + private Inet ip; + + public Long getId() { + return id; + } + + public Inet getIp() { + return ip; + } + + public void setIp(String address) { + this.ip = new Inet(address); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/LongToNumericTypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/LongToNumericTypeTest.java similarity index 81% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/LongToNumericTypeTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/LongToNumericTypeTest.java index 2b0677aa8..f1f4ae1bc 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/LongToNumericTypeTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/LongToNumericTypeTest.java @@ -1,24 +1,24 @@ -package com.vladmihalcea.book.hpjp.hibernate.type; +package com.vladmihalcea.hpjp.hibernate.type; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Properties; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.EntityManagerFactory; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor; import org.junit.Test; -import com.vladmihalcea.book.hpjp.util.AbstractSQLServerIntegrationTest; -import com.vladmihalcea.book.hpjp.util.PersistenceUnitInfoImpl; +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import com.vladmihalcea.hpjp.util.PersistenceUnitInfoImpl; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/MySQLTextStringTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/MySQLTextStringTest.java similarity index 78% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/MySQLTextStringTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/MySQLTextStringTest.java index d4c2a4d01..f54e784f9 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/MySQLTextStringTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/MySQLTextStringTest.java @@ -1,10 +1,11 @@ -package com.vladmihalcea.book.hpjp.hibernate.type; +package com.vladmihalcea.hpjp.hibernate.type; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; -import org.hibernate.annotations.Type; +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.annotations.JdbcType; +import org.hibernate.type.descriptor.jdbc.LongVarcharJdbcType; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Arrays; import java.util.concurrent.atomic.AtomicReference; @@ -42,7 +43,7 @@ public static class Event { private Long id; @Column(name="my_field", columnDefinition="text") - @Type(type = "text") + @JdbcType(LongVarcharJdbcType.class) private String message; } } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgreSQLRangeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgreSQLRangeTest.java new file mode 100644 index 000000000..2356a70ef --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgreSQLRangeTest.java @@ -0,0 +1,146 @@ +package com.vladmihalcea.hpjp.hibernate.type; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType; +import io.hypersistence.utils.hibernate.type.range.Range; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLRangeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPriceRange( + Range.closed( + BigDecimal.valueOf(39.95d), + BigDecimal.valueOf(45.95d) + ) + ); + book.setDiscountDateRange( + Range.closedOpen( + LocalDate.of(2019, 11, 29), + LocalDate.of(2019, 12, 3) + ) + ); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(BigDecimal.valueOf(39.95d), book.getPriceRange().lower()); + assertEquals(BigDecimal.valueOf(45.95d), book.getPriceRange().upper()); + + assertEquals(LocalDate.of(2019, 11, 29), book.getDiscountDateRange().lower()); + assertEquals(LocalDate.of(2019, 12, 3), book.getDiscountDateRange().upper()); + }); + + doInJPA(entityManager -> { + List discountedBooks = entityManager + .createNativeQuery( + "SELECT * " + + "FROM book b " + + "WHERE " + + " b.discount_date_range @> CAST(:today AS date) = true ", Book.class) + .setParameter("today", LocalDate.of(2019, 12, 1)) + .getResultList(); + + assertTrue( + discountedBooks.stream().anyMatch( + book -> book.getTitle().equals("High-Performance Java Persistence") + ) + ); + }); + } + + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Type(PostgreSQLRangeType.class) + @Column(name = "price_cent_range", columnDefinition = "numrange") + private Range priceRange; + + @Type(PostgreSQLRangeType.class) + @Column(name = "discount_date_range", columnDefinition = "daterange") + private Range discountDateRange; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Range getPriceRange() { + return priceRange; + } + + public void setPriceRange(Range priceRange) { + this.priceRange = priceRange; + } + + public Range getDiscountDateRange() { + return discountDateRange; + } + + public void setDiscountDateRange(Range discountDateRange) { + this.discountDateRange = discountDateRange; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgresSelectGeneratorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgresSelectGeneratorTest.java new file mode 100644 index 000000000..851c5edda --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgresSelectGeneratorTest.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.hibernate.type; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class PostgresSelectGeneratorTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + public void init() { + executeStatement("CREATE SEQUENCE event_sequence START 1"); + super.init(); + } + + @Override + public void destroy() { + super.destroy(); + executeStatement("DROP SEQUENCE event_sequence"); + } + + @Test + public void test() { + Long id = doInJPA(entityManager -> { + Event event = new Event(); + event.name = "Hypersistence"; + entityManager.persist(event); + + return event.id; + }); + + assertNotNull(id); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + @GeneratedValue(generator = "select") + @GenericGenerator(name = "select", strategy = "select") + @Column(columnDefinition = "BIGINT DEFAULT nextval('event_sequence')") + private Long id; + + @NaturalId + private String name; + + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgresUUIDIdTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgresUUIDIdTest.java new file mode 100644 index 000000000..4b519026a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgresUUIDIdTest.java @@ -0,0 +1,68 @@ +package com.vladmihalcea.hpjp.hibernate.type; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.UUID; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class PostgresUUIDIdTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + public void init() { + executeStatement("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\""); + super.init(); + } + + @Override + public void destroy() { + doInJPA(entityManager -> { + entityManager.createNativeQuery( + "DROP EXTENSION \"uuid-ossp\" CASCADE" + ).executeUpdate(); + }); + super.destroy(); + } + + @Test + public void test() { + Event _event = doInJPA(entityManager -> { + Event event = new Event(); + entityManager.persist(event); + return event; + }); + + assertNotNull(_event.getId()); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, _event.getId()); + + assertNotNull(event); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + public UUID getId() { + return id; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgresUUIDPropertyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgresUUIDPropertyTest.java new file mode 100644 index 000000000..061dc4292 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgresUUIDPropertyTest.java @@ -0,0 +1,87 @@ +package com.vladmihalcea.hpjp.hibernate.type; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.UUID; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class PostgresUUIDPropertyTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + public void init() { + executeStatement("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\""); + super.init(); + } + + @Override + public void destroy() { + doInJPA(entityManager -> { + entityManager.createNativeQuery( + "DROP EXTENSION \"uuid-ossp\" CASCADE" + ).executeUpdate(); + }); + super.destroy(); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Event event = new Event() + .setId(1L) + .setExternalId(UUID.randomUUID()); + + entityManager.persist(event); + return event; + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + assertNotNull(event.getExternalId()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + private Long id; + + @Column( + name = "external_id", + columnDefinition = "UUID NOT NULL" + ) + private UUID externalId; + + public Long getId() { + return id; + } + + public Event setId(Long id) { + this.id = id; + return this; + } + + public UUID getExternalId() { + return externalId; + } + + public Event setExternalId(UUID externalId) { + this.externalId = externalId; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgresUUIDTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgresUUIDTest.java new file mode 100644 index 000000000..1492ef70c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/PostgresUUIDTest.java @@ -0,0 +1,66 @@ +package com.vladmihalcea.hpjp.hibernate.type; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.annotations.Generated; +import org.hibernate.annotations.GenerationTime; +import org.junit.Test; + +import jakarta.persistence.*; + +import java.util.UUID; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +public class PostgresUUIDTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + public void init() { + executeStatement("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\""); + super.init(); + } + + @Override + public void destroy() { + doInJPA(entityManager -> { + entityManager.createNativeQuery( + "DROP EXTENSION \"uuid-ossp\" CASCADE" + ).executeUpdate(); + }); + super.destroy(); + } + + @Test + public void test() { + Event _event = doInJPA(entityManager -> { + Event event = new Event(); + entityManager.persist(event); + return event; + }); + + assertNotNull(_event.uuid); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + @GeneratedValue + private Long id; + + @Generated(GenerationTime.INSERT) + @Column(columnDefinition = "UUID NOT NULL DEFAULT uuid_generate_v4()", insertable = false) + private UUID uuid; + + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/TaskTypingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/TaskTypingTest.java new file mode 100644 index 000000000..70916b358 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/TaskTypingTest.java @@ -0,0 +1,27 @@ +package com.vladmihalcea.hpjp.hibernate.type; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.entity.TaskEntityProvider; +import org.junit.Test; + +/** + * EntityGraphMapperTest - Test mapping to entity + * + * @author Vlad Mihalcea + */ +public class TaskTypingTest extends AbstractPostgreSQLIntegrationTest { + + private TaskEntityProvider entityProvider = new TaskEntityProvider(); + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Test + public void testJdbcOneToManyMapping() { + doInJDBC(connection -> { + + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/array/EnumArrayTypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/array/EnumArrayTypeTest.java new file mode 100644 index 000000000..428b257bf --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/array/EnumArrayTypeTest.java @@ -0,0 +1,84 @@ +package com.vladmihalcea.hpjp.hibernate.type.array; + +import com.vladmihalcea.hpjp.hibernate.type.json.model.BaseEntity; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.array.EnumArrayType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class EnumArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Sudoku.class, + }; + } + + @Override + protected void beforeInit() { + executeStatement("DROP TYPE IF EXISTS sudoku_state"); + executeStatement("CREATE TYPE sudoku_state AS ENUM ('POSSIBLE', 'IMPOSSIBLE', 'UNDEFINED', 'UNKNOWN')"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Sudoku sudoku = new Sudoku(); + sudoku.setId(1L); + entityManager.persist(sudoku); + + sudoku.setStateValues(new SudokuPossibleValueState[] { + SudokuPossibleValueState.POSSIBLE, + SudokuPossibleValueState.IMPOSSIBLE, + SudokuPossibleValueState.POSSIBLE, + SudokuPossibleValueState.POSSIBLE, + SudokuPossibleValueState.POSSIBLE, + SudokuPossibleValueState.UNDEFINED, + SudokuPossibleValueState.POSSIBLE, + SudokuPossibleValueState.POSSIBLE, + SudokuPossibleValueState.UNKNOWN, + }); + }); + doInJPA(entityManager -> { + Sudoku sudoku = entityManager.find(Sudoku.class, 1L); + + assertEquals( 9L, sudoku.getStateValues().length ); + }); + } + + @Entity(name = "Sudoku") + @Table(name = "sudoku") + public static class Sudoku extends BaseEntity { + + @Type( + value = EnumArrayType.class, + parameters = @org.hibernate.annotations.Parameter( + name = "sql_array_type", + value = "sudoku_state" + ) + ) + @Column(name = "sensor_values", columnDefinition = "sudoku_state[]") + private SudokuPossibleValueState[] stateValues; + + public SudokuPossibleValueState[] getStateValues() { + return stateValues; + } + + public void setStateValues(SudokuPossibleValueState[] stateValues) { + this.stateValues = stateValues; + } + } + + public enum SudokuPossibleValueState { + UNDEFINED, UNKNOWN, IMPOSSIBLE, POSSIBLE, COMMITTED, AS_PUBLISHED; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/array/PostgreSQLArrayTypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/array/PostgreSQLArrayTypeTest.java new file mode 100644 index 000000000..b5dc94038 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/array/PostgreSQLArrayTypeTest.java @@ -0,0 +1,77 @@ +package com.vladmihalcea.hpjp.hibernate.type.array; + +import com.vladmihalcea.hpjp.hibernate.type.json.model.BaseEntity; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.array.IntArrayType; +import io.hypersistence.utils.hibernate.type.array.StringArrayType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLArrayTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class, + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Event event = new Event(); + event.setId(1L); + event.setSensorNames(new String[] {"Temperature", "Pressure"}); + event.setSensorValues( new int[] {12, 756} ); + entityManager.persist(event); + }); + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + + assertArrayEquals( new String[] {"Temperature", "Pressure"}, event.getSensorNames() ); + assertArrayEquals( new int[] {12, 756}, event.getSensorValues() ); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event extends BaseEntity { + + @Type(StringArrayType.class) + @Column(name = "sensor_names", columnDefinition = "text[]") + private String[] sensorNames; + + @Type(IntArrayType.class) + @Column(name = "sensor_values", columnDefinition = "integer[]") + private int[] sensorValues; + + public String[] getSensorNames() { + return sensorNames; + } + + public void setSensorNames(String[] sensorNames) { + this.sensorNames = sensorNames; + } + + public int[] getSensorValues() { + return sensorValues; + } + + public void setSensorValues(int[] sensorValues) { + this.sensorValues = sensorValues; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/array/PostgreSQLArrayUnnestTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/array/PostgreSQLArrayUnnestTest.java new file mode 100644 index 000000000..03baafa47 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/array/PostgreSQLArrayUnnestTest.java @@ -0,0 +1,82 @@ +package com.vladmihalcea.hpjp.hibernate.type.array; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.array.IntArrayType; +import jakarta.persistence.*; +import org.hibernate.annotations.Type; +import org.junit.Test; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLArrayUnnestTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Event.class, + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Event() + .setT1(new int[]{1, 3, 6}) + .setT2(new int[]{8, 9}) + ); + + entityManager.persist( + new Event() + .setT1(new int[]{1, 2}) + .setT2(new int[]{8}) + ); + + entityManager.persist( + new Event() + .setT1(new int[]{6}) + .setT2(new int[]{8, 1}) + ); + }); + //https://stackoverflow.com/questions/23863255/aggregation-to-calculate-number-of-each-tag-where-there-are-two-types-of-tags/23863831#23863831 + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + @GeneratedValue + private Long id; + + @Type(IntArrayType.class) + @Column(columnDefinition = "integer[]") + private int[] t1; + + @Type(IntArrayType.class) + @Column(columnDefinition = "integer[]") + private int[] t2; + + public int[] getT1() { + return t1; + } + + public Event setT1(int[] t1) { + this.t1 = t1; + return this; + } + + public int[] getT2() { + return t2; + } + + public Event setT2(int[] t2) { + this.t2 = t2; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/AttributeConverterMonthDayAutoRegisterTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/AttributeConverterMonthDayAutoRegisterTest.java new file mode 100644 index 000000000..03bf6fa67 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/AttributeConverterMonthDayAutoRegisterTest.java @@ -0,0 +1,130 @@ +package com.vladmihalcea.hpjp.hibernate.type.attributeconverter; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.hibernate.boot.MetadataBuilder; +import org.hibernate.boot.spi.MetadataBuilderContributor; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.LocalDate; +import java.time.Month; +import java.time.MonthDay; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class AttributeConverterMonthDayAutoRegisterTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + AnnualSubscription.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + "hibernate.metadata_builder_contributor", + AttributeConverterMetadataBuilderContributor.class.getName() + ); + } + + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new AnnualSubscription() + .setId(1L) + .setPriceInCents(700) + .setPaymentDay( + MonthDay.of(Month.AUGUST, 17) + ) + ); + }); + + doInJPA(entityManager -> { + AnnualSubscription subscription = entityManager.find(AnnualSubscription.class, 1L); + + assertEquals(MonthDay.of(Month.AUGUST, 17), subscription.getPaymentDay()); + }); + } + + @Entity(name = "AnnualSubscription") + @Table(name = "annual_subscription") + public static class AnnualSubscription { + + @Id + private Long id; + + @Column(name = "price_in_cents") + private int priceInCents; + + @Column(name = "payment_day", columnDefinition = "date") + private MonthDay paymentDay; + + public Long getId() { + return id; + } + + public AnnualSubscription setId(Long id) { + this.id = id; + return this; + } + + public int getPriceInCents() { + return priceInCents; + } + + public AnnualSubscription setPriceInCents(int priceInCents) { + this.priceInCents = priceInCents; + return this; + } + + public MonthDay getPaymentDay() { + return paymentDay; + } + + public AnnualSubscription setPaymentDay(MonthDay paymentDay) { + this.paymentDay = paymentDay; + return this; + } + } + + @Converter(autoApply = true) + public static class MonthDayDateAttributeConverter + implements AttributeConverter { + + @Override + public java.sql.Date convertToDatabaseColumn(MonthDay monthDay) { + if (monthDay != null) { + return java.sql.Date.valueOf( + monthDay.atYear(1) + ); + } + return null; + } + + @Override + public MonthDay convertToEntityAttribute(java.sql.Date date) { + if (date != null) { + LocalDate localDate = date.toLocalDate(); + return MonthDay.of(localDate.getMonth(), localDate.getDayOfMonth()); + } + return null; + } + } + + public static class AttributeConverterMetadataBuilderContributor + implements MetadataBuilderContributor { + + @Override + public void contribute(MetadataBuilder metadataBuilder) { + metadataBuilder.applyAttributeConverter(MonthDayDateAttributeConverter.class); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/AttributeConverterMonthDayTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/AttributeConverterMonthDayTest.java new file mode 100644 index 000000000..95e8d616a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/AttributeConverterMonthDayTest.java @@ -0,0 +1,134 @@ +package com.vladmihalcea.hpjp.hibernate.type.attributeconverter; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.LocalDate; +import java.time.Month; +import java.time.MonthDay; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class AttributeConverterMonthDayTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + AnnualSubscription.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new AnnualSubscription() + .setId(1L) + .setPriceInCents(700) + .setPaymentDay( + MonthDay.of(Month.AUGUST, 17) + ) + ); + }); + + doInJPA(entityManager -> { + AnnualSubscription subscription = entityManager.find(AnnualSubscription.class, 1L); + + assertEquals(MonthDay.of(Month.AUGUST, 17), subscription.getPaymentDay()); + }); + } + + @Test + public void testNull() { + doInJPA(entityManager -> { + entityManager.persist( + new AnnualSubscription() + .setId(1L) + .setPriceInCents(700) + .setPaymentDay(null) + ); + }); + + doInJPA(entityManager -> { + AnnualSubscription subscription = entityManager.find(AnnualSubscription.class, 1L); + + assertNull(subscription.getPaymentDay()); + }); + } + + @Entity(name = "AnnualSubscription") + @Table(name = "annual_subscription") + public static class AnnualSubscription { + + @Id + private Long id; + + @Column(name = "price_in_cents") + private int priceInCents; + + @Column(name = "payment_day", columnDefinition = "date") + @Convert(converter = MonthDayDateAttributeConverter.class) + private MonthDay paymentDay; + + public Long getId() { + return id; + } + + public AnnualSubscription setId(Long id) { + this.id = id; + return this; + } + + public int getPriceInCents() { + return priceInCents; + } + + public AnnualSubscription setPriceInCents(int priceInCents) { + this.priceInCents = priceInCents; + return this; + } + + public MonthDay getPaymentDay() { + return paymentDay; + } + + public AnnualSubscription setPaymentDay(MonthDay paymentDay) { + this.paymentDay = paymentDay; + return this; + } + } + + public static class MonthDayDateAttributeConverter + implements AttributeConverter { + + @Override + public java.sql.Date convertToDatabaseColumn(MonthDay monthDay) { + if (monthDay != null) { + return java.sql.Date.valueOf( + monthDay.atYear(1) + ); + } + return null; + } + + @Override + public MonthDay convertToEntityAttribute(java.sql.Date date) { + if (date != null) { + LocalDate localDate = date.toLocalDate(); + return MonthDay.of(localDate.getMonth(), localDate.getDayOfMonth()); + } + return null; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/MySQLYearAndMonthIntegerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/MySQLYearAndMonthIntegerTest.java new file mode 100644 index 000000000..c91adcb35 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/MySQLYearAndMonthIntegerTest.java @@ -0,0 +1,144 @@ +package com.vladmihalcea.hpjp.hibernate.type.attributeconverter; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.basic.Iso8601MonthType; +import io.hypersistence.utils.hibernate.type.basic.YearType; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.Month; +import java.time.Year; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLYearAndMonthIntegerTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void beforeInit() { + executeStatement("drop table if exists book"); + executeStatement("create table book (publishing_month tinyint, publishing_year int, id bigint not null auto_increment, isbn varchar(255), title varchar(255), primary key (id)) engine=InnoDB"); + executeStatement("alter table book add constraint UK_book_isbn unique (isbn)"); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.hbm2ddl.auto", "validate"); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishingYear(Year.of(2016)); + book.setPublishingMonth(Month.of(10)); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(Year.of(2016), book.getPublishingYear()); + assertEquals(Month.of(10), book.getPublishingMonth()); + }); + + doInJPA(entityManager -> { + Book book = entityManager.createQuery(""" + select b + from Book b + where + b.title = :title and + b.publishingYear = :publishingYear and + b.publishingMonth = :publishingMonth + """, Book.class) + .setParameter("title", "High-Performance Java Persistence") + .setParameter("publishingYear", Year.of(2016)) + .setParameter("publishingMonth", Month.of(10)) + .getSingleResult(); + + assertEquals("978-9730228236", book.getIsbn()); + }); + } + + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Column(name = "publishing_year") + @Type(YearType.class) + private Year publishingYear; + + @Column(name = "publishing_month", columnDefinition = "tinyint") + @Type(Iso8601MonthType.class) + private Month publishingMonth; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Year getPublishingYear() { + return publishingYear; + } + + public void setPublishingYear(Year publishingYear) { + this.publishingYear = publishingYear; + } + + public Month getPublishingMonth() { + return publishingMonth; + } + + public void setPublishingMonth(Month publishingMonth) { + this.publishingMonth = publishingMonth; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/MySQLYearMonthIntegerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/MySQLYearMonthIntegerTest.java new file mode 100644 index 000000000..f281bbf72 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/MySQLYearMonthIntegerTest.java @@ -0,0 +1,133 @@ +package com.vladmihalcea.hpjp.hibernate.type.attributeconverter; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.YearMonth; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLYearMonthIntegerTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishedOn(YearMonth.of(2016, 10)); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(YearMonth.of(2016, 10), book.getPublishedOn()); + }); + + doInJPA(entityManager -> { + Book book = entityManager.createQuery(""" + select b + from Book b + where + b.title = :title and + b.publishedOn = :publishedOn + """, Book.class) + .setParameter("title", "High-Performance Java Persistence") + .setParameter("publishedOn", YearMonth.of(2016, 10)) + .getSingleResult(); + + assertEquals("978-9730228236", book.getIsbn()); + }); + } + + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Column(name = "published_on", columnDefinition = "mediumint") + @Convert(converter = YearMonthIntegerAttributeConverter.class) + private YearMonth publishedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public YearMonth getPublishedOn() { + return publishedOn; + } + + public void setPublishedOn(YearMonth publishedOn) { + this.publishedOn = publishedOn; + } + } + + public static class YearMonthIntegerAttributeConverter + implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(YearMonth attribute) { + if (attribute != null) { + return (attribute.getYear() * 100) + attribute.getMonth().getValue(); + } + return null; + } + + @Override + public YearMonth convertToEntityAttribute(Integer dbData) { + if (dbData != null) { + int year = dbData / 100; + int month = dbData % 100; + return YearMonth.of(year, month); + } + return null; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/PostgreSQLYearMonthDateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/PostgreSQLYearMonthDateTest.java new file mode 100644 index 000000000..dd8e7157c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/PostgreSQLYearMonthDateTest.java @@ -0,0 +1,135 @@ +package com.vladmihalcea.hpjp.hibernate.type.attributeconverter; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.Instant; +import java.time.YearMonth; +import java.time.ZoneId; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLYearMonthDateTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishedOn(YearMonth.of(2016, 10)); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(YearMonth.of(2016, 10), book.getPublishedOn()); + }); + + doInJPA(entityManager -> { + Book book = entityManager.createQuery(""" + select b + from Book b + where + b.title = :title and + b.publishedOn = :publishedOn + """, Book.class) + .setParameter("title", "High-Performance Java Persistence") + .setParameter("publishedOn", YearMonth.of(2016, 10)) + .getSingleResult(); + + assertEquals("978-9730228236", book.getIsbn()); + }); + } + + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Column(name = "published_on", columnDefinition = "date") + @Convert(converter = YearMonthDateAttributeConverter.class) + private YearMonth publishedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public YearMonth getPublishedOn() { + return publishedOn; + } + + public void setPublishedOn(YearMonth publishedOn) { + this.publishedOn = publishedOn; + } + } + + public static class YearMonthDateAttributeConverter + implements AttributeConverter { + + @Override + public java.sql.Date convertToDatabaseColumn(YearMonth attribute) { + if (attribute != null) { + return java.sql.Date.valueOf(attribute.atDay(1)); + } + return null; + } + + @Override + public YearMonth convertToEntityAttribute(java.sql.Date dbData) { + if (dbData != null) { + return YearMonth.from(Instant.ofEpochMilli(dbData.getTime()) + .atZone(ZoneId.systemDefault()) + .toLocalDate()); + } + return null; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/PostgreSQLYearMonthIntegerConverterAutoApplyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/PostgreSQLYearMonthIntegerConverterAutoApplyTest.java new file mode 100644 index 000000000..4b5a39977 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/PostgreSQLYearMonthIntegerConverterAutoApplyTest.java @@ -0,0 +1,143 @@ +package com.vladmihalcea.hpjp.hibernate.type.attributeconverter; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.boot.spi.MetadataBuilderContributor; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.YearMonth; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLYearMonthIntegerConverterAutoApplyTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.metadata_builder_contributor", + (MetadataBuilderContributor) metadataBuilder -> + metadataBuilder.applyAttributeConverter(YearMonthIntegerAttributeConverter.class) + ); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishedOn(YearMonth.of(2016, 10)); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(YearMonth.of(2016, 10), book.getPublishedOn()); + }); + + doInJPA(entityManager -> { + Book book = entityManager.createQuery(""" + select b + from Book b + where + b.title = :title and + b.publishedOn = :publishedOn + """, Book.class) + .setParameter("title", "High-Performance Java Persistence") + .setParameter("publishedOn", YearMonth.of(2016, 10)) + .getSingleResult(); + + assertEquals("978-9730228236", book.getIsbn()); + }); + } + + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Column(name = "published_on", columnDefinition = "integer") + private YearMonth publishedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public YearMonth getPublishedOn() { + return publishedOn; + } + + public void setPublishedOn(YearMonth publishedOn) { + this.publishedOn = publishedOn; + } + } + + @Converter(autoApply = true) + public static class YearMonthIntegerAttributeConverter + implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(YearMonth attribute) { + if (attribute != null) { + return (attribute.getYear() * 100) + attribute.getMonth().getValue(); + } + return null; + } + + @Override + public YearMonth convertToEntityAttribute(Integer dbData) { + if (dbData != null) { + int year = dbData / 100; + int month = dbData % 100; + return YearMonth.of(year, month); + } + return null; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/PostgreSQLYearMonthIntegerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/PostgreSQLYearMonthIntegerTest.java new file mode 100644 index 000000000..1227fe8e1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/PostgreSQLYearMonthIntegerTest.java @@ -0,0 +1,133 @@ +package com.vladmihalcea.hpjp.hibernate.type.attributeconverter; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.YearMonth; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLYearMonthIntegerTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setTitle("High-Performance Java Persistence"); + book.setPublishedOn(YearMonth.of(2016, 10)); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals(YearMonth.of(2016, 10), book.getPublishedOn()); + }); + + doInJPA(entityManager -> { + Book book = entityManager.createQuery(""" + select b + from Book b + where + b.title = :title and + b.publishedOn = :publishedOn + """, Book.class) + .setParameter("title", "High-Performance Java Persistence") + .setParameter("publishedOn", YearMonth.of(2016, 10)) + .getSingleResult(); + + assertEquals("978-9730228236", book.getIsbn()); + }); + } + + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Column(name = "published_on", columnDefinition = "integer") + @Convert(converter = YearMonthIntegerAttributeConverter.class) + private YearMonth publishedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public YearMonth getPublishedOn() { + return publishedOn; + } + + public void setPublishedOn(YearMonth publishedOn) { + this.publishedOn = publishedOn; + } + } + + public static class YearMonthIntegerAttributeConverter + implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(YearMonth attribute) { + if (attribute != null) { + return (attribute.getYear() * 100) + attribute.getMonth().getValue(); + } + return null; + } + + @Override + public YearMonth convertToEntityAttribute(Integer dbData) { + if (dbData != null) { + int year = dbData / 100; + int month = dbData % 100; + return YearMonth.of(year, month); + } + return null; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/YearAndMonthTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/YearAndMonthTest.java new file mode 100644 index 000000000..ef52af4fd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/attributeconverter/YearAndMonthTest.java @@ -0,0 +1,135 @@ +package com.vladmihalcea.hpjp.hibernate.type.attributeconverter; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import jakarta.persistence.*; +import java.time.Month; +import java.time.Year; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class YearAndMonthTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Publisher.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Publisher publisher = new Publisher(); + publisher.setName("vladmihalcea.com"); + publisher.setEstYear(Year.of(2013)); + publisher.setSalesMonth(Month.NOVEMBER); + + entityManager.persist(publisher); + }); + + doInJPA(entityManager -> { + Publisher publisher = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Publisher.class) + .load("vladmihalcea.com"); + + assertEquals(Year.of(2013), publisher.getEstYear()); + assertEquals(Month.NOVEMBER, publisher.getSalesMonth()); + }); + + doInJPA(entityManager -> { + Publisher book = entityManager.createQuery(""" + select p + from Publisher p + where + p.estYear = :estYear and + p.salesMonth = :salesMonth + """, Publisher.class) + .setParameter("estYear", Year.of(2013)) + .setParameter("salesMonth", Month.NOVEMBER) + .getSingleResult(); + + assertEquals("vladmihalcea.com", book.getName()); + }); + } + + @Entity(name = "Publisher") + @Table(name = "publisher") + public static class Publisher { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @Column(name = "est_year", columnDefinition = "smallint") + @Convert(converter = YearAttributeConverter.class) + private Year estYear; + + @Column(name = "sales_month", columnDefinition = "smallint") + @Enumerated + private Month salesMonth; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Year getEstYear() { + return estYear; + } + + public void setEstYear(Year estYear) { + this.estYear = estYear; + } + + public Month getSalesMonth() { + return salesMonth; + } + + public void setSalesMonth(Month salesMonth) { + this.salesMonth = salesMonth; + } + } + + @Converter(autoApply = true) + public static class YearAttributeConverter + implements AttributeConverter { + + @Override + public Short convertToDatabaseColumn(Year attribute) { + if (attribute != null) { + return (short) attribute.getValue(); + } + return null; + } + + @Override + public Year convertToEntityAttribute(Short dbData) { + if (dbData != null) { + return Year.of(dbData); + } + return null; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/binary/MySQLBinaryTypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/binary/MySQLBinaryTypeTest.java new file mode 100644 index 000000000..0c834781f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/binary/MySQLBinaryTypeTest.java @@ -0,0 +1,104 @@ +package com.vladmihalcea.hpjp.hibernate.type.binary; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import static org.junit.Assert.assertArrayEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLBinaryTypeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + User.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Test + public void test() { + String password = "my_secret_password"; + + doInJPA(entityManager -> { + entityManager.persist( + new User() + .setUserName("vladmihalcea") + .setPassword(hash(password)) + ); + }); + + doInJPA(entityManager -> { + User user = entityManager.unwrap(Session.class) + .bySimpleNaturalId(User.class) + .load("vladmihalcea"); + + assertArrayEquals(hash(password), user.getPassword()); + }); + } + + public byte[] hash(String password) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(password.getBytes()); + return md.digest(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + + @Entity(name = "User") + @Table(name = "`user`") + public static class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NaturalId + private String userName; + + @Column(columnDefinition = "BINARY(16)") + private byte[] password; + + public Long getId() { + return id; + } + + public User setId(Long id) { + this.id = id; + return this; + } + + public String getUserName() { + return userName; + } + + public User setUserName(String userName) { + this.userName = userName; + return this; + } + + public byte[] getPassword() { + return password; + } + + public User setPassword(byte[] password) { + this.password = password; + return this; + } + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/DateTimeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/DateTimeTest.java new file mode 100644 index 000000000..bcc2208df --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/DateTimeTest.java @@ -0,0 +1,266 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Ignore; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.*; +import java.util.Date; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DateTimeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + LocalDateEvent.class, + ZonedDateTimeEvent.class, + OffsetDateTimeEvent.class, + TimestampEvent.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.JDBC_TIME_ZONE, "UTC"); + } + + @Test + public void testLocalDateEvent() { + doInJPA(entityManager -> { + LocalDateEvent event = new LocalDateEvent(); + event.id = 1L; + event.createdOn = LocalDate.of(1, 1, 1); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + LocalDateEvent event = entityManager.find(LocalDateEvent.class, 1L); + try { + assertEquals(LocalDate.of(1, 1, 1), event.createdOn); + } catch (Throwable e) { + LOGGER.error("Failed", e); + } + }); + } + + @Test + public void testOffsetDateTimeEvent() { + doInJPA(entityManager -> { + OffsetDateTimeEvent event = new OffsetDateTimeEvent(); + event.id = 1L; + event.createdOn = OffsetDateTime.of(2024, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + OffsetDateTimeEvent event = entityManager.find(OffsetDateTimeEvent.class, 1L); + try { + assertEquals(OffsetDateTime.of(2024, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC), event.createdOn); + } catch (Throwable e) { + LOGGER.error("Failed", e); + } + }); + } + + @Test + public void testZonedDateTimeEvent() { + doInJPA(entityManager -> { + ZonedDateTimeEvent event = new ZonedDateTimeEvent(); + event.id = 1L; + event.createdOn = ZonedDateTime.of( + 2024, 6, 24, 15, 45, 23, 0, ZoneOffset.systemDefault()); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + ZonedDateTimeEvent event = entityManager.find(ZonedDateTimeEvent.class, 1L); + try { + assertEquals(ZonedDateTime.of( + 2024, 6, 24, 15, 45, 23, 0, ZoneOffset.systemDefault()), event.createdOn); + } catch (Throwable e) { + LOGGER.error("Failed", e); + } + }); + } + + @Test + public void testTruncEvent() { + doInJPA(entityManager -> { + TimestampEvent event = new TimestampEvent(); + event.id = 1L; + event.createdOn = new Date(); + entityManager.persist(event); + }); + + doInJPA(entityManager -> { + List events = entityManager.createQuery(""" + select e + from TimestampEvent e + where cast(e.createdOn as date) >= :createdOn + order by e.createdOn asc + """) + .setParameter( + "createdOn", + Timestamp.from( + LocalDateTime.of( + LocalDate.now(), + LocalTime.MIDNIGHT + ).toInstant(ZoneOffset.UTC) + ), TemporalType.DATE + ) + .getResultList(); + assertEquals(1, events.size()); + }); + doInJPA(entityManager -> { + LocalDateTime dt = LocalDateTime.now(); + ZonedDateTime zdt = dt.atZone(ZoneOffset.systemDefault()); + ZoneOffset offset = zdt.getOffset(); + + List events = entityManager.createQuery(""" + select e + from TimestampEvent e + where function('date_trunc', 'hour', e.createdOn) >= :createdOn + order by e.createdOn asc + """) + .setParameter( + "createdOn", + Timestamp.from( + LocalDateTime.of(LocalDate.now(), LocalTime.MIDNIGHT) + .minusSeconds(offset.getTotalSeconds()) + .toInstant(ZoneOffset.UTC) + ), + TemporalType.DATE + ) + .getResultList(); + assertEquals(1, events.size()); + }); + } + + @Test + public void testLocalDateTime() { + LocalDateTime timestamp = LocalDateTime.of(2031, 12, 10, 7, 30, 45); + LOGGER.info("LocalDateTime: {}", timestamp); + } + + @Test + public void testOffsetDateTime() { + OffsetDateTime timestamp = OffsetDateTime.of( + 2031, 12, 10, 7, 30, 45, 0, + ZoneOffset.of("+09:00") + ); + LOGGER.info("OffsetDateTime: {}", timestamp); + + LocalDateTime localDateTime = LocalDateTime.of(2031, 12, 10, 7, 30, 45); + OffsetDateTime offsetDateTime = OffsetDateTime.of( + localDateTime, + ZoneOffset.of("+09:00") + ); + LOGGER.info("OffsetDateTime: {}", offsetDateTime); + assertEquals(timestamp, offsetDateTime); + + ZonedDateTime zonedDateTime = ZonedDateTime.of( + 2031, 12, 10, 7, 30, 45, 0, + ZoneId.of("Asia/Tokyo") + ); + assertEquals(timestamp.toInstant(), zonedDateTime.toInstant()); + LOGGER.info("ZonedDateTime: {}", zonedDateTime); + LOGGER.info("Instant millis: {}", zonedDateTime.toInstant().toEpochMilli()); + } + + @Test + public void testOffsetDateTimeFromLocalDateTime() { + OffsetDateTime timestamp = OffsetDateTime.of( + 2031, 12, 10, 7, 30, 45, 0, + ZoneOffset.of("+09:00") + ); + LOGGER.info("OffsetDateTime: {}", timestamp); + } + + @Test + public void testZonedDateTime() { + ZonedDateTime timestamp = ZonedDateTime.of( + 2031, 12, 10, 7, 30, 45, 0, + ZoneId.of("Europe/London") + ); + LOGGER.info("ZonedDateTime: {}", timestamp); + } + + @Test + public void testZonedDateTimeWithDST() { + ZonedDateTime timestamp = ZonedDateTime.of( + 2032, 6, 10, 7, 30, 45, 0, + ZoneId.of("Europe/London") + ); + LOGGER.info("ZonedDateTime: {}", timestamp); + } + + @Test + public void testInstant() { + Instant timestamp = ZonedDateTime.of( + 2032, 6, 10, 7, 30, 45, 0, + ZoneId.of("Europe/London") + ).toInstant(); + LOGGER.info("Instant: {}", timestamp); + } + + @Entity(name = "LocalDateEvent") + public static class LocalDateEvent { + + @Id + private Long id; + + @NotNull + @Column(name = "created_on", nullable = false) + private LocalDate createdOn; + } + + @Entity(name = "OffsetDateTimeEvent") + public static class OffsetDateTimeEvent { + + @Id + private Long id; + + @NotNull + @Column(name = "created_on", nullable = false) + private OffsetDateTime createdOn; + } + + @Entity(name = "ZonedDateTimeEvent") + public static class ZonedDateTimeEvent { + + @Id + private Long id; + + @NotNull + @Column(name = "created_on", nullable = false) + private ZonedDateTime createdOn; + } + + @Entity(name = "TimestampEvent") + public static class TimestampEvent { + + @Id + private Long id; + + @Column(name = "created_on") + @Temporal(TemporalType.TIMESTAMP) + private Date createdOn; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/InstantTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/InstantTest.java new file mode 100644 index 000000000..bf240bf36 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/InstantTest.java @@ -0,0 +1,201 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.interval.PostgreSQLIntervalType; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.*; +import java.time.temporal.ChronoUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +/** + * @author Vlad Mihalcea + */ +public class InstantTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Employee.class, + Meeting.class + }; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Employee employee = new Employee(); + employee.setName("Vlad Mihalcea"); + employee.setBirthday( + LocalDate.of( + 1981, 12, 10 + ).atStartOfDay(ZoneOffset.UTC).toInstant() + ); + employee.setUpdatedOn( + LocalDateTime.of( + 2015, 12, 1, + 8, 0, 0 + ).toInstant(ZoneOffset.UTC) + ); + + entityManager.persist(employee); + + Meeting meeting = new Meeting(); + meeting.setId(1L); + meeting.setCreatedBy(employee); + meeting.setStartsAt( + ZonedDateTime.of( + 2017, 6, 25, + 11, 30, 0, 0, + ZoneOffset.UTC + ).toInstant() + ); + meeting.setDuration( + Duration.of(45, ChronoUnit.MINUTES) + ); + + entityManager.persist(meeting); + }); + + doInJPA(entityManager -> { + Employee employee = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Employee.class) + .load("Vlad Mihalcea"); + assertEquals( + LocalDate.of( + 1981, 12, 10 + ).atStartOfDay().toInstant(ZoneOffset.UTC), + employee.getBirthday() + ); + assertEquals( + LocalDateTime.of( + 2015, 12, 1, + 8, 0, 0 + ).toInstant(ZoneOffset.UTC), + employee.getUpdatedOn() + ); + + Meeting meeting = entityManager.find(Meeting.class, 1L); + assertSame( + employee, meeting.getCreatedBy() + ); + assertEquals( + ZonedDateTime.of( + 2017, 6, 25, + 11, 30, 0, 0, + ZoneOffset.UTC + ).toInstant(), + meeting.getStartsAt() + ); + assertEquals( + Duration.of(45, ChronoUnit.MINUTES), + meeting.getDuration() + ); + }); + } + + @Entity(name = "Employee") + public static class Employee { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + private Instant birthday; + + @Column(name = "updated_on") + private Instant updatedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Instant getBirthday() { + return birthday; + } + + public void setBirthday(Instant birthday) { + this.birthday = birthday; + } + + public Instant getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(Instant updatedOn) { + this.updatedOn = updatedOn; + } + } + + @Entity(name = "Meeting") + public static class Meeting { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "employee_id") + private Employee createdBy; + + @Column(name = "starts_at") + private Instant startsAt; + + @Type(PostgreSQLIntervalType.class) + @Column(columnDefinition = "interval") + private Duration duration; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Employee getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(Employee createdBy) { + this.createdBy = createdBy; + } + + public Instant getStartsAt() { + return startsAt; + } + + public void setStartsAt(Instant startsAt) { + this.startsAt = startsAt; + } + + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/LocalDateTimeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/LocalDateTimeTest.java new file mode 100644 index 000000000..099e8931b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/LocalDateTimeTest.java @@ -0,0 +1,201 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.interval.PostgreSQLIntervalType; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.time.*; +import java.time.temporal.ChronoUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +/** + * @author Vlad Mihalcea + */ +public class LocalDateTimeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Employee.class, + Meeting.class + }; + } + + @Test + public void testLocalDateEvent() { + doInJPA(entityManager -> { + Employee employee = new Employee(); + employee.setName("Vlad Mihalcea"); + employee.setBirthday( + LocalDate.of( + 1981, 12, 10 + ) + ); + employee.setUpdatedOn( + LocalDateTime.of( + 2015, 12, 1, + 8, 0, 0 + ) + ); + + entityManager.persist(employee); + + Meeting meeting = new Meeting(); + meeting.setId(1L); + meeting.setCreatedBy(employee); + meeting.setStartsAt( + ZonedDateTime.of( + 2017, 6, 25, + 11, 30, 0, 0, + ZoneId.systemDefault() + ) + ); + meeting.setDuration( + Duration.of(45, ChronoUnit.MINUTES) + ); + + entityManager.persist(meeting); + }); + + doInJPA(entityManager -> { + Employee employee = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Employee.class) + .load("Vlad Mihalcea"); + assertEquals( + LocalDate.of( + 1981, 12, 10 + ), + employee.getBirthday() + ); + assertEquals( + LocalDateTime.of( + 2015, 12, 1, + 8, 0, 0 + ), + employee.getUpdatedOn() + ); + + Meeting meeting = entityManager.find(Meeting.class, 1L); + assertSame( + employee, meeting.getCreatedBy() + ); + assertEquals( + ZonedDateTime.of( + 2017, 6, 25, + 11, 30, 0, 0, + ZoneId.systemDefault() + ).toInstant(), + meeting.getStartsAt().toInstant() + ); + assertEquals( + Duration.of(45, ChronoUnit.MINUTES), + meeting.getDuration() + ); + }); + } + + @Entity(name = "Employee") + public static class Employee { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + private LocalDate birthday; + + @Column(name = "updated_on") + private LocalDateTime updatedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public LocalDate getBirthday() { + return birthday; + } + + public void setBirthday(LocalDate birthday) { + this.birthday = birthday; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + } + } + + @Entity(name = "Meeting") + public static class Meeting { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "employee_id") + private Employee createdBy; + + @Column(name = "starts_at") + private ZonedDateTime startsAt; + + @Type(PostgreSQLIntervalType.class) + @Column(columnDefinition = "interval") + private Duration duration; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Employee getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(Employee createdBy) { + this.createdBy = createdBy; + } + + public ZonedDateTime getStartsAt() { + return startsAt; + } + + public void setStartsAt(ZonedDateTime startsAt) { + this.startsAt = startsAt; + } + + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/MySQLDateTimeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/MySQLDateTimeTest.java new file mode 100644 index 000000000..16a35804a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/MySQLDateTimeTest.java @@ -0,0 +1,62 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.junit.Test; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +public class MySQLDateTimeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + DateTimeEvent.class, + TimestampEvent.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Test + public void testLocalDateEvent() { + doInJPA(entityManager -> { + DateTimeEvent dateTimeEvent = new DateTimeEvent(); + dateTimeEvent.id = LocalDateTime.now(); + entityManager.persist(dateTimeEvent); + + TimestampEvent timestampEvent = new TimestampEvent(); + timestampEvent.id = LocalDateTime.now(); + entityManager.persist(timestampEvent); + }); + LOGGER.info("Data created"); + } + + @Entity(name = "DateTimeEvent") + @Table(name = "date_time_event") + public static class DateTimeEvent { + + @Id + @Column(columnDefinition = "DATETIME") + private LocalDateTime id; + } + + @Entity(name = "TimestampEvent") + @Table(name = "timestamp_event") + public static class TimestampEvent { + + @Id + @Column(columnDefinition = "DATETIME") + private LocalDateTime id; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/MySQLJavaDateTimeJDBCBindingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/MySQLJavaDateTimeJDBCBindingTest.java new file mode 100644 index 000000000..94c8704ec --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/MySQLJavaDateTimeJDBCBindingTest.java @@ -0,0 +1,93 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLJavaDateTimeJDBCBindingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + DateTimeEvent.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.JAVA_TIME_USE_DIRECT_JDBC, "true"); + } + + @Test + public void test() { + DateTimeEvent _event = new DateTimeEvent() + .setId(1L) + .setLocalDateTime(LocalDateTime.of(2024, 6, 18, 12, 34)); + + doInJPA(entityManager -> { + entityManager.persist(_event); + }); + + doInJPA(entityManager -> { + DateTimeEvent event = entityManager.createQuery(""" + select e + from DateTimeEvent e + where e.localDateTime = :localDateTime + """, DateTimeEvent.class) + .setParameter("localDateTime", _event.getLocalDateTime()) + .getSingleResult(); + + assertEquals( + LocalDateTime.of(2024, 6, 18, 12, 34), + event.getLocalDateTime() + ); + }); + } + + @Entity(name = "DateTimeEvent") + @Table(name = "date_time_event") + public static class DateTimeEvent { + + @Id + private Long id; + + @Column(name = "local_date_time") + private LocalDateTime localDateTime; + + public Long getId() { + return id; + } + + public DateTimeEvent setId(Long id) { + this.id = id; + return this; + } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public DateTimeEvent setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/PostgreSQLIntervalDurationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/PostgreSQLIntervalDurationTest.java new file mode 100644 index 000000000..b1f60aa21 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/PostgreSQLIntervalDurationTest.java @@ -0,0 +1,175 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.basic.YearMonthDateType; +import io.hypersistence.utils.hibernate.type.interval.PostgreSQLIntervalType; +import io.hypersistence.utils.hibernate.type.json.JsonStringType; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.jpa.boot.spi.TypeContributorList; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.Month; +import java.time.YearMonth; +import java.util.Collections; +import java.util.Properties; + +import static org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.TYPE_CONTRIBUTORS; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLIntervalDurationTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(TYPE_CONTRIBUTORS, + (TypeContributorList) () -> Collections.singletonList( + (typeContributions, serviceRegistry) -> { + typeContributions.contributeType(YearMonthDateType.INSTANCE); + } + ) + ); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setPublishedOn(YearMonth.of(2016, 10)) + .setPresalePeriod( + Duration.between( + LocalDate.of(2015, Month.NOVEMBER, 2).atStartOfDay(), + LocalDate.of(2016, Month.AUGUST, 25).atStartOfDay() + ) + ) + ); + }); + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals( + Duration.between( + LocalDate.of(2015, Month.NOVEMBER, 2).atStartOfDay(), + LocalDate.of(2016, Month.AUGUST, 25).atStartOfDay() + ), + book.getPresalePeriod() + ); + }); + + doInJPA(entityManager -> { + Tuple result = entityManager.createQuery(""" + SELECT + b.publishedOn AS published_on, + b.presalePeriod AS presale_period + FROM + Book b + WHERE + b.isbn = :isbn + """, Tuple.class) + .setParameter("isbn", "978-9730228236") + .getSingleResult(); + + assertEquals( + YearMonth.of(2016, 10), + result.get("published_on") + ); + + assertEquals( + Duration.between( + LocalDate.of(2015, Month.NOVEMBER, 2).atStartOfDay(), + LocalDate.of(2016, Month.AUGUST, 25).atStartOfDay() + ), + result.get("presale_period") + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + @Type(YearMonthDateType.class) + @Column(name = "published_on", columnDefinition = "date") + private YearMonth publishedOn; + + @Type(PostgreSQLIntervalType.class) + @Column(name = "presale_period", columnDefinition = "interval") + private Duration presalePeriod; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public YearMonth getPublishedOn() { + return publishedOn; + } + + public Book setPublishedOn(YearMonth publishedOn) { + this.publishedOn = publishedOn; + return this; + } + + public Duration getPresalePeriod() { + return presalePeriod; + } + + public Book setPresalePeriod(Duration presalePeriod) { + this.presalePeriod = presalePeriod; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/PostgreSQLJavaDateTimeJDBCBindingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/PostgreSQLJavaDateTimeJDBCBindingTest.java new file mode 100644 index 000000000..10232f3c9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/PostgreSQLJavaDateTimeJDBCBindingTest.java @@ -0,0 +1,123 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJavaDateTimeJDBCBindingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + DateTimeEvent.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.JAVA_TIME_USE_DIRECT_JDBC, "true"); + } + + @Test + public void test() { + DateTimeEvent _event = new DateTimeEvent() + .setId(1L) + .setLocalDateTime(LocalDateTime.of(2024, 6, 18, 12, 34)) + .setOffsetDateTime(OffsetDateTime.of(2024, 6, 18, 13, 25, 36, 0, ZoneOffset.UTC)); + + doInJPA(entityManager -> { + entityManager.persist(_event); + }); + + doInJPA(entityManager -> { + DateTimeEvent event = entityManager.createQuery(""" + select e + from DateTimeEvent e + where e.localDateTime = :localDateTime + """, DateTimeEvent.class) + .setParameter("localDateTime", _event.getLocalDateTime()) + .getSingleResult(); + + assertEquals( + LocalDateTime.of(2024, 6, 18, 12, 34), + event.getLocalDateTime() + ); + }); + + doInJPA(entityManager -> { + DateTimeEvent event = entityManager.createQuery(""" + select e + from DateTimeEvent e + where e.offsetDateTime = :offsetDateTime + """, DateTimeEvent.class) + .setParameter("offsetDateTime", _event.getOffsetDateTime()) + .getSingleResult(); + + assertEquals( + OffsetDateTime.of(2024, 6, 18, 13, 25, 36, 0, ZoneOffset.UTC), + event.getOffsetDateTime() + ); + }); + } + + @Entity(name = "DateTimeEvent") + @Table(name = "date_time_event") + public static class DateTimeEvent { + + @Id + private Long id; + + @Column(name = "local_date_time") + private LocalDateTime localDateTime; + + @Column(name = "offset_date_time") + private OffsetDateTime offsetDateTime; + + public Long getId() { + return id; + } + + public DateTimeEvent setId(Long id) { + this.id = id; + return this; + } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public DateTimeEvent setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + return this; + } + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public DateTimeEvent setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/PostgreSQLTimestampTimezoneHikariTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/PostgreSQLTimestampTimezoneHikariTest.java new file mode 100644 index 000000000..d60d13758 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/PostgreSQLTimestampTimezoneHikariTest.java @@ -0,0 +1,95 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import org.hibernate.Session; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import javax.sql.DataSource; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLTimestampTimezoneHikariTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + + protected boolean connectionPooling() { + return true; + } + + protected HikariConfig hikariConfig(DataSource dataSource) { + HikariConfig hikariConfig = super.hikariConfig(dataSource); + hikariConfig.setMaximumPoolSize(1); + hikariConfig.setConnectionInitSql("SET timezone TO 'UTC'"); + return hikariConfig; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void beforeInit() { + executeStatement("drop table if exists event cascade"); + executeStatement(""" + CREATE TABLE event ( + id integer not null, + timestamp_without_tz timestamp without time zone, + timestamp_with_tz timestamp with time zone, + PRIMARY KEY (id) + ) + """ + ); + } + + @Test + public void testDefaultSessionTimezone() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + statement.execute(""" + INSERT INTO event ( + id, timestamp_without_tz, timestamp_with_tz) + VALUES ( + 1, '2031-12-10 07:30:45.0', '2031-12-10 07:30:45.0') + """); + } + }); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + String timeZone = selectStringColumn(connection, "SELECT current_setting('timezone')"); + assertEquals("UTC", timeZone); + + ResultSet resultSet = statement.executeQuery(""" + SELECT timestamp_without_tz, timestamp_with_tz + FROM event + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/PostgreSQLTimestampTimezoneTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/PostgreSQLTimestampTimezoneTest.java new file mode 100644 index 000000000..2a77b3ff0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/PostgreSQLTimestampTimezoneTest.java @@ -0,0 +1,177 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.Session; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.Properties; +import java.util.TimeZone; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLTimestampTimezoneTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "none"); + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void beforeInit() { + executeStatement("drop table if exists event cascade"); + executeStatement(""" + CREATE TABLE event ( + id integer not null, + timestamp_without_tz timestamp without time zone, + timestamp_with_tz timestamp with time zone, + PRIMARY KEY (id) + ) + """ + ); + } + + @Test + public void testDefaultSessionTimezone() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT setting, source, context + FROM pg_settings + WHERE name = 'TimeZone' + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + statement.execute(""" + INSERT INTO event ( + id, timestamp_without_tz, timestamp_with_tz) + VALUES ( + 1, '2031-12-10 07:30:45.0', '2031-12-10 07:30:45.0') + """); + } + }); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + String timeZone = selectStringColumn(connection, "SELECT current_setting('timezone')"); + assertEquals(TimeZone.getDefault().getID(), timeZone); + + ResultSet resultSet = statement.executeQuery(""" + SELECT timestamp_without_tz, timestamp_with_tz + FROM event + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + } + + @Test + public void testDefaultSessionTimezoneOnRead() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + statement.execute("SET timezone TO 'Asia/Tokyo'"); + + statement.execute(""" + INSERT INTO event ( + id, timestamp_without_tz, timestamp_with_tz) + VALUES ( + 1, '2031-12-10 07:30:45.0', '2031-12-10 07:30:45.0') + """); + } + }); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + String timeZone = selectStringColumn(connection, "SELECT current_setting('timezone')"); + assertEquals(TimeZone.getDefault().getID(), timeZone); + + ResultSet resultSet = statement.executeQuery(""" + SELECT timestamp_without_tz, timestamp_with_tz + FROM event + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + } + + @Test + public void testExplicitSessionTimezone() { + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + statement.execute("SET timezone TO 'Asia/Tokyo'"); + + statement.execute(""" + INSERT INTO event ( + id, timestamp_without_tz, timestamp_with_tz) + VALUES ( + 1, '2031-12-10 07:30:45.0', '2031-12-10 07:30:45.0') + """); + } + }); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + statement.execute("SET timezone TO 'UTC'"); + + assertEquals( + "UTC", + selectStringColumn(connection, "SELECT current_setting('timezone')") + ); + + ResultSet resultSet = statement.executeQuery(""" + SELECT timestamp_without_tz, timestamp_with_tz + FROM event + WHERE id = 1 + """); + + while (resultSet.next()) { + String timestampWithoutTimezone = resultSet.getString(1); + String timestampWithTimezone = resultSet.getString(2); + + LOGGER.info("Timestamp without time zone: {}", timestampWithoutTimezone); + LOGGER.info("Timestamp with time zone: {}", timestampWithTimezone); + } + } + }); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/SQLServerJavaDateTimeJDBCBindingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/SQLServerJavaDateTimeJDBCBindingTest.java new file mode 100644 index 000000000..e8648314c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/SQLServerJavaDateTimeJDBCBindingTest.java @@ -0,0 +1,124 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerJavaDateTimeJDBCBindingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + DateTimeEvent.class + }; + } + + @Override + protected Database database() { + return Database.SQLSERVER; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.JAVA_TIME_USE_DIRECT_JDBC, Boolean.TRUE.toString()); + } + + @Test + public void test() { + DateTimeEvent _event = new DateTimeEvent() + .setId(1L) + .setLocalDateTime(LocalDateTime.of(2024, 6, 18, 12, 34)) + .setOffsetDateTime(OffsetDateTime.of(2024, 6, 18, 13, 25, 36, 0, ZoneOffset.UTC)); + + doInJPA(entityManager -> { + entityManager.persist(_event); + }); + + doInJPA(entityManager -> { + DateTimeEvent event = entityManager.createQuery(""" + select e + from DateTimeEvent e + where e.localDateTime = :localDateTime + """, DateTimeEvent.class) + .setParameter("localDateTime", _event.getLocalDateTime()) + .getSingleResult(); + + assertEquals( + LocalDateTime.of(2024, 6, 18, 12, 34), + event.getLocalDateTime() + ); + }); + + doInJPA(entityManager -> { + DateTimeEvent event = entityManager.createQuery(""" + select e + from DateTimeEvent e + where e.offsetDateTime = :offsetDateTime + """, DateTimeEvent.class) + .setParameter("offsetDateTime", _event.getOffsetDateTime()) + .getSingleResult(); + + assertEquals( + OffsetDateTime.of(2024, 6, 18, 13, 25, 36, 0, ZoneOffset.UTC), + event.getOffsetDateTime() + ); + }); + } + + @Entity(name = "DateTimeEvent") + @Table(name = "date_time_event") + public static class DateTimeEvent { + + @Id + private Long id; + + @Column(name = "local_date_time") + private LocalDateTime localDateTime; + + @Column(name = "offset_date_time") + private OffsetDateTime offsetDateTime; + + public Long getId() { + return id; + } + + public DateTimeEvent setId(Long id) { + this.id = id; + return this; + } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public DateTimeEvent setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + return this; + } + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public DateTimeEvent setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/WebToDatabaseTimestampTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/WebToDatabaseTimestampTest.java new file mode 100644 index 000000000..b5c6b6fff --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/WebToDatabaseTimestampTest.java @@ -0,0 +1,106 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +/** + * @author Vlad Mihalcea + */ +public class WebToDatabaseTimestampTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Book.class + }; + } + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Book book = new Book() + .setId(1) + .setTitle("High-Performance Java Persistence, 1st edition") + .setAuthor("Vlad Mihalcea") + .setPublishedOn( + OffsetDateTime.parse("2016-10-12T12:23:45.0+02:00") + .withOffsetSameInstant(ZoneOffset.UTC) + .toLocalDateTime() + ); + + entityManager.persist(book); + }); + + doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, 1); + + LOGGER.info( + "The first edition of High-Performance Java Persistence was published on {}", + book.getPublishedOn() + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + private Integer id; + + @Column(length = 100) + private String title; + + @Column(name = "author", length = 50) + private String author; + + @Column(name = "published_on", columnDefinition = "timestamp") + private LocalDateTime publishedOn; + + public Integer getId() { + return id; + } + + public Book setId(Integer id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public LocalDateTime getPublishedOn() { + return publishedOn; + } + + public Book setPublishedOn(LocalDateTime publishedOn) { + this.publishedOn = publishedOn; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleAllTimestampTypesOffsetDateTimeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleAllTimestampTypesOffsetDateTimeTest.java new file mode 100644 index 000000000..4e5f70575 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleAllTimestampTypesOffsetDateTimeTest.java @@ -0,0 +1,117 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime.oracle; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.DynamicUpdate; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class OracleAllTimestampTypesOffsetDateTimeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + protected Database database() { + return Database.ORACLE; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.createNativeQuery(""" + INSERT INTO event (id, created_on) + VALUES (1, TIMESTAMP '2031-12-10 07:30:45.123456') + """) + .executeUpdate(); + }); + + doInJPA(entityManager -> { + int updateCount = entityManager.createNativeQuery(""" + UPDATE event + SET updated_on = TIMESTAMP '2031-12-10 07:30:45.987654321 +02:00' + WHERE id = 1 + """) + .executeUpdate(); + + assertEquals(1, updateCount); + + Event event = entityManager.find(Event.class, 1); + + assertEquals( + OffsetDateTime.of(2031, 12, 10, 7, 30, 45, 987654321, ZoneOffset.of("+02:00")), + event.getUpdatedOn() + ); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + @DynamicUpdate + public static class Event { + + @Id + private Integer id; + + @Column(name = "created_on", columnDefinition = "TIMESTAMP(6)") + private LocalDateTime createdOn; + + @Column(name = "updated_on", columnDefinition = "TIMESTAMP(9) WITH TIME ZONE") + private OffsetDateTime updatedOn; + + @Column(name = "last_accessed_on", columnDefinition = "TIMESTAMP WITH LOCAL TIME ZONE") + private OffsetDateTime lastAccessedOn; + + public Integer getId() { + return id; + } + + public Event setId(Integer id) { + this.id = id; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Event setCreatedOn(LocalDateTime dateProperty) { + this.createdOn = dateProperty; + return this; + } + + public OffsetDateTime getUpdatedOn() { + return updatedOn; + } + + public Event setUpdatedOn(OffsetDateTime timestampProperty) { + this.updatedOn = timestampProperty; + return this; + } + + public OffsetDateTime getLastAccessedOn() { + return lastAccessedOn; + } + + public Event setLastAccessedOn(OffsetDateTime timestampPrecisionPreproperty) { + this.lastAccessedOn = timestampPrecisionPreproperty; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleDateVsTimestampTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleDateVsTimestampTest.java new file mode 100644 index 000000000..2474232ed --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleDateVsTimestampTest.java @@ -0,0 +1,130 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime.oracle; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Properties; + +import static com.vladmihalcea.hpjp.hibernate.type.json.model.Participant_.event; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class OracleDateVsTimestampTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + protected Database database() { + return Database.ORACLE; + } + + @Test + public void test() { + Event _event = new Event() + .setId(1) + .setCreatedOn(LocalDateTime.of(2031, 12, 10, 7, 30, 45)) + .setUpdatedOn(LocalDateTime.of(2031, 12, 10, 10, 30, 45, 987654321)) + .setLastAccessedOn(LocalDateTime.of(2031, 12, 10, 12, 30, 45, 987654321)); + + doInJPA(entityManager -> { + entityManager.persist(_event); + }); + + doInJPA(entityManager -> { + Event event = entityManager.createQuery(""" + select e + from Event e + where e.id = :id + """, Event.class) + .setParameter("id", 1) + .getSingleResult(); + + assertEquals( + LocalDateTime.of(2031, 12, 10, 7, 30, 45), + event.getCreatedOn() + ); + }); + + doInJPA(entityManager -> { + Tuple bytes = (Tuple) entityManager.createNativeQuery(""" + SELECT + VSIZE(created_on) AS created_on_bytes, + VSIZE(updated_on) AS updated_on_bytes, + VSIZE(last_accessed_on) AS last_accessed_on_bytes + FROM event e + WHERE e.id = :id + """, Tuple.class) + .setParameter("id", 1) + .getSingleResult(); + + LOGGER.info("Byte count: {}", bytes); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event { + + @Id + private Integer id; + + @Column(name = "created_on", columnDefinition = "DATE") + private LocalDateTime createdOn; + + @Column(name = "updated_on", columnDefinition = "TIMESTAMP(0)") + private LocalDateTime updatedOn; + + @Column(name = "last_accessed_on", columnDefinition = "TIMESTAMP(6)") + private LocalDateTime lastAccessedOn; + + public Integer getId() { + return id; + } + + public Event setId(Integer id) { + this.id = id; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Event setCreatedOn(LocalDateTime dateProperty) { + this.createdOn = dateProperty; + return this; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public Event setUpdatedOn(LocalDateTime timestampProperty) { + this.updatedOn = timestampProperty; + return this; + } + + public LocalDateTime getLastAccessedOn() { + return lastAccessedOn; + } + + public Event setLastAccessedOn(LocalDateTime timestampPrecisionPreproperty) { + this.lastAccessedOn = timestampPrecisionPreproperty; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleJavaDateTimeJDBCBindingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleJavaDateTimeJDBCBindingTest.java new file mode 100644 index 000000000..3f714f4b5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleJavaDateTimeJDBCBindingTest.java @@ -0,0 +1,152 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime.oracle; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.cfg.AvailableSettings; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class OracleJavaDateTimeJDBCBindingTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + DateTimeEvent.class + }; + } + + @Override + protected Database database() { + return Database.ORACLE; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.JAVA_TIME_USE_DIRECT_JDBC, "true"); + } + + @Test + public void test() { + DateTimeEvent _event = new DateTimeEvent() + .setId(1L) + .setLocalDateTime(LocalDateTime.of(2024, 6, 18, 12, 34)) + .setOffsetDateTime(OffsetDateTime.of(2024, 6, 18, 13, 25, 36, 0, ZoneOffset.UTC)) + .setZonedDateTime(ZonedDateTime.of(2024, 6, 18, 11, 22, 33, 0, ZoneOffset.UTC)); + + doInJPA(entityManager -> { + entityManager.persist(_event); + }); + + doInJPA(entityManager -> { + DateTimeEvent event = entityManager.createQuery(""" + select e + from DateTimeEvent e + where e.localDateTime = :localDateTime + """, DateTimeEvent.class) + .setParameter("localDateTime", _event.getLocalDateTime()) + .getSingleResult(); + + assertEquals( + LocalDateTime.of(2024, 6, 18, 12, 34), + event.getLocalDateTime() + ); + }); + + doInJPA(entityManager -> { + DateTimeEvent event = entityManager.createQuery(""" + select e + from DateTimeEvent e + where e.offsetDateTime = :offsetDateTime + """, DateTimeEvent.class) + .setParameter("offsetDateTime", _event.getOffsetDateTime()) + .getSingleResult(); + + assertEquals( + OffsetDateTime.of(2024, 6, 18, 13, 25, 36, 0, ZoneOffset.UTC), + event.getOffsetDateTime() + ); + }); + + doInJPA(entityManager -> { + DateTimeEvent event = entityManager.createQuery(""" + select e + from DateTimeEvent e + where e.zonedDateTime = :zonedDateTime + """, DateTimeEvent.class) + .setParameter("zonedDateTime", _event.getZonedDateTime()) + .getSingleResult(); + + assertEquals( + ZonedDateTime.of(2024, 6, 18, 11, 22, 33, 0, ZoneOffset.UTC), + event.getZonedDateTime() + ); + }); + } + + @Entity(name = "DateTimeEvent") + @Table(name = "date_time_event") + public static class DateTimeEvent { + + @Id + private Long id; + + @Column(name = "local_date_time") + private LocalDateTime localDateTime; + + @Column(name = "offset_date_time") + private OffsetDateTime offsetDateTime; + + @Column(name = "zoned_date_time") + private ZonedDateTime zonedDateTime; + + public Long getId() { + return id; + } + + public DateTimeEvent setId(Long id) { + this.id = id; + return this; + } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public DateTimeEvent setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + return this; + } + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public DateTimeEvent setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + return this; + } + + public ZonedDateTime getZonedDateTime() { + return zonedDateTime; + } + + public DateTimeEvent setZonedDateTime(ZonedDateTime zonedDateTime) { + this.zonedDateTime = zonedDateTime; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleTimestampColumnSizeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleTimestampColumnSizeTest.java new file mode 100644 index 000000000..f6c4bc2f9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleTimestampColumnSizeTest.java @@ -0,0 +1,112 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime.oracle; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.annotations.DynamicUpdate; +import org.junit.Test; + +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +/** + * @author Vlad Mihalcea + */ +public class OracleTimestampColumnSizeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + protected Database database() { + return Database.ORACLE; + } + + @Test + public void test() { + Event _event = new Event() + .setId(1) + .setCreatedOn(OffsetDateTime.of(2031, 12, 10, 7, 30, 45, 0, ZoneOffset.of("+09:00"))) + .setUpdatedOn(ZonedDateTime.of(2031, 12, 10, 7, 30, 45, 987654321, ZoneId.of("Asia/Tokyo"))) + .setLastAccessedOn(OffsetDateTime.of(2031, 12, 10, 7, 30, 45, 987654321, ZoneOffset.of("+09:00"))); + + doInJPA(entityManager -> { + entityManager.persist(_event); + }); + + doInJPA(entityManager -> { + Tuple bytes = (Tuple) entityManager.createNativeQuery(""" + SELECT + VSIZE(created_on) AS created_on_bytes, + VSIZE(updated_on) AS updated_on_bytes, + VSIZE(last_accessed_on) AS last_accessed_on_bytes + FROM event e + WHERE e.id = :id + """, Tuple.class) + .setParameter("id", 1) + .getSingleResult(); + + LOGGER.info("Byte count: {}", bytes); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + @DynamicUpdate + public static class Event { + + @Id + private Integer id; + + @Column(name = "created_on", columnDefinition = "TIMESTAMP(0) WITH TIME ZONE") + private OffsetDateTime createdOn; + + @Column(name = "updated_on", columnDefinition = "TIMESTAMP(9) WITH TIME ZONE") + private ZonedDateTime updatedOn; + + @Column(name = "last_accessed_on", columnDefinition = "TIMESTAMP WITH LOCAL TIME ZONE") + private OffsetDateTime lastAccessedOn; + + public Integer getId() { + return id; + } + + public Event setId(Integer id) { + this.id = id; + return this; + } + + public OffsetDateTime getCreatedOn() { + return createdOn; + } + + public Event setCreatedOn(OffsetDateTime dateProperty) { + this.createdOn = dateProperty; + return this; + } + + public ZonedDateTime getUpdatedOn() { + return updatedOn; + } + + public Event setUpdatedOn(ZonedDateTime timestampProperty) { + this.updatedOn = timestampProperty; + return this; + } + + public OffsetDateTime getLastAccessedOn() { + return lastAccessedOn; + } + + public Event setLastAccessedOn(OffsetDateTime timestampPrecisionPreproperty) { + this.lastAccessedOn = timestampPrecisionPreproperty; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleTimestampWithTimeZoneOffsetDateTimeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleTimestampWithTimeZoneOffsetDateTimeTest.java new file mode 100644 index 000000000..5000c5e5d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleTimestampWithTimeZoneOffsetDateTimeTest.java @@ -0,0 +1,193 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime.oracle; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.NaturalId; +import org.junit.Test; + +import java.sql.ResultSet; +import java.sql.Statement; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class OracleTimestampWithTimeZoneOffsetDateTimeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Book.class + }; + } + + @Override + protected Database database() { + return Database.ORACLE; + } + + @Test + public void testPersist() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setId(1) + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea") + .setPublishedOn( + OffsetDateTime.of( + 2016, 10, 12, 7, 30, 45, 0, + ZoneOffset.of("+02:00") + ) + ) + ); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT title, published_on + FROM book + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + + doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, 1); + assertEquals( + book.getPublishedOn(), + OffsetDateTime.of( + 2016, 10, 12, 7, 30, 45, 0, + ZoneOffset.of("+02:00") + ) + ); + + book.setUpdatedOn( + OffsetDateTime.of( + 2024, 7, 18, 10, 45, 0, 0, + ZoneOffset.of("+01:00") + ) + ); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT title, updated_on + FROM book + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + + doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, 1); + assertEquals( + book.getUpdatedOn(), + OffsetDateTime.of( + 2024, 7, 18, 10, 45, 0, 0, + ZoneOffset.of("+01:00") + ) + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + @DynamicInsert @DynamicUpdate + public static class Book { + + @Id + private Integer id; + + @NaturalId + @Column(length = 15) + private String isbn; + + @Column(length = 50) + private String title; + + @Column(length = 50) + private String author; + + @Column(name = "published_on", columnDefinition = "TIMESTAMP WITH TIME ZONE") + private OffsetDateTime publishedOn; + + @Column(name = "updated_on", columnDefinition = "TIMESTAMP WITH TIME ZONE") + private OffsetDateTime updatedOn; + + public Integer getId() { + return id; + } + + public Book setId(Integer id) { + this.id = id; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + + public OffsetDateTime getPublishedOn() { + return publishedOn; + } + + public Book setPublishedOn(OffsetDateTime publishedOn) { + this.publishedOn = publishedOn; + return this; + } + + public OffsetDateTime getUpdatedOn() { + return updatedOn; + } + + public Book setUpdatedOn(OffsetDateTime timestampProperty) { + this.updatedOn = timestampProperty; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleTimestampWithTimeZoneVsLocalTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleTimestampWithTimeZoneVsLocalTest.java new file mode 100644 index 000000000..b442e3d89 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleTimestampWithTimeZoneVsLocalTest.java @@ -0,0 +1,253 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime.oracle; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.JdbcType; +import org.hibernate.type.descriptor.jdbc.ZonedDateTimeJdbcType; +import org.junit.Test; + +import java.sql.ResultSet; +import java.sql.Statement; +import java.time.*; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class OracleTimestampWithTimeZoneVsLocalTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + protected Database database() { + return Database.ORACLE; + } + + @Test + public void testTimestampColumn() { + doInJPA(entityManager -> { + entityManager.createNativeQuery(""" + INSERT INTO event (id, created_on) + VALUES (1, TIMESTAMP '2031-12-10 07:30:45.123456') + """) + .executeUpdate(); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1); + + assertEquals( + LocalDateTime.parse("2031-12-10T07:30:45.123456"), + event.getCreatedOn() + ); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.createNativeQuery(""" + INSERT INTO event (id, created_on) + VALUES (1, TIMESTAMP '2031-12-10 07:30:45.123456') + """) + .executeUpdate(); + }); + + doInJPA(entityManager -> { + int updateCount = entityManager.createNativeQuery(""" + UPDATE event + SET updated_on = TIMESTAMP '2031-12-10 07:30:45.987654321 Europe/Paris' + WHERE id = 1 + """) + .executeUpdate(); + + assertEquals(1, updateCount); + + Event event = entityManager.find(Event.class, 1); + + assertEquals( + ZonedDateTime.of(2031, 12, 10, 7, 30, 45, 987654321, ZoneId.of("Europe/Paris")), + event.getUpdatedOn() + ); + + event.setUpdatedOn( + ZonedDateTime.of(2031, 12, 10, 7, 30, 45, 987654321, ZoneId.of("Europe/London")) + ); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT updated_on + FROM event + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT updated_on + FROM event + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + + doInJPA(entityManager -> { + int updateCount = entityManager.createNativeQuery(""" + UPDATE event + SET updated_on = TIMESTAMP '2031-12-10 07:30:45.987654321 +02:00' + WHERE id = 1 + """) + .executeUpdate(); + + assertEquals(1, updateCount); + + Event event = entityManager.find(Event.class, 1); + + assertEquals( + OffsetDateTime.of(2031, 12, 10, 7, 30, 45, 987654321, ZoneOffset.of("+02:00")), + event.getUpdatedOn().toOffsetDateTime() + ); + }); + + doInJPA(entityManager -> { + int updateCount = entityManager.createNativeQuery(""" + UPDATE event + SET last_accessed_on = TIMESTAMP '2031-12-10 07:30:45.987654321 Europe/Paris' + WHERE id = 1 + """) + .executeUpdate(); + + assertEquals(1, updateCount); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT + TZ_OFFSET('Europe/Paris') as Paris_Zone_Offset, + TZ_OFFSET(SESSIONTIMEZONE) as Our_Zone_Offset + FROM dual + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT last_accessed_on + FROM event + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + executeStatement(connection, "ALTER SESSION SET time_zone='Europe/Paris'"); + + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT last_accessed_on + FROM event + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1); + + assertEquals( + ZonedDateTime.of(2031, 12, 10, 7, 30, 45, 9876543, ZoneId.of("Europe/Paris")) + .withZoneSameInstant(ZoneId.systemDefault()).toOffsetDateTime(), + event.getLastAccessedOn() + ); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + @DynamicUpdate + public static class Event { + + @Id + private Integer id; + + @Column(name = "created_on", columnDefinition = "TIMESTAMP(6)") + private LocalDateTime createdOn; + + @JdbcType(ZonedDateTimeJdbcType.class) + @Column(name = "updated_on", columnDefinition = "TIMESTAMP(9) WITH TIME ZONE") + private ZonedDateTime updatedOn; + + @Column(name = "last_accessed_on", columnDefinition = "TIMESTAMP WITH LOCAL TIME ZONE") + private OffsetDateTime lastAccessedOn; + + public Integer getId() { + return id; + } + + public Event setId(Integer id) { + this.id = id; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Event setCreatedOn(LocalDateTime dateProperty) { + this.createdOn = dateProperty; + return this; + } + + public ZonedDateTime getUpdatedOn() { + return updatedOn; + } + + public Event setUpdatedOn(ZonedDateTime timestampProperty) { + this.updatedOn = timestampProperty; + return this; + } + + public OffsetDateTime getLastAccessedOn() { + return lastAccessedOn; + } + + public Event setLastAccessedOn(OffsetDateTime timestampPrecisionPreproperty) { + this.lastAccessedOn = timestampPrecisionPreproperty; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleTimestampWithTimeZoneZonedDateTimeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleTimestampWithTimeZoneZonedDateTimeTest.java new file mode 100644 index 000000000..4a30a7dea --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/OracleTimestampWithTimeZoneZonedDateTimeTest.java @@ -0,0 +1,197 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime.oracle; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.JdbcType; +import org.hibernate.annotations.NaturalId; +import org.hibernate.type.descriptor.jdbc.ZonedDateTimeJdbcType; +import org.junit.Test; + +import java.sql.ResultSet; +import java.sql.Statement; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class OracleTimestampWithTimeZoneZonedDateTimeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Book.class + }; + } + + @Override + protected Database database() { + return Database.ORACLE; + } + + @Test + public void testPersist() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setId(1) + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea") + .setPublishedOn( + ZonedDateTime.of( + 2016, 10, 12, 7, 30, 45, 0, + ZoneId.of("Europe/Bucharest") + ) + ) + ); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT title, published_on + FROM book + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + + doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, 1); + assertEquals( + book.getPublishedOn(), + ZonedDateTime.of( + 2016, 10, 12, 7, 30, 45, 0, + ZoneId.of("Europe/Bucharest") + ) + ); + + book.setUpdatedOn( + ZonedDateTime.of( + 2024, 7, 18, 10, 45, 0, 0, + ZoneId.of("Europe/Paris") + ) + ); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT title, updated_on + FROM book + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + + doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, 1); + assertEquals( + book.getUpdatedOn(), + ZonedDateTime.of( + 2024, 7, 18, 10, 45, 0, 0, + ZoneId.of("Europe/Paris") + ) + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + @DynamicInsert @DynamicUpdate + public static class Book { + + @Id + private Integer id; + + @NaturalId + @Column(length = 15) + private String isbn; + + @Column(length = 50) + private String title; + + @Column(length = 50) + private String author; + + @JdbcType(ZonedDateTimeJdbcType.class) + @Column(name = "published_on", columnDefinition = "TIMESTAMP WITH TIME ZONE") + private ZonedDateTime publishedOn; + + @JdbcType(ZonedDateTimeJdbcType.class) + @Column(name = "updated_on", columnDefinition = "TIMESTAMP WITH TIME ZONE") + private ZonedDateTime updatedOn; + + public Integer getId() { + return id; + } + + public Book setId(Integer id) { + this.id = id; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + + public ZonedDateTime getPublishedOn() { + return publishedOn; + } + + public Book setPublishedOn(ZonedDateTime publishedOn) { + this.publishedOn = publishedOn; + return this; + } + + public ZonedDateTime getUpdatedOn() { + return updatedOn; + } + + public Book setUpdatedOn(ZonedDateTime timestampProperty) { + this.updatedOn = timestampProperty; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/SQLServerDateTimeColumnSizeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/SQLServerDateTimeColumnSizeTest.java new file mode 100644 index 000000000..71e2bd881 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/SQLServerDateTimeColumnSizeTest.java @@ -0,0 +1,112 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime.oracle; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.annotations.DynamicUpdate; +import org.junit.Test; + +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerDateTimeColumnSizeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + protected Database database() { + return Database.SQLSERVER; + } + + @Test + public void test() { + Event _event = new Event() + .setId(1) + .setCreatedOn(OffsetDateTime.of(2031, 12, 10, 7, 30, 45, 0, ZoneOffset.of("+09:00"))) + .setUpdatedOn(ZonedDateTime.of(2031, 12, 10, 7, 30, 45, 987654321, ZoneId.of("Asia/Tokyo"))) + .setLastAccessedOn(OffsetDateTime.of(2031, 12, 10, 7, 30, 45, 987654321, ZoneOffset.of("+09:00"))); + + doInJPA(entityManager -> { + entityManager.persist(_event); + }); + + doInJPA(entityManager -> { + Tuple bytes = (Tuple) entityManager.createNativeQuery(""" + SELECT + DATALENGTH(created_on) AS created_on_bytes, + DATALENGTH(updated_on) AS updated_on_bytes, + DATALENGTH(last_accessed_on) AS last_accessed_on_bytes + FROM event e + WHERE e.id = :id + """, Tuple.class) + .setParameter("id", 1) + .getSingleResult(); + + LOGGER.info("Byte count: {}", bytes); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + @DynamicUpdate + public static class Event { + + @Id + private Integer id; + + @Column(name = "created_on", columnDefinition = "datetime2(0)") + private OffsetDateTime createdOn; + + @Column(name = "updated_on", columnDefinition = "datetime2(3)") + private ZonedDateTime updatedOn; + + @Column(name = "last_accessed_on", columnDefinition = "datetime2(5)") + private OffsetDateTime lastAccessedOn; + + public Integer getId() { + return id; + } + + public Event setId(Integer id) { + this.id = id; + return this; + } + + public OffsetDateTime getCreatedOn() { + return createdOn; + } + + public Event setCreatedOn(OffsetDateTime dateProperty) { + this.createdOn = dateProperty; + return this; + } + + public ZonedDateTime getUpdatedOn() { + return updatedOn; + } + + public Event setUpdatedOn(ZonedDateTime timestampProperty) { + this.updatedOn = timestampProperty; + return this; + } + + public OffsetDateTime getLastAccessedOn() { + return lastAccessedOn; + } + + public Event setLastAccessedOn(OffsetDateTime timestampPrecisionPreproperty) { + this.lastAccessedOn = timestampPrecisionPreproperty; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/SQLServerDateTimeOffsetColumnSizeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/SQLServerDateTimeOffsetColumnSizeTest.java new file mode 100644 index 000000000..e8217467e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/SQLServerDateTimeOffsetColumnSizeTest.java @@ -0,0 +1,112 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime.oracle; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.annotations.DynamicUpdate; +import org.junit.Test; + +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerDateTimeOffsetColumnSizeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + protected Database database() { + return Database.SQLSERVER; + } + + @Test + public void test() { + Event _event = new Event() + .setId(1) + .setCreatedOn(OffsetDateTime.of(2031, 12, 10, 7, 30, 45, 0, ZoneOffset.of("+09:00"))) + .setUpdatedOn(ZonedDateTime.of(2031, 12, 10, 7, 30, 45, 987654321, ZoneId.of("Asia/Tokyo"))) + .setLastAccessedOn(OffsetDateTime.of(2031, 12, 10, 7, 30, 45, 987654321, ZoneOffset.of("+09:00"))); + + doInJPA(entityManager -> { + entityManager.persist(_event); + }); + + doInJPA(entityManager -> { + Tuple bytes = (Tuple) entityManager.createNativeQuery(""" + SELECT + DATALENGTH(created_on) AS created_on_bytes, + DATALENGTH(updated_on) AS updated_on_bytes, + DATALENGTH(last_accessed_on) AS last_accessed_on_bytes + FROM event e + WHERE e.id = :id + """, Tuple.class) + .setParameter("id", 1) + .getSingleResult(); + + LOGGER.info("Byte count: {}", bytes); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + @DynamicUpdate + public static class Event { + + @Id + private Integer id; + + @Column(name = "created_on", columnDefinition = "datetimeoffset(0)") + private OffsetDateTime createdOn; + + @Column(name = "updated_on", columnDefinition = "datetimeoffset(3)") + private ZonedDateTime updatedOn; + + @Column(name = "last_accessed_on", columnDefinition = "datetimeoffset(5)") + private OffsetDateTime lastAccessedOn; + + public Integer getId() { + return id; + } + + public Event setId(Integer id) { + this.id = id; + return this; + } + + public OffsetDateTime getCreatedOn() { + return createdOn; + } + + public Event setCreatedOn(OffsetDateTime dateProperty) { + this.createdOn = dateProperty; + return this; + } + + public ZonedDateTime getUpdatedOn() { + return updatedOn; + } + + public Event setUpdatedOn(ZonedDateTime timestampProperty) { + this.updatedOn = timestampProperty; + return this; + } + + public OffsetDateTime getLastAccessedOn() { + return lastAccessedOn; + } + + public Event setLastAccessedOn(OffsetDateTime timestampPrecisionPreproperty) { + this.lastAccessedOn = timestampPrecisionPreproperty; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/SQLServerDateTimeVsOffsetTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/SQLServerDateTimeVsOffsetTest.java new file mode 100644 index 000000000..fb417fa3d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/datetime/oracle/SQLServerDateTimeVsOffsetTest.java @@ -0,0 +1,221 @@ +package com.vladmihalcea.hpjp.hibernate.type.datetime.oracle; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.JdbcType; +import org.hibernate.type.descriptor.jdbc.ZonedDateTimeJdbcType; +import org.junit.Test; + +import java.sql.ResultSet; +import java.sql.Statement; +import java.time.*; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerDateTimeVsOffsetTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class + }; + } + + @Override + protected Database database() { + return Database.SQLSERVER; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.createNativeQuery(""" + INSERT INTO event (id, created_on) + VALUES (1, '2031-12-10 07:30:45.1234567 +12:00') + """) + .executeUpdate(); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT created_on + FROM event + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + + Event event = entityManager.find(Event.class, 1); + + assertEquals( + LocalDateTime.of(2031, 12, 10, 7, 30, 45, 123456700), + event.getCreatedOn() + ); + }); + + doInJPA(entityManager -> { + int updateCount = entityManager.createNativeQuery(""" + UPDATE event + SET updated_on = '2031-12-10 07:30:45.9876543 +01:00' + WHERE id = 1 + """) + .executeUpdate(); + + assertEquals(1, updateCount); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT updated_on + FROM event + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + + Event event = entityManager.find(Event.class, 1); + + assertEquals( + OffsetDateTime.of(2031, 12, 10, 7, 30, 45, 987654300, ZoneOffset.of("+01:00")), + event.getUpdatedOn() + ); + + event.setUpdatedOn( + OffsetDateTime.of(2031, 12, 10, 7, 30, 45, 987654300, ZoneOffset.of("+09:00")) + ); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT updated_on + FROM event + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + + doInJPA(entityManager -> { + int updateCount = entityManager.createNativeQuery(""" + UPDATE event + SET updated_on = '2031-12-10 07:30:45.9876543 +00:00' + WHERE id = 1 + """) + .executeUpdate(); + + assertEquals(1, updateCount); + + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT updated_on + FROM event + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + + doInJPA(entityManager -> { + int updateCount = entityManager.createNativeQuery(""" + UPDATE event + SET last_accessed_on = '2031-12-10 07:30:45' + WHERE id = 1 + """) + .executeUpdate(); + + assertEquals(1, updateCount); + }); + + doInJPA(entityManager -> { + entityManager.unwrap(Session.class).doWork(connection -> { + try(Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery(""" + SELECT last_accessed_on + FROM event + WHERE id = 1 + """); + + LOGGER.info("{}{}", System.lineSeparator(), resultSetToString(resultSet)); + } + }); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + @DynamicUpdate + public static class Event { + + @Id + private Integer id; + + @Column(name = "created_on", columnDefinition = "datetime2") + private LocalDateTime createdOn; + + @Column(name = "updated_on", columnDefinition = "datetimeoffset") + private OffsetDateTime updatedOn; + + @Column(name = "last_accessed_on", columnDefinition = "smalldatetime") + private OffsetDateTime lastAccessedOn; + + public Integer getId() { + return id; + } + + public Event setId(Integer id) { + this.id = id; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Event setCreatedOn(LocalDateTime dateProperty) { + this.createdOn = dateProperty; + return this; + } + + public OffsetDateTime getUpdatedOn() { + return updatedOn; + } + + public Event setUpdatedOn(OffsetDateTime timestampProperty) { + this.updatedOn = timestampProperty; + return this; + } + + public OffsetDateTime getLastAccessedOn() { + return lastAccessedOn; + } + + public Event setLastAccessedOn(OffsetDateTime timestampPrecisionPreproperty) { + this.lastAccessedOn = timestampPrecisionPreproperty; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/MySQLJsonRecordTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/MySQLJsonRecordTest.java new file mode 100644 index 000000000..b9b7f795c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/MySQLJsonRecordTest.java @@ -0,0 +1,151 @@ +package com.vladmihalcea.hpjp.hibernate.type.json; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.json.JsonType; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.io.Serializable; +import java.net.URL; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MySQLJsonRecordTest extends AbstractMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + new BookRecord( + "High-Performance Java Persistence", + "Vlad Mihalcea", + "Amazon", + 4499L, + null + ) + ) + ); + }); + } + + @Test + public void testFetchAndUpdate() { + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + BookRecord bookRecord = book.getProperties(); + + assertEquals( + "High-Performance Java Persistence", + bookRecord.title() + ); + assertEquals( + "Vlad Mihalcea", + bookRecord.author() + ); + + LOGGER.info("Book details: {}", book.getProperties()); + + book.setProperties( + new BookRecord( + bookRecord.title(), + bookRecord.author(), + bookRecord.publisher(), + bookRecord.priceInCents(), + urlValue("https://www.amazon.com/dp/973022823X/") + ) + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "json") + private BookRecord properties; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public BookRecord getProperties() { + return properties; + } + + public Book setProperties(BookRecord properties) { + this.properties = properties; + return this; + } + } + + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) + public static record BookRecord ( + String title, + String author, + String publisher, + Long priceInCents, + URL url + ) implements Serializable { + @JsonCreator + public BookRecord( + @JsonProperty("title") String title, + @JsonProperty("author") String author, + @JsonProperty("publisher") String publisher, + @JsonProperty("priceInCents") String priceInCents, + @JsonProperty("url") String url) { + this( + title, + author, + publisher, + longValue(priceInCents), + urlValue(url) + ); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/json/MySQLJsonTypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/MySQLJsonTypeTest.java similarity index 85% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/json/MySQLJsonTypeTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/MySQLJsonTypeTest.java index 4f978a8cd..00dd181ea 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/json/MySQLJsonTypeTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/MySQLJsonTypeTest.java @@ -1,13 +1,17 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json; - -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.BaseEntity; -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.Location; -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.Ticket; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; +package com.vladmihalcea.hpjp.hibernate.type.json; + +import com.vladmihalcea.hpjp.hibernate.type.json.model.BaseEntity; +import com.vladmihalcea.hpjp.hibernate.type.json.model.Location; +import com.vladmihalcea.hpjp.hibernate.type.json.model.Ticket; +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.json.JsonStringType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import org.hibernate.annotations.Type; import org.junit.Test; -import javax.persistence.*; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -89,7 +93,7 @@ public void test() { @Entity(name = "Event") @Table(name = "event") public static class Event extends BaseEntity { - @Type(type = "json") + @Type(JsonStringType.class) @Column(columnDefinition = "json") private Location location; @@ -105,7 +109,7 @@ public void setLocation(Location location) { @Entity(name = "Participant") @Table(name = "participant") public static class Participant extends BaseEntity { - @Type(type = "json") + @Type(JsonStringType.class) @Column(columnDefinition = "json") private Ticket ticket; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeLazyGroupTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeLazyGroupTest.java similarity index 83% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeLazyGroupTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeLazyGroupTest.java index 2062ef411..c516d9ecc 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeLazyGroupTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeLazyGroupTest.java @@ -1,10 +1,10 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json; +package com.vladmihalcea.hpjp.hibernate.type.json; -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.Event; -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.Location; -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.Participant; -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.Ticket; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.hibernate.type.json.model.Event; +import com.vladmihalcea.hpjp.hibernate.type.json.model.Location; +import com.vladmihalcea.hpjp.hibernate.type.json.model.Participant; +import com.vladmihalcea.hpjp.hibernate.type.json.model.Ticket; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.junit.Test; import java.util.concurrent.atomic.AtomicReference; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeNativeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeNativeTest.java new file mode 100644 index 000000000..f06f7291e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeNativeTest.java @@ -0,0 +1,316 @@ +package com.vladmihalcea.hpjp.hibernate.type.json; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonBinaryTypeNativeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setId(1L) + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea") + .setPrice(new BigDecimal("44.99")) + .setPublisher("Amazon KDP") + ); + }); + } + + @Test + public void testBasicParameters() { + doInJPA(entityManager -> { + Book book = entityManager.createQuery(""" + select b + from Book b + where b.isbn = :isbn + """, + Book.class) + .setParameter("isbn", "978-9730228236") + .getSingleResult(); + + assertEquals("High-Performance Java Persistence", book.title); + }); + + doInJPA(entityManager -> { + List books = entityManager.createQuery(""" + select b + from Book b + where b.publisher in (:publishers) + """, + Book.class) + .setParameter( + "publishers", + Arrays.asList( + "O'Reilly", + "Manning", + "Amazon KDP" + ) + ) + .getResultList(); + + assertEquals(1, books.size()); + }); + } + + @Test + public void testUpdateListUsingNativeSQL() { + + doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, 1L); + + assertTrue(book.getReviews().isEmpty()); + + int updateCount = entityManager.createNativeQuery(""" + UPDATE + book + SET + reviews = :reviews + WHERE + isbn = :isbn AND + jsonb_array_length(reviews) = 0 + """) + .setParameter("isbn", "978-9730228236") + .unwrap(org.hibernate.query.Query.class) + .setParameter( + "reviews", + Arrays.asList( + new BookReview() + .setReview("Excellent book to understand Java Persistence") + .setRating(5), + new BookReview() + .setReview("The best JPA ORM book out there") + .setRating(5) + ), + new JsonBinaryType(BookProperties.class) + ) + .executeUpdate(); + + entityManager.refresh(book); + + assertEquals(2, book.getReviews().size()); + }); + } + + @Test + public void testUpdateSerializableUsingNativeSQL() { + + doInJPA(entityManager -> { + Book book = entityManager.find(Book.class, 1L); + + assertNull(book.getProperties()); + + int updateCount = entityManager.createNativeQuery(""" + UPDATE + book + SET + properties = :properties + WHERE + isbn = :isbn AND + properties ->> 'weight' is null + """) + .setParameter("isbn", "978-9730228236") + .unwrap(org.hibernate.query.Query.class) + .setParameter( + "properties", + new BookProperties() + .setWidth(new BigDecimal("8.5")) + .setHeight(new BigDecimal("11")) + .setWeight(new BigDecimal("2.5")), + new JsonBinaryType(BookProperties.class) + ) + .executeUpdate(); + + entityManager.refresh(book); + + BookProperties properties = book.getProperties(); + assertEquals(new BigDecimal("2.5"), properties.getWeight()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + private Long id; + + private String isbn; + + private String title; + + private String author; + + private String publisher; + + private BigDecimal price; + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private List reviews = new ArrayList<>(); + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private BookProperties properties; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + + public String getPublisher() { + return publisher; + } + + public Book setPublisher(String publisher) { + this.publisher = publisher; + return this; + } + + public BigDecimal getPrice() { + return price; + } + + public Book setPrice(BigDecimal price) { + this.price = price; + return this; + } + + public List getReviews() { + return reviews; + } + + public Book setReviews(List reviews) { + this.reviews = reviews; + return this; + } + + public BookProperties getProperties() { + return properties; + } + + public Book setProperties(BookProperties properties) { + this.properties = properties; + return this; + } + } + + public static class BookReview implements Serializable { + + private String review; + + private int rating; + + public String getReview() { + return review; + } + + public BookReview setReview(String review) { + this.review = review; + return this; + } + + public int getRating() { + return rating; + } + + public BookReview setRating(int rating) { + this.rating = rating; + return this; + } + } + + public static class BookProperties implements Serializable { + + private BigDecimal width; + + private BigDecimal height; + + private BigDecimal weight; + + public BigDecimal getWidth() { + return width; + } + + public BookProperties setWidth(BigDecimal width) { + this.width = width; + return this; + } + + public BigDecimal getHeight() { + return height; + } + + public BookProperties setHeight(BigDecimal height) { + this.height = height; + return this; + } + + public BigDecimal getWeight() { + return weight; + } + + public BookProperties setWeight(BigDecimal weight) { + this.weight = weight; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeTest.java new file mode 100644 index 000000000..ff37005f5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonBinaryTypeTest.java @@ -0,0 +1,146 @@ +package com.vladmihalcea.hpjp.hibernate.type.json; + +import com.vladmihalcea.hpjp.hibernate.type.json.model.BaseEntity; +import com.vladmihalcea.hpjp.hibernate.type.json.model.Location; +import com.vladmihalcea.hpjp.hibernate.type.json.model.Ticket; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonBinaryTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Event.class, + Participant.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + Event nullEvent = new Event(); + nullEvent.setId(0L); + entityManager.persist(nullEvent); + + Location location = new Location(); + location.setCountry("Romania"); + location.setCity("Cluj-Napoca"); + + Event event = new Event(); + event.setId(1L); + event.setLocation(location); + entityManager.persist(event); + + Ticket ticket = new Ticket(); + ticket.setPrice(12.34d); + ticket.setRegistrationCode("ABC123"); + + Participant participant = new Participant(); + participant.setId(1L); + participant.setTicket(ticket); + participant.setEvent(event); + + entityManager.persist(participant); + }); + } + + @Test + public void test() { + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + assertEquals("Cluj-Napoca", event.getLocation().getCity()); + + Participant participant = entityManager.find(Participant.class, 1L); + assertEquals("ABC123", participant.getTicket().getRegistrationCode()); + + List participants = entityManager.createNativeQuery( + "select jsonb_pretty(p.ticket) " + + "from participant p " + + "where p.ticket ->> 'price' > :price") + .setParameter("price", "10") + .getResultList(); + + event.getLocation().setCity("Constanța"); + assertEquals(0, event.getVersion().intValue()); + entityManager.flush(); + assertEquals(1, event.getVersion().intValue()); + + assertEquals(1, participants.size()); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + event.getLocation().setCity(null); + assertEquals(1, event.getVersion().intValue()); + entityManager.flush(); + assertEquals(2, event.getVersion().intValue()); + }); + + doInJPA(entityManager -> { + Event event = entityManager.find(Event.class, 1L); + event.setLocation(null); + assertEquals(2, event.getVersion().intValue()); + entityManager.flush(); + assertEquals(3, event.getVersion().intValue()); + }); + } + + @Entity(name = "Event") + @Table(name = "event") + public static class Event extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private Location location; + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + } + + @Entity(name = "Participant") + @Table(name = "participant") + public static class Participant extends BaseEntity { + + @Type(JsonBinaryType.class) + @Column(columnDefinition = "jsonb") + private Ticket ticket; + + @ManyToOne + private Event event; + + public Ticket getTicket() { + return ticket; + } + + public void setTicket(Ticket ticket) { + this.ticket = ticket; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event event) { + this.event = event; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonDynamicUpdateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonDynamicUpdateTest.java new file mode 100644 index 000000000..373e8afa0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonDynamicUpdateTest.java @@ -0,0 +1,138 @@ +package com.vladmihalcea.hpjp.hibernate.type.json; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.json.JsonType; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonDynamicUpdateTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea") + .setProperties(""" + { + "publisher": "Amazon", + "price": 44.99, + "reviews": [ + { + "reviewer": "Cristiano", + "review": "Excellent book to understand Java Persistence", + "date": "2017-11-14", + "rating": 5 + }, + { + "reviewer": "T.W", + "review": "The best JPA ORM book out there", + "date": "2019-01-27", + "rating": 5 + }, + { + "reviewer": "Shaikh", + "review": "The most informative book", + "date": "2016-12-24", + "rating": 4 + } + ] + } + """) + ); + }); + } + + @Test + public void testFetchAndUpdateOtherAttribute() { + doInJPA(entityManager -> { + entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236") + .setTitle("High-Performance Java Persistence, 2nd edition"); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + @DynamicUpdate + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + private String title; + + private String author; + + @Column(columnDefinition = "jsonb") + @Type(JsonType.class) + private String properties; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonNodeBinaryTypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonNodeBinaryTypeTest.java new file mode 100644 index 000000000..192c5385e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonNodeBinaryTypeTest.java @@ -0,0 +1,190 @@ +package com.vladmihalcea.hpjp.hibernate.type.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.json.JsonNodeBinaryType; +import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonNodeBinaryTypeTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + + Book book = new Book(); + book.setIsbn("978-9730228236"); + book.setProperties(JacksonUtil.toJsonNode(""" + { + "title": "High-Performance Java Persistence", + "author": "Vlad Mihalcea", + "publisher": "Amazon", + "price": 44.99 + } + """ + )); + + entityManager.persist(book); + }); + } + + @Test + public void testFetchAndUpdate() { + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertEquals("High-Performance Java Persistence", book.getProperties().get("title").asText()); + + LOGGER.info("Book details: {}", book.getProperties()); + + book.setProperties(JacksonUtil.toJsonNode(""" + { + "title": "High-Performance Java Persistence", + "author": "Vlad Mihalcea", + "publisher": "Amazon", + "price": 44.99, + "url": "https://www.amazon.com/dp/973022823X/" + } + """ + )); + }); + } + + @Test + public void testFetchUsingJPQL() { + doInJPA(entityManager -> { + JsonNode properties = entityManager + .createQuery( + "select b.properties " + + "from Book b " + + "where b.isbn = :isbn", JsonNode.class) + .setParameter("isbn", "978-9730228236") + .getSingleResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + }); + } + + @Test + public void testUpdateUsingNativeSQL() { + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + assertNull(book.getProperties().get("reviews")); + + int updateCount = entityManager.createNativeQuery(""" + UPDATE + book + SET + properties = jsonb_set( + properties, + '{reviews}', + :reviews + ) + WHERE + isbn = :isbn + """) + .setParameter("isbn", "978-9730228236") + .unwrap(org.hibernate.query.Query.class) + .setParameter( + "reviews", + JacksonUtil.toJsonNode(""" + [ + { + "date":"2017-11-14", + "rating":5, + "review":"Excellent book to understand Java Persistence", + "reviewer":"Cristiano" + }, + { + "date":"2019-01-27", + "rating":5, + "review":"The best JPA ORM book out there", + "reviewer":"T.W" + }, + { + "date":"2016-12-24", + "rating":4, + "review":"The most informative book", + "reviewer":"Shaikh" + } + ] + """ + ), JsonNodeBinaryType.INSTANCE + ) + .executeUpdate(); + + entityManager.refresh(book); + + JsonNode reviews = book.getProperties().get("reviews"); + assertEquals(3, reviews.size()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonNodeBinaryType.class) + @Column(columnDefinition = "jsonb") + private JsonNode properties; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public JsonNode getProperties() { + return properties; + } + + public void setProperties(JsonNode properties) { + this.properties = properties; + } + } + + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonRecordTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonRecordTest.java new file mode 100644 index 000000000..094d38fc3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonRecordTest.java @@ -0,0 +1,151 @@ +package com.vladmihalcea.hpjp.hibernate.type.json; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.json.JsonType; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.junit.Test; + +import java.io.Serializable; +import java.net.URL; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonRecordTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + new BookRecord( + "High-Performance Java Persistence", + "Vlad Mihalcea", + "Amazon", + 4499L, + null + ) + ) + ); + }); + } + + @Test + public void testFetchAndUpdate() { + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + BookRecord bookRecord = book.getProperties(); + + assertEquals( + "High-Performance Java Persistence", + bookRecord.title() + ); + assertEquals( + "Vlad Mihalcea", + bookRecord.author() + ); + + LOGGER.info("Book details: {}", book.getProperties()); + + book.setProperties( + new BookRecord( + bookRecord.title(), + bookRecord.author(), + bookRecord.publisher(), + bookRecord.priceInCents(), + urlValue("https://www.amazon.com/dp/973022823X/") + ) + ); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "jsonb") + private BookRecord properties; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public BookRecord getProperties() { + return properties; + } + + public Book setProperties(BookRecord properties) { + this.properties = properties; + return this; + } + } + + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) + public static record BookRecord ( + String title, + String author, + String publisher, + Long priceInCents, + URL url + ) implements Serializable { + @JsonCreator + public BookRecord( + @JsonProperty("title") String title, + @JsonProperty("author") String author, + @JsonProperty("publisher") String publisher, + @JsonProperty("priceInCents") String priceInCents, + @JsonProperty("url") String url) { + this( + title, + author, + publisher, + longValue(priceInCents), + urlValue(url) + ); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/json/PostgreSQLJsonStringTypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonStringTypeTest.java similarity index 84% rename from core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/json/PostgreSQLJsonStringTypeTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonStringTypeTest.java index aaf497138..9a4a95af6 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/hibernate/type/json/PostgreSQLJsonStringTypeTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonStringTypeTest.java @@ -1,13 +1,17 @@ -package com.vladmihalcea.book.hpjp.hibernate.type.json; - -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.BaseEntity; -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.Location; -import com.vladmihalcea.book.hpjp.hibernate.type.json.model.Ticket; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +package com.vladmihalcea.hpjp.hibernate.type.json; + +import com.vladmihalcea.hpjp.hibernate.type.json.model.BaseEntity; +import com.vladmihalcea.hpjp.hibernate.type.json.model.Location; +import com.vladmihalcea.hpjp.hibernate.type.json.model.Ticket; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import org.hibernate.annotations.Type; import org.junit.Test; -import javax.persistence.*; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -83,7 +87,7 @@ public void test() { @Table(name = "event") public static class Event extends BaseEntity { - @Type(type = "jsonb") + @Type(JsonBinaryType.class) @Column(columnDefinition = "json") private Location location; @@ -100,7 +104,7 @@ public void setLocation(Location location) { @Table(name = "participant") public static class Participant extends BaseEntity { - @Type(type = "jsonb") + @Type(JsonBinaryType.class) @Column(columnDefinition = "json") private Ticket ticket; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonTypeRegistryTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonTypeRegistryTest.java new file mode 100644 index 000000000..a0d5c4bab --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLJsonTypeRegistryTest.java @@ -0,0 +1,114 @@ +package com.vladmihalcea.hpjp.hibernate.type.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import io.hypersistence.utils.hibernate.type.json.JsonStringType; +import io.hypersistence.utils.hibernate.type.json.JsonType; +import jakarta.persistence.*; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.jpa.boot.spi.TypeContributorList; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import java.util.Collections; +import java.util.Properties; + +import static org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.TYPE_CONTRIBUTORS; +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLJsonTypeRegistryTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties(""" + { + "title": "High-Performance Java Persistence", + "author": "Vlad Mihalcea", + "publisher": "Amazon", + "price": 44.99 + } + """ + ) + ); + }); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put(TYPE_CONTRIBUTORS, + (TypeContributorList) () -> Collections.singletonList( + (typeContributions, serviceRegistry) -> { + typeContributions.contributeType(new JsonStringType(JsonNode.class)); + } + ) + ); + } + + @Test + public void test() { + doInJPA(entityManager -> { + JsonNode properties = (JsonNode) entityManager.createNativeQuery(""" + SELECT + properties AS properties + FROM book + WHERE + isbn = :isbn + """) + .setParameter("isbn", "978-9730228236") + .unwrap(NativeQuery.class) + .addScalar("properties", JsonNode.class) + .getSingleResult(); + + assertEquals("High-Performance Java Persistence", properties.get("title").asText()); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @Type(JsonType.class) + @Column(columnDefinition = "jsonb") + private String properties; + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public String getProperties() { + return properties; + } + + public Book setProperties(String properties) { + this.properties = properties; + return this; + } + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLNativeJsonMapTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLNativeJsonMapTest.java new file mode 100644 index 000000000..fa02e3e29 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/PostgreSQLNativeJsonMapTest.java @@ -0,0 +1,101 @@ +package com.vladmihalcea.hpjp.hibernate.type.json; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.annotations.NaturalId; +import org.hibernate.type.SqlTypes; +import org.junit.Test; + +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLNativeJsonMapTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Book.class + }; + } + + @Override + protected void afterInit() { + doInJPA(entityManager -> { + entityManager.persist( + new Book() + .setIsbn("978-9730228236") + .setProperties( + Map.of( + "publisher", "Amazon", + "price", "44.99", + "publication_date", "2016-20-12", + "dimensions", "8.5 x 1.1 x 11 inches", + "weight", "2.5 pounds", + "average_review", "4.7 out of 5 stars" + ) + ) + ); + }); + } + + @Test + public void testFetchAndUpdate() { + + doInJPA(entityManager -> { + Book book = entityManager + .unwrap(Session.class) + .bySimpleNaturalId(Book.class) + .load("978-9730228236"); + + Map bookRecord = book.getProperties(); + bookRecord.put("url", "https://amzn.com/973022823X"); + }); + } + + @Entity(name = "Book") + @Table(name = "book") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String isbn; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private Map properties; + + public Long getId() { + return id; + } + + public Book setId(Long id) { + this.id = id; + return this; + } + + public String getIsbn() { + return isbn; + } + + public Book setIsbn(String isbn) { + this.isbn = isbn; + return this; + } + + public Map getProperties() { + return properties; + } + + public Book setProperties(Map properties) { + this.properties = properties; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/sql/DefaultPostgreSQLJsonNodeBinaryTypeFetchTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/sql/DefaultPostgreSQLJsonNodeBinaryTypeFetchTest.java new file mode 100644 index 000000000..ebc37117f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/sql/DefaultPostgreSQLJsonNodeBinaryTypeFetchTest.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.hpjp.hibernate.type.json.sql; + +import com.vladmihalcea.hpjp.hibernate.type.json.PostgreSQLJsonNodeBinaryTypeTest; +import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil; +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class DefaultPostgreSQLJsonNodeBinaryTypeFetchTest extends PostgreSQLJsonNodeBinaryTypeTest { + + @Test + public void testFetchJsonPropertyUsingNativeSQL() { + doInJPA(entityManager -> { + String properties = (String) entityManager.createNativeQuery(""" + SELECT properties + FROM book + WHERE isbn = :isbn + """) + .setParameter("isbn", "978-9730228236") + .getSingleResult(); + + assertEquals( + "High-Performance Java Persistence", + JacksonUtil.fromString(properties, Map.class).get("title") + ); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/sql/NativeQueryScalarJsonNodeBinaryTypeFetchTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/sql/NativeQueryScalarJsonNodeBinaryTypeFetchTest.java new file mode 100644 index 000000000..b1fa6b1b3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/json/sql/NativeQueryScalarJsonNodeBinaryTypeFetchTest.java @@ -0,0 +1,57 @@ +package com.vladmihalcea.hpjp.hibernate.type.json.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vladmihalcea.hpjp.hibernate.type.json.PostgreSQLJsonNodeBinaryTypeTest; +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; +import io.hypersistence.utils.hibernate.type.json.JsonNodeBinaryType; +import org.hibernate.boot.spi.MetadataBuilderContributor; +import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; +import org.hibernate.jpa.boot.spi.TypeContributorList; +import org.junit.Test; + +import java.util.Collections; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class NativeQueryScalarJsonNodeBinaryTypeFetchTest extends PostgreSQLJsonNodeBinaryTypeTest { + + @Override + protected void additionalProperties(Properties properties) { + properties.put( + EntityManagerFactoryBuilderImpl.METADATA_BUILDER_CONTRIBUTOR, + (MetadataBuilderContributor) metadataBuilder -> metadataBuilder.applyBasicType( + JsonNodeBinaryType.INSTANCE + ) + ); + properties.put("hibernate.type_contributors", + (TypeContributorList) () -> Collections.singletonList( + (typeContributions, serviceRegistry) -> { + typeContributions.contributeType(new JsonBinaryType(JsonNode.class)); + } + )); + } + + @Test + public void testFetchJsonProperty() { + doInJPA(entityManager -> { + JsonNode properties = (JsonNode) entityManager + .createNativeQuery( + "SELECT properties " + + "FROM book " + + "WHERE isbn = :isbn") + .setParameter("isbn", "978-9730228236") + .unwrap(org.hibernate.query.NativeQuery.class) + .addScalar("properties", JsonNode.class) + .getSingleResult(); + + assertEquals( + "High-Performance Java Persistence", + properties.get("title").asText() + ); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/money/MonetaryAmountTypeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/money/MonetaryAmountTypeTest.java new file mode 100644 index 000000000..0d30dc8c5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/type/money/MonetaryAmountTypeTest.java @@ -0,0 +1,215 @@ +package com.vladmihalcea.hpjp.hibernate.type.money; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import io.hypersistence.utils.hibernate.type.money.MonetaryAmountType; +import org.hibernate.annotations.Columns; +import org.hibernate.annotations.CompositeType; +import org.javamoney.moneta.Money; +import org.junit.Test; + +import javax.money.MonetaryAmount; +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class MonetaryAmountTypeTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Product.class, + ProductPricing.class + }; + } + + @Override + protected Database database() { + return Database.MYSQL; + } + + @Test + public void test() { + doInJPA(entityManager -> { + entityManager.persist( + new Product() + .setId(1L) + .setName("Hypersistence Optimizer") + .addPricingPlan( + new ProductPricing() + .setName("Individual License") + .setType(PricingType.SUBSCRIPTION) + .setPrice( + Money.of( + new BigDecimal("49.0"), + "USD" + ) + ) + ) + .addPricingPlan( + new ProductPricing() + .setName("5-Year Individual License") + .setType(PricingType.ONE_TIME_PURCHASE) + .setPrice( + Money.of( + new BigDecimal("199.0"), + "USD" + ) + ) + ) + .addPricingPlan( + new ProductPricing() + .setName("10-Dev Group License") + .setType(PricingType.SUBSCRIPTION) + .setPrice( + Money.of( + new BigDecimal("349.0"), + "USD" + ) + ) + ) + ); + }); + + doInJPA(entityManager -> { + ProductPricing pricing = entityManager.createQuery(""" + select pp + from ProductPricing pp + where + pp.product.id = :productId and + pp.name = :name + """, ProductPricing.class) + .setParameter("productId", 1L) + .setParameter("name", "Individual License") + .getSingleResult(); + + assertEquals(pricing.getPrice().getNumber().longValue(), 49); + assertEquals(pricing.getPrice().getCurrency().getCurrencyCode(), "USD"); + }); + } + + @Entity(name = "Product") + @Table(name = "product") + public static class Product { + + @Id + private Long id; + + private String name; + + @OneToMany( + mappedBy = "product", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List pricingPlans = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Product setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Product setName(String name) { + this.name = name; + return this; + } + + public List getPricingPlans() { + return pricingPlans; + } + + public Product addPricingPlan(ProductPricing pricingPlan) { + pricingPlans.add(pricingPlan); + pricingPlan.setProduct(this); + return this; + } + } + + @Entity(name = "ProductPricing") + @Table(name = "product_pricing") + public static class ProductPricing { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Product product; + + private String name; + + @Enumerated + private PricingType type; + + @Columns(columns = { + @Column(name = "price_amount"), + @Column(name = "price_currency") + }) + @CompositeType(MonetaryAmountType.class) + private MonetaryAmount price; + + public Long getId() { + return id; + } + + public ProductPricing setId(Long id) { + this.id = id; + return this; + } + + public Product getProduct() { + return product; + } + + public ProductPricing setProduct(Product product) { + this.product = product; + return this; + } + + public String getName() { + return name; + } + + public ProductPricing setName(String name) { + this.name = name; + return this; + } + + public PricingType getType() { + return type; + } + + public ProductPricing setType(PricingType type) { + this.type = type; + return this; + } + + public MonetaryAmount getPrice() { + return price; + } + + public ProductPricing setPrice(MonetaryAmount salary) { + this.price = salary; + return this; + } + } + + public enum PricingType { + ONE_TIME_PURCHASE, + SUBSCRIPTION + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/view/SubselectTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/view/SubselectTest.java new file mode 100644 index 000000000..3dc846d31 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/view/SubselectTest.java @@ -0,0 +1,133 @@ +package com.vladmihalcea.hpjp.hibernate.view; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.Immutable; +import org.hibernate.annotations.Subselect; +import org.junit.Test; + +import java.util.List; + +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.*; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SubselectTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + + return new Class[]{ + Post.class, + PostDetails.class, + PostComment.class, + Tag.class, + DatabaseFunction.class + }; + } + + @Entity(name = "DatabaseFunction") + @Immutable + @Subselect(""" + SELECT + functions.routine_name as name, + string_agg(functions.data_type, ',') as params + FROM ( + SELECT + routines.routine_name, + parameters.data_type, + parameters.ordinal_position + FROM + information_schema.routines + LEFT JOIN + information_schema.parameters + ON + routines.specific_name = parameters.specific_name + WHERE + routines.specific_schema='public' + ORDER BY + routines.routine_name, + parameters.ordinal_position + ) AS functions + GROUP BY functions.routine_name + """ + ) + public static class DatabaseFunction { + + @Id + private String name; + + private String params; + + public String getName() { + return name; + } + + public String[] getParams() { + return params.split(","); + } + } + + public void beforeInit() { + executeStatement("DROP FUNCTION IF EXISTS fn_count_comments(bigint)"); + executeStatement("DROP FUNCTION IF EXISTS fn_post_comments(bigint)"); + executeStatement(""" + CREATE OR REPLACE FUNCTION fn_count_comments( + IN postId bigint, + OUT commentCount bigint) + RETURNS bigint AS + $BODY$ + BEGIN + SELECT COUNT(*) INTO commentCount + FROM post_comment + WHERE post_id = postId; + END; + $BODY$ + LANGUAGE plpgsql; + """); + executeStatement(""" + CREATE OR REPLACE FUNCTION fn_post_comments(postId BIGINT) + RETURNS REFCURSOR AS + $BODY$ + DECLARE + postComments REFCURSOR; + BEGIN + OPEN postComments FOR + SELECT * + FROM post_comment + WHERE post_id = postId; + RETURN postComments; + END; + $BODY$ + LANGUAGE plpgsql + """ + ); + } + + @Test + public void test() { + doInJPA(entityManager -> { + List databaseFunctions = entityManager.createQuery(""" + select df + from DatabaseFunction df + where df.name like 'fn_%comments' + order by df.name + """, DatabaseFunction.class) + .getResultList(); + + DatabaseFunction countComments = databaseFunctions.get(0); + assertEquals("fn_count_comments", countComments.getName()); + assertEquals(2, countComments.getParams().length); + assertEquals("bigint", countComments.getParams()[0]); + + DatabaseFunction postComments = databaseFunctions.get(1); + assertEquals("fn_post_comments", postComments.getName()); + assertEquals(1, postComments.getParams().length); + assertEquals("bigint", postComments.getParams()[0]); + }); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/hibernate/view/ViewTest.java b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/view/ViewTest.java new file mode 100644 index 000000000..c4688a305 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/hibernate/view/ViewTest.java @@ -0,0 +1,134 @@ +package com.vladmihalcea.hpjp.hibernate.view; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Immutable; +import org.junit.Test; + +import java.util.List; +import java.util.Properties; + +import static com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.*; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class ViewTest extends AbstractPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostDetails.class, + PostComment.class, + Tag.class, + DatabaseFunction.class + }; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.hbm2ddl.auto", "none"); + } + + @Entity(name = "DatabaseFunction") + @Immutable + @Table(name = "database_functions") + public static class DatabaseFunction { + + @Id + private String name; + + private String params; + + public String getName() { + return name; + } + + public String[] getParams() { + return params.split(","); + } + } + + @Override + protected void beforeInit() { + executeStatement("DROP FUNCTION IF EXISTS fn_count_comments(bigint)"); + executeStatement("DROP FUNCTION IF EXISTS fn_post_comments(bigint)"); + executeStatement(""" + CREATE OR REPLACE FUNCTION fn_count_comments( + IN postId bigint, + OUT commentCount bigint) + RETURNS bigint AS + $BODY$ + BEGIN + SELECT COUNT(*) INTO commentCount + FROM post_comment + WHERE post_id = postId; + END; + $BODY$ + LANGUAGE plpgsql; + """); + executeStatement(""" + CREATE OR REPLACE FUNCTION fn_post_comments(postId BIGINT) + RETURNS REFCURSOR AS + $BODY$ + DECLARE + postComments REFCURSOR; + BEGIN + OPEN postComments FOR + SELECT * + FROM post_comment + WHERE post_id = postId; + RETURN postComments; + END; + $BODY$ + LANGUAGE plpgsql + """); + executeStatement(""" + CREATE OR REPLACE VIEW database_functions AS + SELECT + functions.routine_name as name, + string_agg(functions.data_type, ',') as params + FROM ( + SELECT + routines.routine_name, + parameters.data_type, + parameters.ordinal_position + FROM + information_schema.routines + LEFT JOIN + information_schema.parameters ON + routines.specific_name = parameters.specific_name + WHERE + routines.specific_schema='public' and + routines.routine_name LIKE 'fn_%_comments' + ORDER BY routines.routine_name, parameters.ordinal_position + ) AS functions + GROUP BY functions.routine_name + """); + } + + @Test + public void testStoredProcedureOutParameter() { + doInJPA(entityManager -> { + List databaseFunctions = entityManager.createQuery(""" + SELECT df + FROM DatabaseFunction df + """, DatabaseFunction.class) + .getResultList(); + + DatabaseFunction countComments = databaseFunctions.get(0); + assertEquals("fn_count_comments", countComments.getName()); + assertEquals(2, countComments.getParams().length); + assertEquals("bigint", countComments.getParams()[0]); + + DatabaseFunction postComments = databaseFunctions.get(1); + assertEquals("fn_post_comments", postComments.getName()); + assertEquals(1, postComments.getParams().length); + assertEquals("bigint", postComments.getParams()[0]); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/AbstractBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/AbstractBatchPreparedStatementTest.java similarity index 75% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/AbstractBatchPreparedStatementTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/AbstractBatchPreparedStatementTest.java index b4a665fd9..412270057 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/AbstractBatchPreparedStatementTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/AbstractBatchPreparedStatementTest.java @@ -1,9 +1,8 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch; - -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; +package com.vladmihalcea.hpjp.jdbc.batch; +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; import org.junit.Test; import java.sql.Connection; @@ -18,12 +17,12 @@ * * @author Vlad Mihalcea */ -public abstract class AbstractBatchPreparedStatementTest extends DataSourceProviderIntegrationTest { +public abstract class AbstractBatchPreparedStatementTest extends DatabaseProviderIntegrationTest { private BlogEntityProvider entityProvider = new BlogEntityProvider(); - public AbstractBatchPreparedStatementTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); + public AbstractBatchPreparedStatementTest(Database database) { + super(database); } @Override @@ -33,6 +32,9 @@ protected Class[] entities() { @Test public void testBatch() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } doInJDBC(connection -> { batchInsert(connection); batchUpdate(connection); @@ -45,7 +47,7 @@ public void testBatch() { private void executeStatement(PreparedStatement statement, AtomicInteger statementCount) throws SQLException { onStatement(statement); int count = statementCount.incrementAndGet(); - if(count % getBatchSize() == 0) { + if (count % getBatchSize() == 0) { onFlush(statement); } } @@ -104,10 +106,10 @@ protected void batchInsert(Connection connection) throws SQLException { onEnd(postCommentStatement); LOGGER.info("{}.testInsert for {} using batch size {} took {} millis", - getClass().getSimpleName(), - dataSourceProvider().getClass().getSimpleName(), - getBatchSize(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + getClass().getSimpleName(), + dataSourceProvider().getClass().getSimpleName(), + getBatchSize(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); } } @@ -117,9 +119,9 @@ protected void batchUpdate(Connection connection) throws SQLException { AtomicInteger postCommentStatementCount = new AtomicInteger(); try ( - PreparedStatement postStatement = connection.prepareStatement("update Post set version = ? where id = ?"); - PreparedStatement postCommentStatement = connection.prepareStatement("update post_comment set version = ? where id = ?"); - Statement bulkUpdateStatement = connection.createStatement(); + PreparedStatement postStatement = connection.prepareStatement("update Post set version = ? where id = ?"); + PreparedStatement postCommentStatement = connection.prepareStatement("update post_comment set version = ? where id = ?"); + Statement bulkUpdateStatement = connection.createStatement(); ) { int postCount = getPostCount(); int postCommentCount = getPostCommentCount(); @@ -150,18 +152,18 @@ protected void batchUpdate(Connection connection) throws SQLException { onEnd(postCommentStatement); LOGGER.info("{}.testUpdate for {} took {} millis", - getClass().getSimpleName(), - dataSourceProvider().getClass().getSimpleName(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + getClass().getSimpleName(), + dataSourceProvider().getClass().getSimpleName(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); LOGGER.info("Test bulk update"); startNanos = System.nanoTime(); bulkUpdateStatement.executeUpdate("update Post set version = version + 1"); bulkUpdateStatement.executeUpdate("update post_comment set version = version + 1"); LOGGER.info("{}.testBulkUpdate for {} took {} millis", - getClass().getSimpleName(), - dataSourceProvider().getClass().getSimpleName(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + getClass().getSimpleName(), + dataSourceProvider().getClass().getSimpleName(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); } } @@ -170,9 +172,9 @@ protected void batchDelete(Connection connection) throws SQLException { AtomicInteger postCommentStatementCount = new AtomicInteger(); try ( - PreparedStatement postStatement = connection.prepareStatement("delete from Post where id = ?"); - PreparedStatement postCommentStatement = connection.prepareStatement("delete from post_comment where id = ?"); - Statement bulkUpdateStatement = connection.createStatement(); + PreparedStatement postStatement = connection.prepareStatement("delete from Post where id = ?"); + PreparedStatement postCommentStatement = connection.prepareStatement("delete from post_comment where id = ?"); + Statement bulkUpdateStatement = connection.createStatement(); ) { int postCount = getPostCount(); int postCommentCount = getPostCommentCount(); @@ -202,9 +204,9 @@ protected void batchDelete(Connection connection) throws SQLException { onEnd(postStatement); LOGGER.info("{}.testDelete for {} took {} millis", - getClass().getSimpleName(), - dataSourceProvider().getClass().getSimpleName(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + getClass().getSimpleName(), + dataSourceProvider().getClass().getSimpleName(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); batchInsert(connection); @@ -213,9 +215,9 @@ protected void batchDelete(Connection connection) throws SQLException { bulkUpdateStatement.executeUpdate("delete from post_comment where version > 0"); bulkUpdateStatement.executeUpdate("delete from Post where version > 0"); LOGGER.info("{}.testBulkDelete for {} took {} millis", - getClass().getSimpleName(), - dataSourceProvider().getClass().getSimpleName(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + getClass().getSimpleName(), + dataSourceProvider().getClass().getSimpleName(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); } } diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/AbstractBatchStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/AbstractBatchStatementTest.java similarity index 82% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/AbstractBatchStatementTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/AbstractBatchStatementTest.java index 7c49e3112..c821c1a4e 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/AbstractBatchStatementTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/AbstractBatchStatementTest.java @@ -1,9 +1,8 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch; - -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; +package com.vladmihalcea.hpjp.jdbc.batch; +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; import org.junit.Test; import java.sql.SQLException; @@ -18,7 +17,7 @@ * * @author Vlad Mihalcea */ -public abstract class AbstractBatchStatementTest extends DataSourceProviderIntegrationTest { +public abstract class AbstractBatchStatementTest extends DatabaseProviderIntegrationTest { public static final String INSERT_POST = "insert into post (title, version, id) values ('Post no. %1$d', 0, %1$d)"; @@ -26,8 +25,8 @@ public abstract class AbstractBatchStatementTest extends DataSourceProviderInteg private final BlogEntityProvider entityProvider = new BlogEntityProvider(); - public AbstractBatchStatementTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); + public AbstractBatchStatementTest(Database database) { + super(database); } @Override @@ -37,6 +36,9 @@ protected Class[] entities() { @Test public void testInsert() { + if (!ENABLE_LONG_RUNNING_TESTS) { + return; + } LOGGER.info("Test batch insert"); AtomicInteger statementCount = new AtomicInteger(); long startNanos = System.nanoTime(); @@ -71,9 +73,9 @@ public void testInsert() { } }); LOGGER.info("{}.testInsert for {} took {} millis", - getClass().getSimpleName(), - dataSourceProvider().getClass().getSimpleName(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + getClass().getSimpleName(), + dataSourceProvider().getClass().getSimpleName(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); } protected abstract void onFlush(Statement statement) throws SQLException; @@ -81,7 +83,7 @@ public void testInsert() { private void executeStatement(Statement statement, String dml, AtomicInteger statementCount) throws SQLException { onStatement(statement, dml); int count = statementCount.incrementAndGet(); - if(count % getBatchSize() == 0) { + if (count % getBatchSize() == 0) { onFlush(statement); } } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/BatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/BatchPreparedStatementTest.java new file mode 100644 index 000000000..ca14e2e0c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/BatchPreparedStatementTest.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.jdbc.batch; + +import com.vladmihalcea.hpjp.util.providers.Database; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * BatchPreparedStatementTest - Test batching with PreparedStatements + * + * @author Vlad Mihalcea + */ +public class BatchPreparedStatementTest extends AbstractBatchPreparedStatementTest { + + public BatchPreparedStatementTest(Database database) { + super(database); + } + + @Override + protected void onStatement(PreparedStatement statement) throws SQLException { + statement.addBatch(); + } + + @Override + protected void onEnd(PreparedStatement statement) throws SQLException { + int[] updateCount = statement.executeBatch(); + statement.clearBatch(); + } + + @Override + protected void onFlush(PreparedStatement statement) throws SQLException { + statement.executeBatch(); + } + + @Override + protected int getBatchSize() { + return 100 * 10; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/BatchStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/BatchStatementTest.java new file mode 100644 index 000000000..70c9d98bb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/BatchStatementTest.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.jdbc.batch; + +import com.vladmihalcea.hpjp.util.providers.Database; + +import java.sql.SQLException; +import java.sql.Statement; + +/** + * BatchStatementTest - Test batching with Statements + * + * @author Vlad Mihalcea + */ +public class BatchStatementTest extends AbstractBatchStatementTest { + + public BatchStatementTest(Database database) { + super(database); + } + + @Override + protected void onStatement(Statement statement, String dml) throws SQLException { + statement.addBatch(dml); + } + + @Override + protected void onEnd(Statement statement) throws SQLException { + int[] updateCount = statement.executeBatch(); + statement.clearBatch(); + } + + @Override + protected void onFlush(Statement statement) throws SQLException { + statement.executeBatch(); + } + + @Override + protected int getBatchSize() { + return 100; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/MySQLBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/MySQLBatchPreparedStatementTest.java new file mode 100644 index 000000000..ecf545ff0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/MySQLBatchPreparedStatementTest.java @@ -0,0 +1,133 @@ +package com.vladmihalcea.hpjp.jdbc.batch; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.fail; + +/** + * MySqlBatchStatementTest - Test MySQl JDBC Statement batching with and w/o rewriteBatchedStatements + * + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public class MySQLBatchPreparedStatementTest extends AbstractMySQLIntegrationTest { + + private final BlogEntityProvider entityProvider = new BlogEntityProvider(); + + private boolean cachePrepStmts; + + private boolean useServerPrepStmts; + + public MySQLBatchPreparedStatementTest(boolean cachePrepStmts, boolean useServerPrepStmts) { + this.cachePrepStmts = cachePrepStmts; + this.useServerPrepStmts = useServerPrepStmts; + } + + @Parameterized.Parameters + public static Collection rdbmsDataSourceProvider() { + List providers = new ArrayList<>(); + providers.add(new Boolean[]{Boolean.FALSE, Boolean.FALSE}); + providers.add(new Boolean[]{Boolean.FALSE, Boolean.TRUE}); + providers.add(new Boolean[]{Boolean.TRUE, Boolean.FALSE}); + providers.add(new Boolean[]{Boolean.TRUE, Boolean.TRUE}); + return providers; + } + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + MySQLDataSourceProvider dataSourceProvider = (MySQLDataSourceProvider) super.dataSourceProvider(); + dataSourceProvider.setCachePrepStmts(cachePrepStmts); + dataSourceProvider.setUseServerPrepStmts(useServerPrepStmts); + return dataSourceProvider; + } + + @Test + public void testInsert() { + if (!ENABLE_LONG_RUNNING_TESTS) { + return; + } + LOGGER.info("Test MySQL batch insert with cachePrepStmts={}, useServerPrepStmts={}", cachePrepStmts, useServerPrepStmts); + AtomicInteger statementCount = new AtomicInteger(); + long startNanos = System.nanoTime(); + doInJDBC(connection -> { + AtomicInteger postStatementCount = new AtomicInteger(); + AtomicInteger postCommentStatementCount = new AtomicInteger(); + + try (PreparedStatement postStatement = connection.prepareStatement("insert into post (title, version, id) values (?, ?, ?)"); + PreparedStatement postCommentStatement = connection.prepareStatement("insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"); + ) { + int postCount = getPostCount(); + int postCommentCount = getPostCommentCount(); + + for (int i = 0; i < postCount; i++) { + int index = 0; + + postStatement.setString(++index, String.format("Post no. %1$d", i)); + postStatement.setInt(++index, 0); + postStatement.setLong(++index, i); + executeStatement(postStatement, postStatementCount); + } + postStatement.executeBatch(); + + for (int i = 0; i < postCount; i++) { + for (int j = 0; j < postCommentCount; j++) { + int index = 0; + + postCommentStatement.setLong(++index, i); + postCommentStatement.setString(++index, String.format("Post comment %1$d", j)); + postCommentStatement.setInt(++index, 0); + postCommentStatement.setLong(++index, (postCommentCount * i) + j); + executeStatement(postCommentStatement, postCommentStatementCount); + } + } + postCommentStatement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + LOGGER.info("{}.testInsert for cachePrepStmts={}, useServerPrepStmts={} took {} millis", + getClass().getSimpleName(), + cachePrepStmts, + useServerPrepStmts, + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + } + + private void executeStatement(PreparedStatement statement, AtomicInteger statementCount) throws SQLException { + statement.addBatch(); + int count = statementCount.incrementAndGet(); + if (count % getBatchSize() == 0) { + statement.executeBatch(); + } + } + + protected int getPostCount() { + return 5000; + } + + protected int getPostCommentCount() { + return 1; + } + + protected int getBatchSize() { + return 100 * 10; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/MySQLBatchStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/MySQLBatchStatementTest.java new file mode 100644 index 000000000..79383cb0d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/MySQLBatchStatementTest.java @@ -0,0 +1,114 @@ +package com.vladmihalcea.hpjp.jdbc.batch; + +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.fail; + +/** + * MySqlBatchStatementTest - Test MySQl JDBC Statement batching with and w/o rewriteBatchedStatements + * + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public class MySQLBatchStatementTest extends AbstractMySQLIntegrationTest { + + public static final String INSERT_POST = "insert into post (title, version, id) values ('Post no. %1$d', 0, %1$d)"; + + public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (%1$d, 'Post comment %2$d', 0, %2$d)"; + + private final BlogEntityProvider entityProvider = new BlogEntityProvider(); + + private boolean rewriteBatchedStatements; + + public MySQLBatchStatementTest(boolean rewriteBatchedStatements) { + this.rewriteBatchedStatements = rewriteBatchedStatements; + } + + @Parameterized.Parameters + public static Collection rdbmsDataSourceProvider() { + List providers = new ArrayList<>(); + providers.add(new Boolean[]{Boolean.FALSE}); + providers.add(new Boolean[]{Boolean.TRUE}); + return providers; + } + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + MySQLDataSourceProvider dataSourceProvider = (MySQLDataSourceProvider) super.dataSourceProvider(); + dataSourceProvider.setRewriteBatchedStatements(rewriteBatchedStatements); + return dataSourceProvider; + } + + @Test + public void testInsert() { + if (!ENABLE_LONG_RUNNING_TESTS) { + return; + } + LOGGER.info("Test MySQL batch insert with rewriteBatchedStatements={}", rewriteBatchedStatements); + AtomicInteger statementCount = new AtomicInteger(); + long startNanos = System.nanoTime(); + doInJDBC(connection -> { + try (Statement statement = connection.createStatement()) { + int postCount = getPostCount(); + int postCommentCount = getPostCommentCount(); + + for (int i = 0; i < postCount; i++) { + executeStatement(statement, String.format(INSERT_POST, i), statementCount); + } + statement.executeBatch(); + + for (int i = 0; i < postCount; i++) { + for (int j = 0; j < postCommentCount; j++) { + executeStatement(statement, String.format(INSERT_POST_COMMENT, i, (postCommentCount * i) + j), statementCount); + } + } + statement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + LOGGER.info("{}.testInsert for rewriteBatchedStatements={} took {} millis", + getClass().getSimpleName(), + rewriteBatchedStatements, + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + } + + private void executeStatement(Statement statement, String dml, AtomicInteger statementCount) throws SQLException { + statement.addBatch(dml); + int count = statementCount.incrementAndGet(); + if (count % getBatchSize() == 0) { + statement.executeBatch(); + } + } + + protected int getPostCount() { + return 1000; + } + + protected int getPostCommentCount() { + return 4; + } + + protected int getBatchSize() { + return 100 * 10; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/MySQLRewriteBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/MySQLRewriteBatchPreparedStatementTest.java new file mode 100644 index 000000000..d41a8ce8f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/MySQLRewriteBatchPreparedStatementTest.java @@ -0,0 +1,131 @@ +package com.vladmihalcea.hpjp.jdbc.batch; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.fail; + +/** + * MySqlRewriteBatchPreparedStatementTest - Test MySQl JDBC Statement batching with and w/o rewriteBatchedStatements + * + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public class MySQLRewriteBatchPreparedStatementTest extends AbstractMySQLIntegrationTest { + + private final BlogEntityProvider entityProvider = new BlogEntityProvider(); + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .build(); + + private Timer timer = metricRegistry.timer("batchInsertTimer"); + + private boolean rewriteBatchedStatements; + + public MySQLRewriteBatchPreparedStatementTest(boolean rewriteBatchedStatements) { + this.rewriteBatchedStatements = rewriteBatchedStatements; + } + + @Parameterized.Parameters + public static Collection rdbmsDataSourceProvider() { + List providers = new ArrayList<>(); + providers.add(new Boolean[]{Boolean.FALSE}); + providers.add(new Boolean[]{Boolean.TRUE}); + return providers; + } + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + MySQLDataSourceProvider dataSourceProvider = (MySQLDataSourceProvider) super.dataSourceProvider(); + dataSourceProvider.setRewriteBatchedStatements(rewriteBatchedStatements); + return dataSourceProvider; + } + + @Test + public void testInsert() { + if (!ENABLE_LONG_RUNNING_TESTS) { + return; + } + long ttlMillis = System.currentTimeMillis() + getRunMillis(); + + final AtomicInteger postIdHolder = new AtomicInteger(); + + while (System.currentTimeMillis() < ttlMillis) { + doInJDBC(connection -> { + long startNanos = System.nanoTime(); + + AtomicInteger postStatementCount = new AtomicInteger(); + + try (PreparedStatement postStatement = connection.prepareStatement("insert into post (title, version, id) values (?, ?, ?)")) { + int postCount = getPostCount(); + + for (int i = 0; i < postCount; i++) { + int index = 0; + int postId = postIdHolder.incrementAndGet(); + postStatement.setString(++index, String.format("Post no. %1$d", postId)); + postStatement.setInt(++index, 0); + postStatement.setLong(++index, postId); + executeStatement(postStatement, postStatementCount); + } + postStatement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + }); + } + LOGGER.info("Test MySQL batch insert with rewriteBatchedStatements={}", rewriteBatchedStatements); + logReporter.report(); + LOGGER.info( + "Test MySQL batch insert with rewriteBatchedStatements={} took=[{}] ms", + rewriteBatchedStatements, + timer.getSnapshot().get99thPercentile() + ); + } + + private void executeStatement(PreparedStatement statement, AtomicInteger statementCount) throws SQLException { + statement.addBatch(); + int count = statementCount.incrementAndGet(); + if (count % getBatchSize() == 0) { + statement.executeBatch(); + } + } + + protected int getPostCount() { + return 5000; + } + + protected int getBatchSize() { + return 100 * 10; + } + + protected int getRunMillis() { + return 60 * 1000; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/NoBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/NoBatchPreparedStatementTest.java new file mode 100644 index 000000000..a03a0b85d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/NoBatchPreparedStatementTest.java @@ -0,0 +1,32 @@ +package com.vladmihalcea.hpjp.jdbc.batch; + +import com.vladmihalcea.hpjp.util.providers.Database; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * NoBatchPreparedStatementTest - Test without batching PreparedStatements + * + * @author Vlad Mihalcea + */ +public class NoBatchPreparedStatementTest extends AbstractBatchPreparedStatementTest { + + public NoBatchPreparedStatementTest(Database database) { + super(database); + } + + @Override + protected void onStatement(PreparedStatement statement) throws SQLException { + statement.executeUpdate(); + } + + @Override + protected void onEnd(PreparedStatement statement) throws SQLException { + } + + @Override + protected void onFlush(PreparedStatement statement) throws SQLException { + + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/NoBatchStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/NoBatchStatementTest.java new file mode 100644 index 000000000..1e1a1ac7c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/NoBatchStatementTest.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.jdbc.batch; + +import com.vladmihalcea.hpjp.util.providers.Database; + +import java.sql.SQLException; +import java.sql.Statement; + +/** + * BatchStatementTest - Test without batching Statements + * + * @author Vlad Mihalcea + */ +public class NoBatchStatementTest extends AbstractBatchStatementTest { + + private int count; + + public NoBatchStatementTest(Database database) { + super(database); + } + + @Override + protected void onStatement(Statement statement, String dml) throws SQLException { + statement.executeUpdate(dml); + count++; + } + + @Override + protected void onEnd(Statement statement) throws SQLException { + //assertEquals((getPostCommentCount() + 1) * getPostCount(), count); + } + + @Override + protected void onFlush(Statement statement) { + + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/OracleBatchStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/OracleBatchStatementTest.java similarity index 77% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/OracleBatchStatementTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/OracleBatchStatementTest.java index 554dec587..b4032ed05 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/OracleBatchStatementTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/OracleBatchStatementTest.java @@ -1,11 +1,10 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch; - -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; -import com.vladmihalcea.book.hpjp.util.ReflectionUtils; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; +package com.vladmihalcea.hpjp.jdbc.batch; +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.OracleDataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; import org.junit.Test; import javax.sql.DataSource; @@ -21,7 +20,7 @@ * * @author Vlad Mihalcea */ -public class OracleBatchStatementTest extends AbstractOracleXEIntegrationTest { +public class OracleBatchStatementTest extends AbstractOracleIntegrationTest { public static final String INSERT_POST = "insert into post (title, version, id) values ('Post no. %1$d', 0, %1$d)"; @@ -37,7 +36,7 @@ public DataSource dataSource() { DataSource dataSource = super.dataSource(); try { Properties connectionProperties = ReflectionUtils.invokeGetter(dataSource, "connectionProperties"); - if(connectionProperties == null) { + if (connectionProperties == null) { connectionProperties = new Properties(); } connectionProperties.put("defaultExecuteBatch", 30); @@ -57,6 +56,9 @@ protected Class[] entities() { @Test public void testInsert() { + if (!ENABLE_LONG_RUNNING_TESTS) { + return; + } LOGGER.info("Test batch insert"); long startNanos = System.nanoTime(); doInJDBC(connection -> { @@ -75,9 +77,9 @@ public void testInsert() { } }); LOGGER.info("{}.testInsert for {} took {} millis", - getClass().getSimpleName(), - dataSourceProvider().getClass().getSimpleName(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + getClass().getSimpleName(), + dataSourceProvider().getClass().getSimpleName(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); } protected int getPostCount() { diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/OracleDefaultExecuteBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/OracleDefaultExecuteBatchPreparedStatementTest.java similarity index 76% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/OracleDefaultExecuteBatchPreparedStatementTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/OracleDefaultExecuteBatchPreparedStatementTest.java index 72fed6d9a..61a02c1ff 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/OracleDefaultExecuteBatchPreparedStatementTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/OracleDefaultExecuteBatchPreparedStatementTest.java @@ -1,11 +1,10 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch; - -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; -import com.vladmihalcea.book.hpjp.util.ReflectionUtils; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; +package com.vladmihalcea.hpjp.jdbc.batch; +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.OracleDataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -27,7 +26,7 @@ * @author Vlad Mihalcea */ @RunWith(Parameterized.class) -public class OracleDefaultExecuteBatchPreparedStatementTest extends AbstractOracleXEIntegrationTest { +public class OracleDefaultExecuteBatchPreparedStatementTest extends AbstractOracleIntegrationTest { public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; @@ -44,8 +43,8 @@ public OracleDefaultExecuteBatchPreparedStatementTest(int defaultExecuteBatch) { @Parameterized.Parameters public static Collection defaultExecuteBatches() { List providers = new ArrayList<>(); - providers.add(new Integer[] {1}); - providers.add(new Integer[] {50}); + providers.add(new Integer[]{1}); + providers.add(new Integer[]{50}); return providers; } @@ -57,7 +56,7 @@ public DataSource dataSource() { DataSource dataSource = super.dataSource(); try { Properties connectionProperties = ReflectionUtils.invokeGetter(dataSource, "connectionProperties"); - if(connectionProperties == null) { + if (connectionProperties == null) { connectionProperties = new Properties(); } connectionProperties.setProperty("defaultExecuteBatch", String.valueOf(defaultExecuteBatch)); @@ -77,15 +76,16 @@ protected Class[] entities() { @Test public void testInsert() { + if (!ENABLE_LONG_RUNNING_TESTS) { + return; + } LOGGER.info("Test batch insert for defaultExecuteBatch {}", defaultExecuteBatch); long startNanos = System.nanoTime(); doInJDBC(connection -> { try ( - PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); - PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); + PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); ) { int postCount = getPostCount(); - int postCommentCount = getPostCommentCount(); int index; @@ -96,6 +96,18 @@ public void testInsert() { postStatement.setLong(++index, i); postStatement.executeUpdate(); } + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + doInJDBC(connection -> { + try ( + PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); + ) { + int postCount = getPostCount(); + int postCommentCount = getPostCommentCount(); + + int index; for (int i = 0; i < postCount; i++) { for (int j = 0; j < postCommentCount; j++) { @@ -112,9 +124,9 @@ public void testInsert() { } }); LOGGER.info("{}.testInsert for defaultExecuteBatch {}, took {} millis", - getClass().getSimpleName(), - defaultExecuteBatch, - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + getClass().getSimpleName(), + defaultExecuteBatch, + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); } protected int getPostCount() { diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/PostgreSQLRewriteBatchInsertsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/PostgreSQLRewriteBatchInsertsTest.java new file mode 100644 index 000000000..26c4a4e32 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/PostgreSQLRewriteBatchInsertsTest.java @@ -0,0 +1,130 @@ +package com.vladmihalcea.hpjp.jdbc.batch; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.PostgreSQLDataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.fail; + +/** + * PostgreSQLRewriteBatchInsertsTest - Test PostgreSQL JDBC Statement batching with and w/o reWriteBatchedInserts + * + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public class PostgreSQLRewriteBatchInsertsTest extends AbstractPostgreSQLIntegrationTest { + + private final BlogEntityProvider entityProvider = new BlogEntityProvider(); + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .build(); + + private Timer timer = metricRegistry.timer("batchInsertTimer"); + + private boolean reWriteBatchedInserts; + + public PostgreSQLRewriteBatchInsertsTest(boolean reWriteBatchedInserts) { + this.reWriteBatchedInserts = reWriteBatchedInserts; + } + + @Parameterized.Parameters + public static Collection rdbmsDataSourceProvider() { + List providers = new ArrayList<>(); + providers.add(new Boolean[]{Boolean.FALSE}); + providers.add(new Boolean[]{Boolean.TRUE}); + return providers; + } + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return ((PostgreSQLDataSourceProvider) super.dataSourceProvider()) + .setReWriteBatchedInserts(reWriteBatchedInserts); + } + + @Test + public void testInsert() { + if (!ENABLE_LONG_RUNNING_TESTS) { + return; + } + long ttlMillis = System.currentTimeMillis() + getRunMillis(); + + final AtomicInteger postIdHolder = new AtomicInteger(); + + while (System.currentTimeMillis() < ttlMillis) { + doInJDBC(connection -> { + long startNanos = System.nanoTime(); + + AtomicInteger postStatementCount = new AtomicInteger(); + + try (PreparedStatement postStatement = connection.prepareStatement("insert into post (title, version, id) values (?, ?, ?)")) { + int postCount = getPostCount(); + + for (int i = 0; i < postCount; i++) { + int index = 0; + int postId = postIdHolder.incrementAndGet(); + postStatement.setString(++index, String.format("Post no. %1$d", postId)); + postStatement.setInt(++index, 0); + postStatement.setLong(++index, postId); + executeStatement(postStatement, postStatementCount); + } + postStatement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + }); + } + LOGGER.info("Test PostgreSQL batch insert with reWriteBatchedInserts={}", reWriteBatchedInserts); + logReporter.report(); + LOGGER.info( + "Test PostgreSQL batch insert with reWriteBatchedInserts={} took=[{}] ms", + reWriteBatchedInserts, + timer.getSnapshot().get99thPercentile() + ); + } + + private void executeStatement(PreparedStatement statement, AtomicInteger statementCount) throws SQLException { + statement.addBatch(); + int count = statementCount.incrementAndGet(); + if (count % getBatchSize() == 0) { + statement.executeBatch(); + } + } + + protected int getPostCount() { + return 5000; + } + + protected int getBatchSize() { + return 100 * 10; + } + + protected int getRunMillis() { + return 60 * 1000; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/SQLServerBulkCopyForBatchInsertPerformanceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/SQLServerBulkCopyForBatchInsertPerformanceTest.java new file mode 100644 index 000000000..45eceaa23 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/SQLServerBulkCopyForBatchInsertPerformanceTest.java @@ -0,0 +1,129 @@ +package com.vladmihalcea.hpjp.jdbc.batch; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.SQLServerDataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public class SQLServerBulkCopyForBatchInsertPerformanceTest extends AbstractSQLServerIntegrationTest { + + private final BlogEntityProvider entityProvider = new BlogEntityProvider(); + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .build(); + + private Timer timer = metricRegistry.timer("batchInsertTimer"); + + private boolean useBulkCopyForBatchInsert; + + public SQLServerBulkCopyForBatchInsertPerformanceTest(boolean useBulkCopyForBatchInsert) { + this.useBulkCopyForBatchInsert = useBulkCopyForBatchInsert; + } + + @Parameterized.Parameters + public static Collection rdbmsDataSourceProvider() { + List providers = new ArrayList<>(); + providers.add(new Boolean[]{Boolean.FALSE}); + providers.add(new Boolean[]{Boolean.TRUE}); + return providers; + } + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return ((SQLServerDataSourceProvider) super.dataSourceProvider()) + .setUseBulkCopyForBatchInsert(true) + .setSendStringParametersAsUnicode(true); + } + + @Test + public void testInsert() { + if (!ENABLE_LONG_RUNNING_TESTS) { + return; + } + long ttlMillis = System.currentTimeMillis() + getRunMillis(); + + final AtomicInteger postIdHolder = new AtomicInteger(); + + while (System.currentTimeMillis() < ttlMillis) { + doInJDBC(connection -> { + long startNanos = System.nanoTime(); + + AtomicInteger postStatementCount = new AtomicInteger(); + + try (PreparedStatement postStatement = connection.prepareStatement("INSERT INTO post (id, title, version) VALUES (?, ?, ?)")) { + int postCount = getPostCount(); + + for (int i = 0; i < postCount; i++) { + int index = 0; + int postId = postIdHolder.incrementAndGet(); + postStatement.setLong(++index, postId); + postStatement.setString(++index, String.format("Post no. %1$d", postId)); + postStatement.setInt(++index, 0); + executeStatement(postStatement, postStatementCount); + } + postStatement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + }); + } + LOGGER.info("Test SQL Server batch insert with useBulkCopyForBatchInsert={}", useBulkCopyForBatchInsert); + logReporter.report(); + LOGGER.info( + "Test SQL Server batch insert with useBulkCopyForBatchInsert={} took=[{}] ms", + useBulkCopyForBatchInsert, + timer.getSnapshot().get99thPercentile() + ); + } + + private void executeStatement(PreparedStatement statement, AtomicInteger statementCount) throws SQLException { + statement.addBatch(); + int count = statementCount.incrementAndGet(); + if (count % getBatchSize() == 0) { + statement.executeBatch(); + } + } + + protected int getPostCount() { + return 5000; + } + + protected int getBatchSize() { + return 100 * 10; + } + + protected int getRunMillis() { + return 60 * 1000; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/SimpleBatchTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/SimpleBatchTest.java new file mode 100644 index 000000000..651c51152 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/SimpleBatchTest.java @@ -0,0 +1,103 @@ +package com.vladmihalcea.hpjp.jdbc.batch; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.sql.Statement; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SimpleBatchTest extends AbstractTest { + + private BlogEntityProvider blogEntityProvider = new BlogEntityProvider(); + + @Override + protected Class[] entities() { + return blogEntityProvider.entities(); + } + + @Override + protected Database database() { + return Database.SQLSERVER; + } + + @Test + public void testNoBatching() { + LOGGER.info("Test Statement batch insert"); + doInJDBC(connection -> { + try (Statement statement = connection.createStatement()) { + int rowCount = statement.executeUpdate(""" + INSERT INTO post (title, version, id) + VALUES ('Post no. 1', 0, 1) + """); + + assertEquals(1, rowCount); + + rowCount = statement.executeUpdate(""" + INSERT INTO post (title, version, id) + VALUES ('Post no. 2', 0, 2) + """); + + assertEquals(1, rowCount); + } + }); + } + + @Test + public void testStatement() { + LOGGER.info("Test Statement batch insert"); + doInJDBC(connection -> { + try (Statement statement = connection.createStatement()) { + + statement.addBatch(""" + INSERT INTO post (title, version, id) + VALUES ('Post no. 1', 0, 1) + """); + + statement.addBatch(""" + INSERT INTO post (title, version, id) + VALUES ('Post no. 2', 0, 2) + """); + + int[] updateCounts = statement.executeBatch(); + assertEquals(2, updateCounts.length); + assertEquals(1, updateCounts[0]); + assertEquals(1, updateCounts[1]); + } + }); + } + + @Test + public void testPreparedStatement() { + LOGGER.info("Test Statement batch insert"); + doInJDBC(connection -> { + try (PreparedStatement postStatement = connection.prepareStatement(""" + INSERT INTO post (title, version, id) + VALUES (?, ?, ?) + """) + ) { + + postStatement.setString(1, String.format("Post no. %1$d", 1)); + postStatement.setInt(2, 0); + postStatement.setLong(3, 1); + postStatement.addBatch(); + + postStatement.setString(1, String.format("Post no. %1$d", 2)); + postStatement.setInt(2, 0); + postStatement.setLong(3, 2); + postStatement.addBatch(); + + int[] updateCounts = postStatement.executeBatch(); + + assertEquals(2, updateCounts.length); + } + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/identity/MySQLGeneratedKeysBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/identity/MySQLGeneratedKeysBatchPreparedStatementTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/identity/MySQLGeneratedKeysBatchPreparedStatementTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/identity/MySQLGeneratedKeysBatchPreparedStatementTest.java index d14bef357..1d87b96ef 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/identity/MySQLGeneratedKeysBatchPreparedStatementTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/identity/MySQLGeneratedKeysBatchPreparedStatementTest.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch.generatedkeys.identity; +package com.vladmihalcea.hpjp.jdbc.batch.generatedkeys.identity; -import com.vladmihalcea.book.hpjp.util.AbstractMySQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractMySQLIntegrationTest; import org.junit.Test; import java.sql.*; @@ -34,7 +34,7 @@ protected int getBatchSize() { protected void batchInsert(Connection connection) throws SQLException { LOGGER.info("Identity generated keys for MySQL"); - try(Statement statement = connection.createStatement()) { + try (Statement statement = connection.createStatement()) { statement.executeUpdate("drop table if exists post cascade"); statement.executeUpdate( diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/identity/OracleGeneratedKeysBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/identity/OracleGeneratedKeysBatchPreparedStatementTest.java similarity index 84% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/identity/OracleGeneratedKeysBatchPreparedStatementTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/identity/OracleGeneratedKeysBatchPreparedStatementTest.java index 61db750ae..a94044824 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/identity/OracleGeneratedKeysBatchPreparedStatementTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/identity/OracleGeneratedKeysBatchPreparedStatementTest.java @@ -1,17 +1,16 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch.generatedkeys.identity; +package com.vladmihalcea.hpjp.jdbc.batch.generatedkeys.identity; -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import org.junit.Ignore; import org.junit.Test; import java.sql.*; import java.util.concurrent.atomic.AtomicInteger; /** - * GeneratedKeysBatchPreparedStatementTest - Base class for testing JDBC PreparedStatement generated keys - * * @author Vlad Mihalcea */ -public class OracleGeneratedKeysBatchPreparedStatementTest extends AbstractOracleXEIntegrationTest { +public class OracleGeneratedKeysBatchPreparedStatementTest extends AbstractOracleIntegrationTest { @Override protected Class[] entities() { @@ -19,6 +18,7 @@ protected Class[] entities() { } @Test + @Ignore public void testBatch() throws SQLException { doInJDBC(this::batchInsert); } @@ -34,15 +34,17 @@ protected int getBatchSize() { protected void batchInsert(Connection connection) throws SQLException { LOGGER.info("Identity generated keys for Oracle"); - try(Statement statement = connection.createStatement()) { + try (Statement statement = connection.createStatement()) { statement.executeUpdate("drop sequence post_seq"); - } catch (Exception ignore) {} + } catch (Exception ignore) { + } - try(Statement statement = connection.createStatement()) { + try (Statement statement = connection.createStatement()) { statement.executeUpdate("drop table post"); - } catch (Exception ignore) {} + } catch (Exception ignore) { + } - try(Statement statement = connection.createStatement()) { + try (Statement statement = connection.createStatement()) { statement.executeUpdate( "CREATE SEQUENCE post_seq" diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/identity/PostgreSQLGeneratedKeysBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/identity/PostgreSQLGeneratedKeysBatchPreparedStatementTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/identity/PostgreSQLGeneratedKeysBatchPreparedStatementTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/identity/PostgreSQLGeneratedKeysBatchPreparedStatementTest.java index 753c5cbc6..9037636db 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/identity/PostgreSQLGeneratedKeysBatchPreparedStatementTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/identity/PostgreSQLGeneratedKeysBatchPreparedStatementTest.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch.generatedkeys.identity; +package com.vladmihalcea.hpjp.jdbc.batch.generatedkeys.identity; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; import org.junit.Test; import java.sql.*; @@ -34,7 +34,7 @@ protected int getBatchSize() { protected void batchInsert(Connection connection) throws SQLException { LOGGER.info("Identity generated keys for PostgreSQL"); - try(Statement statement = connection.createStatement()) { + try (Statement statement = connection.createStatement()) { statement.executeUpdate("drop table if exists post cascade"); statement.executeUpdate( diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/identity/SQLServerGeneratedKeysBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/identity/SQLServerGeneratedKeysBatchPreparedStatementTest.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/identity/SQLServerGeneratedKeysBatchPreparedStatementTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/identity/SQLServerGeneratedKeysBatchPreparedStatementTest.java index bbb5a9360..ca4c8dd03 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/identity/SQLServerGeneratedKeysBatchPreparedStatementTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/identity/SQLServerGeneratedKeysBatchPreparedStatementTest.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch.generatedkeys.identity; +package com.vladmihalcea.hpjp.jdbc.batch.generatedkeys.identity; -import com.vladmihalcea.book.hpjp.util.AbstractSQLServerIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; import org.hibernate.exception.GenericJDBCException; import org.junit.Test; @@ -35,11 +35,12 @@ protected int getBatchSize() { protected void batchInsert(Connection connection) throws SQLException { LOGGER.info("Identity generated keys for SQL Server"); - try(Statement statement = connection.createStatement()) { + try (Statement statement = connection.createStatement()) { statement.executeUpdate("drop table post"); - } catch (Exception ignore) {} + } catch (Exception ignore) { + } - try(Statement statement = connection.createStatement()) { + try (Statement statement = connection.createStatement()) { statement.executeUpdate( "create table post (" + " id bigint identity not null, " + diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/AbstractSequenceCallTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/AbstractSequenceCallTest.java similarity index 83% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/AbstractSequenceCallTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/AbstractSequenceCallTest.java index 3a9b07ce8..fcba55996 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/batch/generatedkeys/sequence/AbstractSequenceCallTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/AbstractSequenceCallTest.java @@ -1,10 +1,11 @@ -package com.vladmihalcea.book.hpjp.jdbc.batch.generatedkeys.sequence; +package com.vladmihalcea.hpjp.jdbc.batch.generatedkeys.sequence; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.Timer; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.SequenceBatchEntityProvider; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.entity.SequenceBatchEntityProvider; +import org.junit.Ignore; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,9 +28,9 @@ public abstract class AbstractSequenceCallTest extends AbstractTest { private Timer timer = metricRegistry.timer("callSequence"); private Slf4jReporter logReporter = Slf4jReporter - .forRegistry(metricRegistry) - .outputTo(LOGGER) - .build(); + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .build(); private int ttl = 60; @@ -45,6 +46,7 @@ protected Class[] entities() { } @Test + @Ignore public void testBatch() { doInJDBC(this::callSequence); } @@ -57,7 +59,7 @@ protected void callSequence(Connection connection) throws SQLException { while (TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startNanos) < ttl + 1) { long startCall = System.nanoTime(); try (ResultSet resultSet = statement.executeQuery( - callSequenceSyntax())) { + callSequenceSyntax())) { resultSet.next(); resultSet.getLong(1); } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/AbstractSequenceGeneratedKeysBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/AbstractSequenceGeneratedKeysBatchPreparedStatementTest.java new file mode 100644 index 000000000..a1e4bade0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/AbstractSequenceGeneratedKeysBatchPreparedStatementTest.java @@ -0,0 +1,101 @@ +package com.vladmihalcea.hpjp.jdbc.batch.generatedkeys.sequence; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.entity.SequenceBatchEntityProvider; +import org.junit.Test; + +import java.sql.*; +import java.util.concurrent.TimeUnit; + +/** + * AbstractSequenceGeneratedKeysBatchPreparedStatementTest - Base class for testing JDBC PreparedStatement generated keys for Sequences + * + * @author Vlad Mihalcea + */ +public abstract class AbstractSequenceGeneratedKeysBatchPreparedStatementTest extends AbstractTest { + + private SequenceBatchEntityProvider entityProvider = new SequenceBatchEntityProvider(); + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Test + public void testBatch() { + if (!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJDBC(this::batchInsert); + } + + protected int getPostCount() { + return 5 * 1000; + } + + protected int getBatchSize() { + return 25; + } + + protected int getAllocationSize() { + return 1; + } + + protected void batchInsert(Connection connection) throws SQLException { + DatabaseMetaData databaseMetaData = connection.getMetaData(); + LOGGER.info("{} Driver supportsGetGeneratedKeys: {}", dataSourceProvider().database(), databaseMetaData.supportsGetGeneratedKeys()); + + dropSequence(connection); + createSequence(connection); + + long startNanos = System.nanoTime(); + int postCount = getPostCount(); + int batchSize = getBatchSize(); + try (PreparedStatement postStatement = connection.prepareStatement( + "INSERT INTO post (id, title, version) VALUES (?, ?, ?)")) { + for (int i = 0; i < postCount; i++) { + if (i > 0 && i % batchSize == 0) { + postStatement.executeBatch(); + } + postStatement.setLong(1, getNextSequenceValue(connection)); + postStatement.setString(2, String.format("Post no. %1$d", i)); + postStatement.setInt(3, 0); + postStatement.addBatch(); + } + postStatement.executeBatch(); + } + + LOGGER.info("{}.testInsert for {} using allocation size {} took {} millis", + getClass().getSimpleName(), + dataSourceProvider().getClass().getSimpleName(), + getAllocationSize(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + } + + private long getNextSequenceValue(Connection connection) + throws SQLException { + try (Statement statement = connection.createStatement()) { + try (ResultSet resultSet = statement.executeQuery( + callSequenceSyntax())) { + resultSet.next(); + return resultSet.getLong(1); + } + } + } + + protected abstract String callSequenceSyntax(); + + protected void dropSequence(Connection connection) { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate("drop sequence post_seq"); + } catch (Exception ignore) { + } + } + + protected void createSequence(Connection connection) { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate(String.format("create sequence post_seq start with 1 increment by %d", getAllocationSize())); + } catch (Exception ignore) { + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/OracleSequenceCallTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/OracleSequenceCallTest.java new file mode 100644 index 000000000..61792f73c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/OracleSequenceCallTest.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.jdbc.batch.generatedkeys.sequence; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.OracleDataSourceProvider; + +/** + * OracleSequenceCallTest - Oracle sequence call + * + * @author Vlad Mihalcea + */ +public class OracleSequenceCallTest extends AbstractSequenceCallTest { + + @Override + protected String callSequenceSyntax() { + return "select post_seq.NEXTVAL from dual"; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new OracleDataSourceProvider(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/OracleSequenceGeneratedKeysBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/OracleSequenceGeneratedKeysBatchPreparedStatementTest.java new file mode 100644 index 000000000..8dfc046ff --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/OracleSequenceGeneratedKeysBatchPreparedStatementTest.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.jdbc.batch.generatedkeys.sequence; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.OracleDataSourceProvider; + +/** + * OracleSequenceGeneratedKeysBatchPreparedStatementTest - Oracle class for testing JDBC PreparedStatement generated keys for Sequences + * + * @author Vlad Mihalcea + */ +public class OracleSequenceGeneratedKeysBatchPreparedStatementTest extends AbstractSequenceGeneratedKeysBatchPreparedStatementTest { + + @Override + protected String callSequenceSyntax() { + return "select post_seq.NEXTVAL from dual"; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new OracleDataSourceProvider(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/PostgreSQLSequenceCallTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/PostgreSQLSequenceCallTest.java new file mode 100644 index 000000000..277926326 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/PostgreSQLSequenceCallTest.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.jdbc.batch.generatedkeys.sequence; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.PostgreSQLDataSourceProvider; + +/** + * PostgreSQLSequenceCallTest - PostgreSQL sequence call + * + * @author Vlad Mihalcea + */ +public class PostgreSQLSequenceCallTest extends AbstractSequenceCallTest { + + @Override + protected String callSequenceSyntax() { + return "select nextval('post_seq')"; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/PostgreSQLSequenceGeneratedKeysBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/PostgreSQLSequenceGeneratedKeysBatchPreparedStatementTest.java new file mode 100644 index 000000000..a2947e74d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/PostgreSQLSequenceGeneratedKeysBatchPreparedStatementTest.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.jdbc.batch.generatedkeys.sequence; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.PostgreSQLDataSourceProvider; + +/** + * PostgreSQLSequenceGeneratedKeysBatchPreparedStatementTest - PostgreSQL class for testing JDBC PreparedStatement generated keys for Sequences + * + * @author Vlad Mihalcea + */ +public class PostgreSQLSequenceGeneratedKeysBatchPreparedStatementTest extends AbstractSequenceGeneratedKeysBatchPreparedStatementTest { + + @Override + protected String callSequenceSyntax() { + return "select nextval('post_seq')"; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider(); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/SQLServerSequenceCallTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/SQLServerSequenceCallTest.java new file mode 100644 index 000000000..0e14cccef --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/SQLServerSequenceCallTest.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.jdbc.batch.generatedkeys.sequence; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.SQLServerDataSourceProvider; + +/** + * PostgreSQLSequenceCallTest - PostgreSQL sequence call + * + * @author Vlad Mihalcea + */ +public class SQLServerSequenceCallTest extends AbstractSequenceCallTest { + + @Override + protected String callSequenceSyntax() { + return "select NEXT VALUE FOR post_seq"; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new SQLServerDataSourceProvider(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/SQLServerSequenceGeneratedKeysBatchPreparedStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/SQLServerSequenceGeneratedKeysBatchPreparedStatementTest.java new file mode 100644 index 000000000..55b3ad5ac --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/batch/generatedkeys/sequence/SQLServerSequenceGeneratedKeysBatchPreparedStatementTest.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.jdbc.batch.generatedkeys.sequence; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.SQLServerDataSourceProvider; + +/** + * SQLServerSequenceGeneratedKeysBatchPreparedStatementTest - SQL Server class for testing JDBC PreparedStatement generated keys for Sequences + * + * @author Vlad Mihalcea + */ +public class SQLServerSequenceGeneratedKeysBatchPreparedStatementTest extends AbstractSequenceGeneratedKeysBatchPreparedStatementTest { + + @Override + protected String callSequenceSyntax() { + return "select NEXT VALUE FOR post_seq"; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new SQLServerDataSourceProvider(); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/MySQLStatementCacheTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/MySQLStatementCacheTest.java new file mode 100644 index 000000000..d563d84a8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/MySQLStatementCacheTest.java @@ -0,0 +1,285 @@ +package com.vladmihalcea.hpjp.jdbc.caching; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.Post; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.PostComment; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider.PostDetails; +import org.hibernate.annotations.QueryHints; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runners.Parameterized; + +import jakarta.persistence.EntityManager; +import java.sql.*; +import java.util.Date; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +/** + * @author Vlad Mihalcea + */ +public class MySQLStatementCacheTest extends DatabaseProviderIntegrationTest { + + private BlogEntityProvider entityProvider = new BlogEntityProvider(); + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .build(); + + private Timer findByIdTimer = metricRegistry.timer("findByIdTimer"); + + private Timer flushTimer = metricRegistry.timer("flushTimer"); + + private Timer query1Timer = metricRegistry.timer("query1Timer"); + + private Timer query2Timer = metricRegistry.timer("query2Timer"); + + public MySQLStatementCacheTest(Database database) { + super(database); + } + + @Parameterized.Parameters + public static Collection rdbmsDataSourceProvider() { + List providers = new ArrayList<>(); + + providers.add(new DataSourceProvider[]{ + new MySQLDataSourceProvider() + .setUseServerPrepStmts(false) + .setCachePrepStmts(false) + }); + + providers.add(new DataSourceProvider[]{ + new MySQLDataSourceProvider() + .setUseServerPrepStmts(true) + .setCachePrepStmts(false) + }); + + providers.add(new DataSourceProvider[]{ + new MySQLDataSourceProvider() + .setUseServerPrepStmts(false) + .setCachePrepStmts(true) + .setPrepStmtCacheSqlLimit(2048) + }); + + providers.add(new DataSourceProvider[]{ + new MySQLDataSourceProvider() + .setUseServerPrepStmts(true) + .setCachePrepStmts(true) + .setPrepStmtCacheSqlLimit(2048) + }); + + return providers; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.generate_statistics", Boolean.FALSE.toString()); + properties.put("hibernate.jdbc.batch_size", "50"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + protected boolean connectionPooling() { + return true; + } + + @Override + protected boolean proxyDataSource() { + return false; + } + + public int getRunCount() { + return 5000; + } + + protected int getRunMillis() { + return 5 * 60 * 1000; + } + + protected int getPostCount() { + return 1000; + } + + protected int getPostCommentCount() { + return 5; + } + + @Override + public void afterInit() { + int postCommentCount = getPostCommentCount(); + + doInJPA(entityManager -> { + for (long postId = 0; postId < getPostCount(); postId++) { + Post post = new Post(); + post.setId(postId); + post.setTitle(String.format("Post no. %1$d", postId)); + entityManager.persist(post); + + PostDetails details = new PostDetails(); + details.setId(postId); + details.setCreatedBy("Vlad Mihalcea"); + details.setCreatedOn(new Date(System.currentTimeMillis())); + details.setPost(post); + entityManager.persist(details); + + for (int j = 0; j < postCommentCount; j++) { + PostComment comment = new PostComment(); + comment.setId((postCommentCount * postId) + j); + comment.setReview(String.format("Post comment %1$d", j)); + comment.setPost(post); + + entityManager.persist(comment); + } + } + }); + } + + @Test + @Ignore + public void testStatementCachingJPA() { + long ttlMillis = System.currentTimeMillis() + getRunMillis(); + AtomicInteger transactionCount = new AtomicInteger(); + while (System.currentTimeMillis() < ttlMillis) { + transactionCount.incrementAndGet(); + doInJPA(_entityManager -> { + executeWithTiming( + _entityManager, + findByIdTimer, + (EntityManager entityManager) -> { + Post post = entityManager.find(Post.class, randomId()); + PostDetails details = entityManager.find(PostDetails.class, randomId()); + PostComment comment = entityManager.find(PostComment.class, randomId()); + + long millis = System.currentTimeMillis(); + post.setTitle(String.format("Post no. %1$d", millis)); + details.setCreatedOn(new Date(millis)); + comment.setReview( + String.format("Post comment - %1$d", millis) + ); + } + ); + + executeWithTiming( + _entityManager, + flushTimer, + EntityManager::flush + ); + + executeWithTiming( + _entityManager, + query1Timer, + (EntityManager entityManager) -> entityManager.createQuery(""" + select p + from Post p + join fetch p.details pd + where p.id > :id + order by pd.id desc + """) + .setParameter("id", randomId()) + .setMaxResults(5) + .setHint(QueryHints.FETCH_SIZE, Integer.MIN_VALUE) + .getResultStream() + .close() + ); + + executeWithTiming( + _entityManager, + query2Timer, + (EntityManager entityManager) -> entityManager.createQuery(""" + select pc + from PostComment pc + join fetch pc.post p + where p.id > :postId + order by p.id asc + """) + .setParameter("postId", randomId()) + .setMaxResults(5) + .setHint(QueryHints.FETCH_SIZE, Integer.MIN_VALUE) + .getResultStream() + .close() + ); + }); + } + + LOGGER.info( + "MySQL connection settings: {}, throughput {} tps", + dataSourceProvider(), + transactionCount.get() + ); + logReporter.report(); + } + + private void executeWithTiming( + EntityManager entityManager, + Timer timer, + Consumer consumer + ) { + long startNanos = System.nanoTime(); + consumer.accept(entityManager); + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + } + + private Long randomId() { + return (long) (Math.random() * getPostCount()); + } + + @Test + @Ignore + public void testStatementCachingJDBC() { + for (long i = 1; i <= getRunCount(); i++) { + doInJDBC(connection -> { + for (int j = 0; j < 5; j++) { + long startNanos = System.nanoTime(); + try (PreparedStatement statement = connection.prepareStatement(""" + SELECT p.title, pd.created_on, pc_c.comment_count + FROM post p + LEFT JOIN post_details pd ON p.id = pd.id + JOIN ( + SELECT pc.post_id AS post_id, count(pc.id) AS comment_count + FROM post_comment pc + GROUP BY pc.post_id + ORDER BY pc.post_id DESC + LIMIT ? + ) pc_c ON p.id = pc_c.post_id + """)) { + + statement.setInt(1, 5); + statement.executeQuery(); + } + query1Timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + } + }); + } + + LOGGER.info("MySQL connection settings: {}", dataSourceProvider()); + logReporter.report(); + } + + private void printServerSidePreparedStatementCount(Connection connection) throws SQLException { + try (Statement psCountStatement = connection.createStatement(); + ResultSet psCountResultSet = psCountStatement.executeQuery("SHOW SESSION STATUS LIKE 'Prepared_stmt_count'")) { + psCountResultSet.next(); + String statusName = psCountResultSet.getString(1); + int statusValue = psCountResultSet.getInt(2); + LOGGER.info("MySQL session status: {}, value: {}", statusName, statusValue); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/OracleExplicitStatementCacheTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/OracleExplicitStatementCacheTest.java similarity index 81% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/OracleExplicitStatementCacheTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/OracleExplicitStatementCacheTest.java index 01cd3a1dc..a8a60f7c0 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/OracleExplicitStatementCacheTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/OracleExplicitStatementCacheTest.java @@ -1,8 +1,10 @@ -package com.vladmihalcea.book.hpjp.jdbc.caching; +package com.vladmihalcea.hpjp.jdbc.caching; -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; -import com.vladmihalcea.book.hpjp.util.ReflectionUtils; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import oracle.jdbc.OracleConnection; +import oracle.jdbc.OraclePreparedStatement; +import org.junit.Ignore; import org.junit.Test; import java.sql.PreparedStatement; @@ -15,7 +17,7 @@ * * @author Vlad Mihalcea */ -public class OracleExplicitStatementCacheTest extends AbstractOracleXEIntegrationTest { +public class OracleExplicitStatementCacheTest extends AbstractOracleIntegrationTest { public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; @@ -75,19 +77,21 @@ public void init() { } @Test + @Ignore public void testStatementCaching() { doInJDBC(connection -> { for (int i = 0; i < 5; i++) { - ReflectionUtils.invokeSetter(connection,"explicitCachingEnabled", true); - ReflectionUtils.invokeSetter(connection,"statementCacheSize", 1); - PreparedStatement statement = ReflectionUtils.invoke(connection, ReflectionUtils.getMethod(connection, "getStatementWithKey", String.class), SELECT_POST_REVIEWS_KEY); + OracleConnection oracleConnection = (OracleConnection) connection; + oracleConnection.setExplicitCachingEnabled(true); + oracleConnection.setStatementCacheSize(1); + PreparedStatement statement = oracleConnection.getStatementWithKey(SELECT_POST_REVIEWS_KEY); if (statement == null) statement = connection.prepareStatement(SELECT_POST_REVIEWS); try { statement.setInt(1, 10); statement.execute(); } finally { - ReflectionUtils.invoke(statement, ReflectionUtils.getMethod(statement, "closeWithKey", String.class), SELECT_POST_REVIEWS_KEY); + ((OraclePreparedStatement) statement).closeWithKey(SELECT_POST_REVIEWS_KEY); } } }); diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/OracleImplicitStatementCacheTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/OracleImplicitStatementCacheTest.java similarity index 92% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/OracleImplicitStatementCacheTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/OracleImplicitStatementCacheTest.java index c9183fb75..431f60388 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/OracleImplicitStatementCacheTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/OracleImplicitStatementCacheTest.java @@ -1,11 +1,12 @@ -package com.vladmihalcea.book.hpjp.jdbc.caching; +package com.vladmihalcea.hpjp.jdbc.caching; -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; -import com.vladmihalcea.book.hpjp.util.ReflectionUtils; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.OracleDataSourceProvider; +import org.junit.Ignore; import org.junit.Test; import javax.sql.DataSource; @@ -21,7 +22,7 @@ * * @author Vlad Mihalcea */ -public class OracleImplicitStatementCacheTest extends AbstractOracleXEIntegrationTest { +public class OracleImplicitStatementCacheTest extends AbstractOracleIntegrationTest { public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; @@ -93,11 +94,13 @@ public void init() { } @Test + @Ignore public void testStatementCaching() { selectWhenCaching(true); } @Test + @Ignore public void testStatementWithoutCaching() { selectWhenCaching(false); } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/OraclePreparedStatementLifecycleTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/OraclePreparedStatementLifecycleTest.java new file mode 100644 index 000000000..0b900c4c1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/OraclePreparedStatementLifecycleTest.java @@ -0,0 +1,119 @@ +package com.vladmihalcea.hpjp.jdbc.caching; + +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class OraclePreparedStatementLifecycleTest extends AbstractOracleIntegrationTest { + + public static final String INSERT_POST = "INSERT INTO post (id, title) VALUES (:1 , :2 )"; + public static final String INSERT_POST_PREFIX = "INSERT INTO post"; + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Test + public void testPreparedStatement() { + doInJDBC(connection -> { + // This setting doesn't influence the outcome + executeStatement("ALTER SESSION SET session_cached_cursors=0"); + assertEquals(0, getOpenCursorsForStatement(INSERT_POST_PREFIX).size()); + PreparedStatement preparedStatement1 = null; + PreparedStatement preparedStatement2 = null; + try { + preparedStatement1 = connection.prepareStatement(INSERT_POST); + assertEquals(0, getOpenCursorsForStatement(INSERT_POST_PREFIX).size()); + + int index = 0; + preparedStatement1.setLong(++index, 1L); + preparedStatement1.setString(++index, "High-Performance SQL"); + preparedStatement1.executeUpdate(); + + preparedStatement2 = connection.prepareStatement("INSERT INTO post (id) VALUES (:1)"); + preparedStatement2.setLong(1, 2L); + preparedStatement2.executeUpdate(); + + List openCursors = getOpenCursorsForStatement(INSERT_POST_PREFIX); + assertEquals(2, openCursors.size()); + } catch (SQLException e) { + fail(e.getMessage()); + } finally { + if (preparedStatement1 != null) { + preparedStatement1.close(); + } + if (preparedStatement2 != null) { + preparedStatement2.close(); + } + assertEquals(2, getOpenCursorsForStatement(INSERT_POST_PREFIX).size()); + connection.commit(); + assertEquals(0, getOpenCursorsForStatement(INSERT_POST_PREFIX).size()); + } + }); + } + + private List getOpenCursorsForStatement(String sqlPrefix) { + return doInJPA(entityManager -> { + return entityManager.createNativeQuery(""" + SELECT + sql_text, + count(*) AS "OPEN CURSORS" + FROM v$open_cursor oc + WHERE + user_name= 'ORACLE' AND + sql_text LIKE :sqlPrefix + GROUP BY + sql_text, + user_name + ORDER BY + count(*) DESC + """, Tuple.class) + .setParameter("sqlPrefix", sqlPrefix + "%") + .getResultList(); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/PostgreSQLPlanCacheModeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/PostgreSQLPlanCacheModeTest.java new file mode 100644 index 000000000..a1359532d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/PostgreSQLPlanCacheModeTest.java @@ -0,0 +1,268 @@ +package com.vladmihalcea.hpjp.jdbc.caching; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.queries.PostgreSQLQueries; +import jakarta.persistence.*; +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLPlanCacheModeTest extends AbstractTest { + + public static final String INSERT_POST = "INSERT INTO post (id, title, status) VALUES (?, ?, ?)"; + + private static ThreadLocalRandom random = ThreadLocalRandom.current(); + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void beforeInit() { + executeStatement("DROP TYPE IF EXISTS post_status"); + executeStatement("CREATE TYPE post_status AS ENUM ('PENDING', 'APPROVED', 'SPAM')"); + } + + @Override + protected void afterInit() { + AtomicInteger statementCount = new AtomicInteger(); + long startNanos = System.nanoTime(); + doInJDBC(connection -> { + try (PreparedStatement statement = connection.prepareStatement(INSERT_POST)) { + int postCount = getPostCount(); + + for (int i = 1; i <= postCount; i++) { + PostStatus status = PostStatus.APPROVED; + if (i > postCount * 0.99) { + status = PostStatus.SPAM; + } else if (i > postCount * 0.95) { + status = PostStatus.PENDING; + } + statement.setLong(1, i); + statement.setString(2, String.format("High-Performance Java Persistence, page %d", i)); + statement.setObject(3, PostgreSQLQueries.toEnum(status, "post_status"), Types.OTHER); + + addToBatch(statement, statementCount); + } + statement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + LOGGER.info("{}.testInsert took {} millis", + getClass().getSimpleName(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos) + ); + } + + @Test + public void testIndexSelectivity() { + executeStatement( + "DROP INDEX IF EXISTS idx_post_status", + """ + CREATE INDEX IF NOT EXISTS idx_post_status ON post (status) + """, + "ANALYZE VERBOSE" + ); + + doInJDBC(connection -> { + executeStatement( + connection, + "LOAD 'auto_explain'", + "SET auto_explain.log_analyze=true", + "SET auto_explain.log_min_duration=0" + ); + String planCacheMode = selectColumn(connection, "SHOW plan_cache_mode", String.class); + LOGGER.info("Plan cache mode: {}", planCacheMode); + try (PreparedStatement statement = connection.prepareStatement(""" + SELECT id, title, status + FROM post + WHERE status = ? + """)) { + executeStatementWithStatus(statement, PostStatus.PENDING); + executeStatementWithStatus(statement, PostStatus.SPAM); + executeStatementWithStatus(statement, PostStatus.APPROVED); + } + }); + } + + @Test + public void testDefaultGenericPlanCaching() { + executeStatement( + "DROP INDEX IF EXISTS idx_post_status", + """ + CREATE INDEX IF NOT EXISTS idx_post_status ON post (status) WHERE status != 'APPROVED' + """, + "ANALYZE VERBOSE" + ); + + doInJDBC(connection -> { + executeStatement( + connection, + "LOAD 'auto_explain'", + "SET auto_explain.log_analyze=true", + "SET auto_explain.log_min_duration=0" + ); + LOGGER.info( + "Plan cache mode: {}", + selectColumn( + connection, + "SHOW plan_cache_mode", + String.class + ) + ); + try (PreparedStatement statement = connection.prepareStatement(""" + SELECT id, title, status + FROM post + WHERE status = ? + """)) { + for (int i = 1; i <= 10; i++) { + executeStatementWithStatus(statement, PostStatus.APPROVED); + } + executeStatementWithStatus(statement, PostStatus.SPAM); + } + }); + } + + @Test + public void testForceCustomPlanCacheMode() { + executeStatement( + "DROP INDEX IF EXISTS idx_post_status", + """ + CREATE INDEX IF NOT EXISTS idx_post_status ON post (status) WHERE status != 'APPROVED' + """, + "ANALYZE VERBOSE" + ); + + doInJDBC(connection -> { + executeStatement( + connection, + "LOAD 'auto_explain'", + "SET auto_explain.log_analyze=true", + "SET auto_explain.log_min_duration=0" + ); + + try (PreparedStatement statement = connection.prepareStatement(""" + SELECT id, title, status + FROM post + WHERE status = ? + """)) { + for (int i = 1; i <= 10; i++) { + executeStatementWithStatus(statement, PostStatus.APPROVED); + } + executeStatementWithStatus(statement, PostStatus.SPAM); + executeStatement( + connection, + "SET plan_cache_mode=force_custom_plan" + ); + executeStatementWithStatus(statement, PostStatus.SPAM); + } + }); + } + + protected int executeStatementWithStatus(PreparedStatement statement, PostStatus status) + throws SQLException { + LOGGER.info( + "Statement is {}prepared on the server", + PostgreSQLQueries.isUseServerPrepare(statement) ? "" : + "not " + ); + int rowCount = 0; + statement.setObject( + 1, + PostgreSQLQueries.toEnum(status, "post_status"), + Types.OTHER + ); + try(ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + rowCount++; + } + } + return rowCount; + } + + private void addToBatch(PreparedStatement statement, AtomicInteger statementCount) throws SQLException { + statement.addBatch(); + int count = statementCount.incrementAndGet(); + if(count % getBatchSize() == 0) { + statement.executeBatch(); + } + } + + protected int getPostCount() { + return 100_000; + } + + protected int getBatchSize() { + return 1000; + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @Column(length = 100) + private String title; + + @Column(columnDefinition = "post_status") + @JdbcType(PostgreSQLEnumJdbcType.class) + private PostStatus status; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public PostStatus getStatus() { + return status; + } + + public void setStatus(PostStatus status) { + this.status = status; + } + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/PostgreSQLPreparedStatementCompilingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/PostgreSQLPreparedStatementCompilingTest.java new file mode 100644 index 000000000..afbd386b9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/PostgreSQLPreparedStatementCompilingTest.java @@ -0,0 +1,185 @@ +package com.vladmihalcea.hpjp.jdbc.caching; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLPreparedStatementCompilingTest extends AbstractTest { + + public static final String INSERT_POST = "INSERT INTO post (id, title, status) VALUES (?, ?, ?)"; + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testInsert() { + AtomicInteger statementCount = new AtomicInteger(); + long startNanos = System.nanoTime(); + doInJDBC(connection -> { + try (PreparedStatement statement = connection.prepareStatement(INSERT_POST)) { + int postCount = getPostCount(); + + for (int i = 1; i <= postCount; i++) { + PostStatus status = PostStatus.APPROVED; + if (i > postCount * 0.99) { + status = PostStatus.SPAM; + } else if (i > postCount * 0.95) { + status = PostStatus.PENDING; + } + statement.setLong(1, i); + statement.setString(2, String.format("High-Performance Java Persistence, page %d", i)); + statement.setInt(3, status.ordinal()); + + executeStatement(statement, statementCount); + } + statement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + LOGGER.info("{}.testInsert took {} millis", + getClass().getSimpleName(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos) + ); + + /*doInJDBC(connection -> { + //c:\Program Files\PostgreSQL\13\data\log\postgresql-yyyy-MM-dd_hhmmss.log + executeStatement( + connection, + "LOAD 'auto_explain'", + "SET auto_explain.log_analyze=true", + "SET auto_explain.log_min_duration=0" + ); + + try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM post WHERE status = ?")) { + postIdsForStatus(statement, PostStatus.PENDING); + postIdsForStatus(statement, PostStatus.APPROVED); + postIdsForStatus(statement, PostStatus.SPAM); + postIdsForStatus(statement, PostStatus.PENDING); + postIdsForStatus(statement, PostStatus.APPROVED); + postIdsForStatus(statement, PostStatus.SPAM); + postIdsForStatus(statement, PostStatus.PENDING); + } + });*/ + + doInJPA(entityManager -> { + ThreadLocalRandom random = ThreadLocalRandom.current(); + for (int i = 0; i < 10; i++) { + List planLines = entityManager.createNativeQuery(""" + EXPLAIN ANALYZE + SELECT * + FROM post p + WHERE p.status = :status + """) + .setParameter("status", random.nextInt(PostStatus.values().length)) + .getResultList(); + + LOGGER.info("Execution plan: {}{}", + System.lineSeparator(), + planLines.stream().collect(Collectors.joining(System.lineSeparator())) + ); + } + }); + } + + protected List postIdsForStatus(PreparedStatement statement, PostStatus status) + throws SQLException { + List ids = new ArrayList<>(); + + statement.setInt(1, status.ordinal()); + try(ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + ids.add(resultSet.getLong(1)); + } + } + + return ids; + } + + private void executeStatement(PreparedStatement statement, AtomicInteger statementCount) throws SQLException { + statement.addBatch(); + int count = statementCount.incrementAndGet(); + if(count % getBatchSize() == 0) { + statement.executeBatch(); + } + } + + protected int getPostCount() { + return 1000; + } + + protected int getBatchSize() { + return 100; + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @Enumerated + private PostStatus status; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public PostStatus getStatus() { + return status; + } + + public void setStatus(PostStatus status) { + this.status = status; + } + } + + public enum PostStatus { + PENDING, + APPROVED, + SPAM + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/PostgreSQLStatementCachePoolingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/PostgreSQLStatementCachePoolingTest.java new file mode 100644 index 000000000..e8c719801 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/PostgreSQLStatementCachePoolingTest.java @@ -0,0 +1,184 @@ +package com.vladmihalcea.hpjp.jdbc.caching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import com.zaxxer.hikari.HikariConfig; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.junit.Test; +import org.postgresql.core.CachedQuery; +import org.postgresql.jdbc.PgStatement; + +import javax.sql.DataSource; +import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.*; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLStatementCachePoolingTest extends AbstractPostgreSQLIntegrationTest { + + public static final String INSERT_POST = "insert into post (title, id) values (?, ?)"; + + @Override + protected Class[] entities() { + return new Class[] {Post.class}; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + for (long i = 1; i <= postCount(); i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle( + String.format( + "High-Performance Java Persistence, page %d", i + ) + ) + ); + } + }); + } + + @Test + public void testStatementCaching() throws ExecutionException, InterruptedException { + ThreadLocalRandom random = ThreadLocalRandom.current(); + ExecutorService executorService = Executors.newFixedThreadPool(4); + int statementExecutionCount = 100; + List> futures = new ArrayList<>(statementExecutionCount); + ConcurrentMap cachedQueryStatsMap = new ConcurrentHashMap<>(); + + for (long i = 1; i <= statementExecutionCount; i++) { + futures.add( + executorService.submit(() -> { + doInJDBC(connection -> { + try (PreparedStatement statement = connection.prepareStatement( + "SELECT title FROM post WHERE id = ?" + )) { + PgStatement pgStatement = statement.unwrap(PgStatement.class); + CachedQuery cachedQuery = ReflectionUtils.getFieldValue(pgStatement, "preparedQuery"); + CachedQueryStats cachedQueryStats = cachedQueryStatsMap.computeIfAbsent(cachedQuery, pgc -> new CachedQueryStats()); + cachedQueryStats.incrementExecutionCount(); + if(pgStatement.isUseServerPrepare()) { + cachedQueryStats.incrementPreparedExecutions(); + } else { + cachedQueryStats.incrementUnpreparedExecutions(); + } + statement.setLong(1, random.nextLong(postCount())); + statement.executeQuery(); + } + }); + }) + ); + } + + for(Future future : futures) { + future.get(); + } + for(Map.Entry statisticsMapEntry : cachedQueryStatsMap.entrySet()) { + CachedQuery cachedQuery = statisticsMapEntry.getKey(); + CachedQueryStats cachedQueryStats = statisticsMapEntry.getValue(); + LOGGER.error("Statement [{}] stats: [{}]", cachedQuery, cachedQueryStats); + } + } + + protected int postCount() { + return 100; + } + + @Override + protected boolean proxyDataSource() { + return false; + } + + @Override + protected boolean connectionPooling() { + return true; + } + + @Override + protected HikariConfig hikariConfig(DataSource dataSource) { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setMaximumPoolSize(4); + hikariConfig.setDataSource(dataSource); + return hikariConfig; + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + public static class CachedQueryStats { + private long executionCount; + private long unpreparedExecutions; + private long preparedExecutions; + + public long executionCount() { + return executionCount; + } + + public long unpreparedExecutions() { + return unpreparedExecutions; + } + + public long preparedExecutions() { + return preparedExecutions; + } + + public void incrementExecutionCount() { + executionCount++; + } + + public void incrementUnpreparedExecutions() { + unpreparedExecutions++; + } + + public void incrementPreparedExecutions() { + preparedExecutions++; + } + + @Override + public String toString() { + return "executionCount=" + executionCount + + ", unpreparedExecutions=" + unpreparedExecutions + + ", preparedExecutions=" + preparedExecutions; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/PostgreSQLStatementCacheTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/PostgreSQLStatementCacheTest.java new file mode 100644 index 000000000..4db2c7cc7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/PostgreSQLStatementCacheTest.java @@ -0,0 +1,87 @@ +package com.vladmihalcea.hpjp.jdbc.caching; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.entity.TaskEntityProvider; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Properties; + +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLStatementCacheTest extends AbstractPostgreSQLIntegrationTest { + + public static final String INSERT_TASK = "insert into task (id, status) values (?, ?)"; + + private TaskEntityProvider entityProvider = new TaskEntityProvider(); + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } + + @Override + public void afterInit() { + doInJDBC(connection -> { + try (PreparedStatement taskStatement = connection.prepareStatement(INSERT_TASK)) { + int taskCount = getTaskCount(); + + int index; + + for (int i = 0; i < taskCount; i++) { + index = 0; + TaskEntityProvider.StatusType statusType; + if (i > taskCount * 0.99) { + statusType = TaskEntityProvider.StatusType.FAILED; + } else if (i > taskCount * 0.95) { + statusType = TaskEntityProvider.StatusType.TO_D0; + } else { + statusType = TaskEntityProvider.StatusType.DONE; + } + taskStatement.setInt(++index, i); + taskStatement.setString(++index, statusType.name()); + taskStatement.executeUpdate(); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + } + + @Test + public void testStatementCaching() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJDBC(connection -> { + try (PreparedStatement statement = connection.prepareStatement(""" + SELECT * + FROM task + WHERE status = ? + """ + )) { + statement.setString(1, TaskEntityProvider.StatusType.FAILED.name()); + statement.executeQuery(); + } + }); + } + + protected int getTaskCount() { + return 10_000; + } + + @Override + protected boolean proxyDataSource() { + return false; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/SQLServerImplicitStatementCacheTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/SQLServerImplicitStatementCacheTest.java similarity index 91% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/SQLServerImplicitStatementCacheTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/SQLServerImplicitStatementCacheTest.java index 01ce91011..4bcd52e55 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/caching/SQLServerImplicitStatementCacheTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/SQLServerImplicitStatementCacheTest.java @@ -1,7 +1,8 @@ -package com.vladmihalcea.book.hpjp.jdbc.caching; +package com.vladmihalcea.hpjp.jdbc.caching; -import com.vladmihalcea.book.hpjp.util.AbstractSQLServerIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.TaskEntityProvider; +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.entity.TaskEntityProvider; +import org.junit.Ignore; import org.junit.Test; import java.sql.PreparedStatement; @@ -57,6 +58,7 @@ public void init() { } @Test + @Ignore public void testStatementCaching() { doInJDBC(connection -> { try (PreparedStatement statement = connection.prepareStatement( diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/SQLServerPreparedStatementLifecycleTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/SQLServerPreparedStatementLifecycleTest.java new file mode 100644 index 000000000..2176c5d23 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/SQLServerPreparedStatementLifecycleTest.java @@ -0,0 +1,97 @@ +package com.vladmihalcea.hpjp.jdbc.caching; + +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerPreparedStatementLifecycleTest extends AbstractSQLServerIntegrationTest { + + public static final String SELECT_POST = "SELECT id, title FROM post"; + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Test + public void testPreparedStatement() { + doInJPA(entityManager -> { + for (long i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle(String.format("High-Performance Java Persistence, part %d", i)) + ); + } + }); + doInJDBC(connection -> { + PreparedStatement preparedStatement = null; + ResultSet resultSet = null; + try { + preparedStatement = connection.prepareStatement(SELECT_POST); + + resultSet = preparedStatement.executeQuery(); + + resultSet.close(); + + resultSet = preparedStatement.executeQuery(); + + resultSet.close(); + + resultSet = preparedStatement.executeQuery(); + } catch (SQLException e) { + fail(e.getMessage()); + } finally { + if(resultSet != null) { + resultSet.close(); + } + if (preparedStatement != null) { + preparedStatement.close(); + } + } + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/StatementCachePoolableTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/StatementCachePoolableTest.java new file mode 100644 index 000000000..1343d1eda --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/StatementCachePoolableTest.java @@ -0,0 +1,219 @@ +package com.vladmihalcea.hpjp.jdbc.caching; + +import com.microsoft.sqlserver.jdbc.SQLServerDataSource; +import com.vladmihalcea.hpjp.util.DataSourceProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import com.vladmihalcea.hpjp.util.providers.*; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.junit.Test; +import org.junit.runners.Parameterized; +import org.postgresql.ds.PGSimpleDataSource; + +import javax.sql.DataSource; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.fail; + +/** + * StatementCacheTest - Test Statement cache + * + * @author Vlad Mihalcea + */ +public class StatementCachePoolableTest extends DataSourceProviderIntegrationTest { + + public static class CachingOracleDataSourceProvider extends OracleDataSourceProvider { + private final int cacheSize; + + CachingOracleDataSourceProvider(int cacheSize) { + this.cacheSize = cacheSize; + } + + @Override + public DataSource dataSource() { + DataSource dataSource = super.dataSource(); + try { + Properties connectionProperties = ReflectionUtils.invokeGetter(dataSource, "connectionProperties"); + if (connectionProperties == null) { + connectionProperties = new Properties(); + } + connectionProperties.put("oracle.jdbc.implicitStatementCacheSize", Integer.toString(cacheSize)); + ReflectionUtils.invokeSetter(dataSource, "connectionProperties", connectionProperties); + } catch (Exception e) { + fail(e.getMessage()); + } + return dataSource; + } + + @Override + public String toString() { + return "CachingOracleDataSourceProvider{" + + "cacheSize=" + cacheSize + + '}'; + } + } + + public static class CachingSQLServerDataSourceProvider extends SQLServerDataSourceProvider { + private final int cacheSize; + + CachingSQLServerDataSourceProvider(int cacheSize) { + this.cacheSize = cacheSize; + } + + @Override + public DataSource dataSource() { + SQLServerDataSource dataSource = (SQLServerDataSource) super.dataSource(); + dataSource.setDisableStatementPooling(false); + dataSource.setStatementPoolingCacheSize(cacheSize); + return dataSource; + } + + @Override + public String toString() { + return "CachingSQLServerDataSourceProvider{" + + "cacheSize=" + cacheSize + + '}'; + } + } + + public static class CachingPostgreSQLDataSourceProvider extends PostgreSQLDataSourceProvider { + private final int cacheSize; + + CachingPostgreSQLDataSourceProvider(int cacheSize) { + this.cacheSize = cacheSize; + } + + @Override + public DataSource dataSource() { + PGSimpleDataSource dataSource = (PGSimpleDataSource) super.dataSource(); + dataSource.setPreparedStatementCacheQueries(cacheSize); + return dataSource; + } + + @Override + public String toString() { + return "CachingPostgreSQLDataSourceProvider{" + + "cacheSize=" + cacheSize + + '}'; + } + } + + public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; + + public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"; + + private BlogEntityProvider entityProvider = new BlogEntityProvider(); + + public StatementCachePoolableTest(DataSourceProvider dataSourceProvider) { + super(dataSourceProvider); + } + + @Parameterized.Parameters + public static Collection rdbmsDataSourceProvider() { + List providers = new ArrayList<>(); + providers.add(new DataSourceProvider[]{ + new CachingOracleDataSourceProvider(1) + }); + providers.add(new DataSourceProvider[]{ + new CachingSQLServerDataSourceProvider(1) + }); + providers.add(new DataSourceProvider[]{ + new CachingPostgreSQLDataSourceProvider(1) + }); + MySQLDataSourceProvider mySQLCachingDataSourceProvider = new MySQLDataSourceProvider(); + mySQLCachingDataSourceProvider.setUseServerPrepStmts(true); + mySQLCachingDataSourceProvider.setCachePrepStmts(true); + providers.add(new DataSourceProvider[]{ + mySQLCachingDataSourceProvider + }); + return providers; + } + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + public void init() { + super.init(); + doInJDBC(connection -> { + try ( + PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); + PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); + ) { + int postCount = getPostCount(); + int postCommentCount = getPostCommentCount(); + + int index; + + for (int i = 0; i < postCount; i++) { + index = 0; + postStatement.setString(++index, String.format("Post no. %1$d", i)); + postStatement.setInt(++index, 0); + postStatement.setLong(++index, i); + postStatement.executeUpdate(); + } + + for (int i = 0; i < postCount; i++) { + for (int j = 0; j < postCommentCount; j++) { + index = 0; + postCommentStatement.setLong(++index, i); + postCommentStatement.setString(++index, String.format("Post comment %1$d", j)); + postCommentStatement.setInt(++index, (int) (Math.random() * 1000)); + postCommentStatement.setLong(++index, (postCommentCount * i) + j); + postCommentStatement.executeUpdate(); + } + } + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + } + + @Test + public void selectWhenCaching() { + AtomicInteger counter = new AtomicInteger(); + doInJDBC(connection -> { + for (int i = 0; i < 2; i++) { + try (PreparedStatement statement = connection.prepareStatement(""" + SELECT p.title, pd.created_on + FROM post p + LEFT JOIN post_details pd ON p.id = pd.id + WHERE EXISTS ( + SELECT 1 + FROM post_comment + WHERE post_id > p.id AND version = ? + )""" + )) { + statement.setPoolable(false); + statement.setInt(1, counter.incrementAndGet()); + statement.execute(); + } catch (Throwable e) { + LOGGER.error("Failed test", e); + } + } + }); + LOGGER.info("When using {}, throughput is {} statements", + dataSourceProvider(), + counter.get()); + } + + protected int getPostCount() { + return 1000; + } + + protected int getPostCommentCount() { + return 5; + } + + @Override + protected boolean proxyDataSource() { + return false; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/StatementCacheTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/StatementCacheTest.java new file mode 100644 index 000000000..39110047a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/caching/StatementCacheTest.java @@ -0,0 +1,358 @@ +package com.vladmihalcea.hpjp.jdbc.caching; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.microsoft.sqlserver.jdbc.SQLServerDataSource; +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.*; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import oracle.jdbc.pool.OracleDataSource; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runners.Parameterized; +import org.postgresql.ds.PGSimpleDataSource; + +import javax.sql.DataSource; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class StatementCacheTest extends DatabaseProviderIntegrationTest { + + public static class CachingOracleDataSourceProvider extends OracleDataSourceProvider { + private final int cacheSize; + + CachingOracleDataSourceProvider(int cacheSize) { + this.cacheSize = cacheSize; + } + + @Override + public DataSource dataSource() { + OracleDataSource dataSource = (OracleDataSource) super.dataSource(); + try { + Properties connectionProperties = dataSource.getConnectionProperties(); + if(connectionProperties == null) { + connectionProperties = new Properties(); + } + connectionProperties.put("oracle.jdbc.implicitStatementCacheSize", Integer.toString(cacheSize)); + dataSource.setConnectionProperties(connectionProperties); + } catch (Exception e) { + fail(e.getMessage()); + } + return dataSource; + } + + @Override + public String toString() { + return "CachingOracleDataSourceProvider{" + + "cacheSize=" + cacheSize + + '}'; + } + } + + public static class CachingSQLServerDataSourceProvider extends SQLServerDataSourceProvider { + private final int cacheSize; + + CachingSQLServerDataSourceProvider(int cacheSize) { + this.cacheSize = cacheSize; + } + + @Override + public DataSource dataSource() { + SQLServerDataSource dataSource = (SQLServerDataSource) super.dataSource(); + dataSource.setStatementPoolingCacheSize(cacheSize); + if (cacheSize > 0) { + dataSource.setDisableStatementPooling(false); + } + return dataSource; + } + + @Override + public String toString() { + return "CachingSQLServerDataSourceProvider{" + + "cacheSize=" + cacheSize + + '}'; + } + } + + public static class CachingPostgreSQLDataSourceProvider extends PostgreSQLDataSourceProvider { + private final int cacheSize; + + CachingPostgreSQLDataSourceProvider(int cacheSize) { + this.cacheSize = cacheSize; + } + + @Override + public DataSource dataSource() { + PGSimpleDataSource dataSource = (PGSimpleDataSource) super.dataSource(); + dataSource.setPreparedStatementCacheQueries(cacheSize); + return dataSource; + } + + @Override + public String toString() { + return "CachingPostgreSQLDataSourceProvider{" + + "cacheSize=" + cacheSize + + '}'; + } + } + + public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; + + public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"; + + private BlogEntityProvider entityProvider = new BlogEntityProvider(); + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .build(); + + private Timer queryTimer = metricRegistry.timer("queryTimer"); + + public StatementCacheTest(Database database) { + super(database); + } + + @Parameterized.Parameters + public static Collection rdbmsDataSourceProvider() { + List providers = new ArrayList<>(); + /*providers.add(new DataSourceProvider[]{ + new CachingOracleDataSourceProvider(1) + }); + providers.add(new DataSourceProvider[]{ + new CachingOracleDataSourceProvider(0) + }); + providers.add(new DataSourceProvider[]{ + new CachingSQLServerDataSourceProvider(1) + }); + providers.add(new DataSourceProvider[]{ + new CachingSQLServerDataSourceProvider(0) + }); + providers.add(new DataSourceProvider[]{ + new CachingPostgreSQLDataSourceProvider(1) + }); + providers.add(new DataSourceProvider[]{ + new CachingPostgreSQLDataSourceProvider(0) + });*/ + + providers.add(new DataSourceProvider[]{ + new MySQLDataSourceProvider() + .setUseServerPrepStmts(false) + .setCachePrepStmts(false) + }); + + providers.add(new DataSourceProvider[]{ + new MySQLDataSourceProvider() + .setUseServerPrepStmts(true) + .setCachePrepStmts(false) + }); + + providers.add(new DataSourceProvider[]{ + new MySQLDataSourceProvider() + .setUseServerPrepStmts(false) + .setCachePrepStmts(true) + .setPrepStmtCacheSqlLimit(2048) + }); + + providers.add(new DataSourceProvider[]{ + new MySQLDataSourceProvider() + .setUseServerPrepStmts(true) + .setCachePrepStmts(true) + .setPrepStmtCacheSqlLimit(2048) + }); + + return providers; + } + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + protected boolean connectionPooling() { + return true; + } + + @Override + public void afterInit() { + doInJDBC(connection -> { + try ( + PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); + PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); + ) { + int postCount = getPostCount(); + int postCommentCount = getPostCommentCount(); + + int index; + + for (int i = 0; i < postCount; i++) { + index = 0; + postStatement.setString(++index, String.format("Post no. %1$d", i)); + postStatement.setInt(++index, 0); + postStatement.setLong(++index, i); + postStatement.executeUpdate(); + } + + for (int i = 0; i < postCount; i++) { + for (int j = 0; j < postCommentCount; j++) { + index = 0; + postCommentStatement.setLong(++index, i); + postCommentStatement.setString(++index, String.format("Post comment %1$d", j)); + postCommentStatement.setInt(++index, (int) (Math.random() * 1000)); + postCommentStatement.setLong(++index, (postCommentCount * i) + j); + postCommentStatement.executeUpdate(); + } + } + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + } + + @Test + @Ignore + public void testMySQLStatementCaching() { + if(dataSourceProvider().database() != Database.MYSQL) { + return; + } + AtomicInteger queryCount = new AtomicInteger(); + doInJDBC(connection -> { + long ttlNanos = System.nanoTime() + getRunNanos(); + while (System.nanoTime() < ttlNanos) { + long startNanos = System.nanoTime(); + try (PreparedStatement statement = connection.prepareStatement(""" + SELECT p.title, pd.created_on + FROM post p + LEFT JOIN post_details pd ON p.id = pd.id + WHERE EXISTS ( + SELECT 1 FROM post_comment WHERE post_id = p.id + ) + ORDER BY p.id + LIMIT ? + OFFSET ? + """ + )) { + statement.setInt(1, 1); + statement.setInt(2, 100); + try(ResultSet resultSet = statement.executeQuery()) { + queryCount.incrementAndGet(); + } finally { + queryTimer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + } + } + } + }); + LOGGER.info("When using {}, throughput is {} statements", + dataSourceProvider(), + queryCount.get() + ); + logReporter.report(); + } + + @Test + @Ignore + public void testPostgreSQLStatementCaching() { + if(dataSourceProvider().database() != Database.POSTGRESQL) { + return; + } + AtomicInteger queryCount = new AtomicInteger(); + doInJDBC(connection -> { + long ttlNanos = System.nanoTime() + getRunNanos(); + while (System.nanoTime() < ttlNanos) { + long startNanos = System.nanoTime(); + try (PreparedStatement statement = connection.prepareStatement(""" + SELECT p.title, pd.created_on + FROM post p + LEFT JOIN post_details pd ON p.id = pd.id + WHERE EXISTS ( + SELECT 1 FROM post_comment WHERE post_id = p.id + ) + ORDER BY p.id + LIMIT ? + OFFSET ? + """ + )) { + statement.setInt(1, 1); + statement.setInt(2, 100); + try(ResultSet resultSet = statement.executeQuery()) { + queryCount.incrementAndGet(); + } finally { + queryTimer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + } + }); + LOGGER.info("When using {}, throughput is {} statements", + dataSourceProvider(), + queryCount.get() + ); + logReporter.report(); + } + + @Test + @Ignore + public void testStatementCaching2() { + long ttlMillis = System.currentTimeMillis() + getRunNanos(); + AtomicLong counterHolder = new AtomicLong(); + doInJDBC(connection -> { + long statementCount = 0; + while (System.currentTimeMillis() < ttlMillis) { + try (PreparedStatement statement = connection.prepareStatement(""" + select p.title + from post p + where p.id = ? + """ + )) { + statement.setInt(1, (int) (Math.random() * getPostCount())); + try (ResultSet resultSet = statement.executeQuery()) { + assertTrue(resultSet.next()); + } + statementCount++; + } catch (SQLException e) { + fail(e.getMessage()); + } + } + counterHolder.set(statementCount); + }); + LOGGER.info("When using {}, throughput is {} statements per minute", + dataSourceProvider(), + counterHolder.get()); + } + + protected int getPostCount() { + return 1000; + } + + protected int getPostCommentCount() { + return 5; + } + + protected long getRunNanos() { + return TimeUnit.MINUTES.toNanos(5); + } + + @Override + protected boolean proxyDataSource() { + return false; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/connection/ConnectionPoolCallTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/connection/ConnectionPoolCallTest.java new file mode 100644 index 000000000..dd33ff31e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/connection/ConnectionPoolCallTest.java @@ -0,0 +1,83 @@ +package com.vladmihalcea.hpjp.jdbc.connection; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.junit.Ignore; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.TimeUnit; + +/** + * @author Vlad Mihalcea + */ +@Ignore +public class ConnectionPoolCallTest extends DatabaseProviderIntegrationTest { + + private final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Timer timer = metricRegistry.timer("connectionTimer"); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .build(); + + private int warmingUpCount = 100; + private int connectionAcquisitionCount = 1000; + + public ConnectionPoolCallTest(Database database) { + super(database); + } + + @Override + protected Class[] entities() { + return new Class[]{}; + } + + @Test + public void testNoPooling() throws SQLException { + LOGGER.info("Test without pooling for {}", dataSourceProvider().database()); + test(dataSourceProvider().dataSource()); + } + + @Test + public void testPooling() throws SQLException { + LOGGER.info("Test with pooling for {}", dataSourceProvider().database()); + test(poolingDataSource()); + } + + private void test(DataSource dataSource) throws SQLException { + //Warming up + for (int i = 0; i < warmingUpCount; i++) { + try (Connection connection = dataSource.getConnection()) { + } + } + for (int i = 0; i < connectionAcquisitionCount; i++) { + long startNanos = System.nanoTime(); + try (Connection connection = dataSource.getConnection()) { + } + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + } + logReporter.report(); + } + + protected HikariDataSource poolingDataSource() { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(dataSourceProvider().url()); + config.setUsername(dataSourceProvider().username()); + config.setPassword(dataSourceProvider().password()); + return new HikariDataSource(config); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/connection/ConnectionPoolThreadsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/connection/ConnectionPoolThreadsTest.java new file mode 100644 index 000000000..a1b5770ba --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/connection/ConnectionPoolThreadsTest.java @@ -0,0 +1,58 @@ +package com.vladmihalcea.hpjp.jdbc.connection; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class ConnectionPoolThreadsTest extends AbstractTest { + + private final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Override + protected Class[] entities() { + return new Class[]{}; + } + + @Test + public void test() { + try(Connection connection = dataSource().getConnection()) { + executeSync(() -> { + try(Connection _connection = dataSource().getConnection()) { + assertTrue(connection != _connection); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected HikariConfig hikariConfig(DataSource dataSource) { + HikariConfig config = super.hikariConfig(dataSource); + config.setMinimumIdle(0); + return config; + } + + protected boolean connectionPooling() { + return true; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/connection/FlexyPoolAutoSizingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/connection/FlexyPoolAutoSizingTest.java new file mode 100644 index 000000000..9da69f182 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/connection/FlexyPoolAutoSizingTest.java @@ -0,0 +1,91 @@ +package com.vladmihalcea.hpjp.jdbc.connection; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.vladmihalcea.flexypool.FlexyPoolDataSource; +import com.vladmihalcea.flexypool.adaptor.HikariCPPoolAdapter; +import com.vladmihalcea.flexypool.config.FlexyPoolConfiguration; +import com.vladmihalcea.flexypool.strategy.IncrementPoolOnTimeoutConnectionAcquisitionStrategy; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.TimeUnit; + +/** + * @author Vlad Mihalcea + */ +public class FlexyPoolAutoSizingTest extends AbstractTest { + + private final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Timer timer = metricRegistry.timer("connectionTimer"); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .build(); + + private int warmingUpCount = 100; + private int connectionAcquisitionCount = 1000; + + @Override + protected Class[] entities() { + return new Class[]{}; + } + + @Test + public void testPooling() throws SQLException { + LOGGER.info("Test with pooling for {}", dataSourceProvider().database()); + DataSource poolingDataSource = poolingDataSource(); + } + + private void test(DataSource dataSource) throws SQLException { + //Warming up + for (int i = 0; i < warmingUpCount; i++) { + try (Connection connection = dataSource.getConnection()) { + } + } + for (int i = 0; i < connectionAcquisitionCount; i++) { + long startNanos = System.nanoTime(); + try (Connection connection = dataSource.getConnection()) { + } + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + } + logReporter.report(); + } + + protected DataSource poolingDataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(dataSourceProvider().url()); + hikariConfig.setUsername(dataSourceProvider().username()); + hikariConfig.setPassword(dataSourceProvider().password()); + hikariConfig.setMaximumPoolSize(1); + HikariDataSource connectionPoolDataSource = new HikariDataSource(hikariConfig); + + FlexyPoolConfiguration flexyPoolConfiguration = new FlexyPoolConfiguration + .Builder<>( + getClass().getSimpleName(), + connectionPoolDataSource, + HikariCPPoolAdapter.FACTORY + ) + .setMetricLogReporterMillis(TimeUnit.SECONDS.toMillis(15)) + .build(); + + FlexyPoolDataSource flexyPoolDataSource = new FlexyPoolDataSource<>( + flexyPoolConfiguration, + new IncrementPoolOnTimeoutConnectionAcquisitionStrategy.Factory<>(10, 200) + ); + + return flexyPoolDataSource; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/connection/OracleConnectionCallTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/connection/OracleConnectionCallTest.java similarity index 84% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/connection/OracleConnectionCallTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/connection/OracleConnectionCallTest.java index ba59f2491..b492cffe0 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/connection/OracleConnectionCallTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/connection/OracleConnectionCallTest.java @@ -1,6 +1,7 @@ -package com.vladmihalcea.book.hpjp.jdbc.connection; +package com.vladmihalcea.hpjp.jdbc.connection; -import com.vladmihalcea.book.hpjp.util.AbstractOracleXEIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractOracleIntegrationTest; +import org.junit.Ignore; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,7 +13,7 @@ /** * @author Vlad Mihalcea */ -public class OracleConnectionCallTest extends AbstractOracleXEIntegrationTest { +public class OracleConnectionCallTest extends AbstractOracleIntegrationTest { private final Logger LOGGER = LoggerFactory.getLogger(getClass()); @@ -24,6 +25,7 @@ protected Class[] entities() { } @Test + @Ignore public void testConnections() throws SQLException { LOGGER.info("Test without pooling for {}", dataSourceProvider().database()); simulateLowLatencyTransactions(dataSourceProvider().dataSource(), 10); diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/JDBCVsJPATest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/JDBCVsJPATest.java new file mode 100644 index 000000000..01df02a85 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/JDBCVsJPATest.java @@ -0,0 +1,138 @@ +package com.vladmihalcea.hpjp.jdbc.fetching; + +import com.vladmihalcea.hpjp.hibernate.forum.Post; +import com.vladmihalcea.hpjp.hibernate.forum.PostComment; +import com.vladmihalcea.hpjp.hibernate.forum.PostDetails; +import com.vladmihalcea.hpjp.hibernate.forum.Tag; +import com.vladmihalcea.hpjp.util.AbstractTest; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class JDBCVsJPATest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostDetails.class, + PostComment.class, + Tag.class + }; + } + + @Test + public void testWriteAndReadUsingJDBC() { + doInJDBC(connection -> { + int postCount = 100; + int batchSize = 50; + + try (PreparedStatement postStatement = connection.prepareStatement(""" + INSERT INTO post ( + id, + title + ) + VALUES ( + ?, + ? + ) + """ + )) { + for (int i = 1; i <= postCount; i++) { + if (i % batchSize == 0) { + postStatement.executeBatch(); + } + int index = 0; + postStatement.setLong(++index, i); + postStatement.setString(++index, String.format("High-Performance Java Persistence, review no. %1$d", i)); + postStatement.addBatch(); + } + postStatement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + + doInJDBC(connection -> { + int maxResults = 10; + + List posts = new ArrayList<>(); + + try (PreparedStatement preparedStatement = connection.prepareStatement(""" + SELECT + p.id AS id, + p.title AS title + FROM post p + ORDER BY p.id + LIMIT ? + """ + )) { + preparedStatement.setInt(1, maxResults); + + try (ResultSet resultSet = preparedStatement.executeQuery()) { + while (resultSet.next()) { + int index = 0; + posts.add( + new Post() + .setId(resultSet.getLong(++index)) + .setTitle(resultSet.getString(++index)) + ); + } + } + + } catch (SQLException e) { + fail(e.getMessage()); + } + assertEquals(maxResults, posts.size()); + }); + } + + @Test + public void testWriteAndReadUsingJPA() { + doInJPA(entityManager -> { + int postCount = 100; + + for (long i = 1; i <= postCount; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle( + String.format( + "High-Performance Java Persistence, review no. %1$d", + i + ) + ) + ); + } + }); + + doInJPA(entityManager -> { + int maxResults = 10; + + List posts = entityManager.createQuery(""" + select p + from Post p + order by p.id + """, Post.class) + .setMaxResults(maxResults) + .getResultList(); + + assertEquals(maxResults, posts.size()); + }); + } + + @Override + protected boolean proxyDataSource() { + return false; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/OracleResultSetLimitTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/OracleResultSetLimitTest.java similarity index 76% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/OracleResultSetLimitTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/OracleResultSetLimitTest.java index 150512e66..dd08c6fe4 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/OracleResultSetLimitTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/OracleResultSetLimitTest.java @@ -1,11 +1,9 @@ -package com.vladmihalcea.book.hpjp.jdbc.fetching; +package com.vladmihalcea.hpjp.jdbc.fetching; -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; - -import org.hibernate.engine.spi.RowSelection; +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.assertj.core.util.Arrays; import org.junit.Test; import org.junit.runners.Parameterized; @@ -25,7 +23,7 @@ * * @author Vlad Mihalcea */ -public class OracleResultSetLimitTest extends DataSourceProviderIntegrationTest { +public class OracleResultSetLimitTest extends DatabaseProviderIntegrationTest { public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; public static final String SELECT_POST = @@ -34,15 +32,15 @@ public class OracleResultSetLimitTest extends DataSourceProviderIntegrationTest private BlogEntityProvider entityProvider = new BlogEntityProvider(); - public OracleResultSetLimitTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); + public OracleResultSetLimitTest(Database database) { + super(database); } @Parameterized.Parameters - public static Collection rdbmsDataSourceProvider() { - List providers = new ArrayList<>(); - providers.add(new DataSourceProvider[]{new OracleDataSourceProvider()}); - return providers; + public static Collection databases() { + List databases = new ArrayList<>(); + databases.add(Arrays.array(Database.ORACLE)); + return databases; } @Override @@ -80,8 +78,6 @@ public void init() { @Test public void testLimit() { - RowSelection rowSelection = new RowSelection(); - rowSelection.setMaxRows(getMaxRows()); long startNanos = System.nanoTime(); doInJDBC(connection -> { try (PreparedStatement statement = connection.prepareStatement(SELECT_POST) diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetCursorTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetCursorTest.java similarity index 94% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetCursorTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetCursorTest.java index 64fdde2b4..4839df8b7 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetCursorTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetCursorTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.jdbc.fetching; +package com.vladmihalcea.hpjp.jdbc.fetching; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.Timer; -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; import org.junit.Test; @@ -22,7 +22,7 @@ * * @author Vlad Mihalcea */ -public class ResultSetCursorTest extends DataSourceProviderIntegrationTest { +public class ResultSetCursorTest extends DatabaseProviderIntegrationTest { public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; @@ -47,8 +47,8 @@ public class ResultSetCursorTest extends DataSourceProviderIntegrationTest { private BlogEntityProvider entityProvider = new BlogEntityProvider(); - public ResultSetCursorTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); + public ResultSetCursorTest(Database database) { + super(database); } @Override diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetFetchSizeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetFetchSizeTest.java new file mode 100644 index 000000000..ddf657e35 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetFetchSizeTest.java @@ -0,0 +1,129 @@ +package com.vladmihalcea.hpjp.jdbc.fetching; + +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.*; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; + +import org.junit.Test; +import org.junit.runners.Parameterized; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.fail; + +/** + * ResultSetFetchSizeTest - Test result set fetch size + * + * @author Vlad Mihalcea + */ +public class ResultSetFetchSizeTest extends DatabaseProviderIntegrationTest { + + public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; + + private BlogEntityProvider entityProvider = new BlogEntityProvider(); + + private final Integer fetchSize; + + public ResultSetFetchSizeTest(Database database, Integer fetchSize) { + super(database); + this.fetchSize = fetchSize; + } + + @Parameterized.Parameters + public static Collection parameters() { + List providers = new ArrayList<>(); + for (int i = 0; i < databases.length; i++) { + for (int j = 0; j < fetchSizes.length; j++) { + Integer fetchSize = fetchSizes[j]; + providers.add(new Object[] {databases[i], fetchSize}); + } + } + return providers; + } + + private static Integer[] fetchSizes = new Integer[] { + //null, 1, 10, 100, 1000, 10000 + 1, 10, 100, 1000, 10000 + }; + + private static Database[] databases = new Database[]{ + Database.ORACLE, + Database.SQLSERVER, + Database.POSTGRESQL, + Database.MYSQL, + }; + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + public void init() { + super.init(); + doInJDBC(connection -> { + try ( + PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); + ) { + int postCount = getPostCount(); + + int index; + + for (int i = 0; i < postCount; i++) { + if (i > 0 && i % 100 == 0) { + postStatement.executeBatch(); + } + index = 0; + postStatement.setString(++index, String.format("Post no. %1$d", i)); + postStatement.setInt(++index, 0); + postStatement.setLong(++index, i); + postStatement.addBatch(); + } + postStatement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + } + + @Test + public void testFetchSize() { + long startNanos = System.nanoTime(); + doInJDBC(connection -> { + try (PreparedStatement statement = connection.prepareStatement( + "select * from post" + )) { + if (fetchSize != null) { + statement.setFetchSize(fetchSize); + } + statement.execute(); + ResultSet resultSet = statement.getResultSet(); + while (resultSet.next()) { + resultSet.getLong(1); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + + }); + LOGGER.info("{} fetch size {} took {} millis", + dataSourceProvider().database(), + fetchSize != null ? fetchSize : "N/A", + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + } + + protected int getPostCount() { + return 10000; + } + + @Override + protected boolean proxyDataSource() { + return false; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetLimitTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetLimitTest.java new file mode 100644 index 000000000..4e12bc9c9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetLimitTest.java @@ -0,0 +1,246 @@ +package com.vladmihalcea.hpjp.jdbc.fetching; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.LegacyOracleDialect; +import com.vladmihalcea.hpjp.util.providers.OracleDataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.hibernate.dialect.pagination.LimitHandler; +import org.hibernate.query.spi.Limit; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * ResultSetLimitTest - Test limiting result set vs fetching and discarding rows + * + * @author Vlad Mihalcea + */ +public class ResultSetLimitTest extends DatabaseProviderIntegrationTest { + + public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; + + public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"; + + public static final String SELECT_POST_COMMENT = """ + SELECT pc.id AS pc_id, p.title AS p_title + FROM post_comment pc + INNER JOIN post p ON p.id = pc.post_id + ORDER BY pc_id + """; + + private BlogEntityProvider entityProvider = new BlogEntityProvider(); + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .build(); + + private Timer noLimitTimer = metricRegistry.timer("noLimitTimer"); + private Timer limitTimer = metricRegistry.timer("limitTimer"); + private Timer maxSizeTimer = metricRegistry.timer("maxSizeTimer"); + + public ResultSetLimitTest(Database database) { + super(database); + } + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + if(database() == Database.ORACLE) { + return new OracleDataSourceProvider() { + @Override + public String hibernateDialect() { + return LegacyOracleDialect.class.getName(); + } + }; + } + return super.dataSourceProvider(); + } + + public void afterInit() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJDBC(connection -> { + try ( + PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); + PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); + ) { + int postCount = getPostCount(); + int postCommentCount = getPostCommentCount(); + + int index; + + for (int i = 0; i < postCount; i++) { + if (i > 0 && i % 100 == 0) { + postStatement.executeBatch(); + } + index = 0; + postStatement.setString(++index, String.format("Post no. %1$d", i)); + postStatement.setInt(++index, 0); + postStatement.setLong(++index, i); + postStatement.addBatch(); + } + postStatement.executeBatch(); + + for (int i = 0; i < postCount; i++) { + for (int j = 0; j < postCommentCount; j++) { + index = 0; + postCommentStatement.setLong(++index, i); + postCommentStatement.setString(++index, String.format("Post comment %1$d", j)); + postCommentStatement.setInt(++index, (int) (Math.random() * 1000)); + postCommentStatement.setLong(++index, (postCommentCount * i) + j); + postCommentStatement.addBatch(); + if (j % 100 == 0) { + postCommentStatement.executeBatch(); + } + } + } + postCommentStatement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + if(database() != Database.MYSQL) { + executeStatement("CREATE INDEX idx_post_comment_post_id ON post_comment (post_id)"); + if(database() == Database.POSTGRESQL) { + executeStatement("VACUUM FULL ANALYZE"); + } + } + } + + @Test + public void test() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + doInJDBC(connection -> { + try (PreparedStatement statement = connection.prepareStatement( + SELECT_POST_COMMENT + )) { + for (int i = 0; i < runCount() / 10; i++) { + noLimit(statement); + } + for (int i = 0; i < runCount(); i++) { + long startNanos = System.nanoTime(); + noLimit(statement); + noLimitTimer.update((System.nanoTime() - startNanos), TimeUnit.NANOSECONDS); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + + doInJDBC(connection -> { + final Limit rowSelection = new Limit(); + rowSelection.setMaxRows(getMaxRows()); + LimitHandler limitHandler = dialect().getLimitHandler(); + String limitStatement = limitHandler.processSql(SELECT_POST_COMMENT, rowSelection); + try (PreparedStatement statement = connection.prepareStatement(limitStatement)) { + for (int i = 0; i < runCount() / 10; i++) { + limit(statement, limitHandler, rowSelection); + } + for (int i = 0; i < runCount(); i++) { + long startNanos = System.nanoTime(); + limit(statement, limitHandler, rowSelection); + limitTimer.update((System.nanoTime() - startNanos), TimeUnit.NANOSECONDS); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + + doInJDBC(connection -> { + try (PreparedStatement statement = connection.prepareStatement( + SELECT_POST_COMMENT + )) { + for (int i = 0; i < runCount() / 10; i++) { + maxSize(statement); + } + for (int i = 0; i < runCount(); i++) { + long startNanos = System.nanoTime(); + maxSize(statement); + maxSizeTimer.update((System.nanoTime() - startNanos), TimeUnit.NANOSECONDS); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + LOGGER.info("{} results:", database()); + logReporter.report(); + } + + private void noLimit(PreparedStatement statement) throws SQLException { + statement.setFetchSize(100); + statement.execute(); + ResultSet resultSet = statement.getResultSet(); + int count = 0; + while (resultSet.next()) { + resultSet.getLong(1); + count++; + } + assertEquals(getPostCount() * getPostCommentCount(), count); + } + + public void limit(PreparedStatement statement, LimitHandler limitHandler, Limit rowSelection) throws SQLException { + limitHandler.bindLimitParametersAtEndOfQuery(rowSelection, statement, 1); + statement.setInt(1, getMaxRows()); + statement.execute(); + int count = 0; + ResultSet resultSet = statement.getResultSet(); + while (resultSet.next()) { + resultSet.getLong(1); + count++; + } + assertEquals(getMaxRows(), count); + } + + public void maxSize(PreparedStatement statement) throws SQLException { + statement.setMaxRows(getMaxRows()); + ResultSet resultSet = statement.executeQuery(); + int count = 0; + while (resultSet.next()) { + resultSet.getLong(1); + count++; + } + assertEquals(getMaxRows(), count); + } + + protected int getPostCount() { + return 10_000; + } + + protected int getPostCommentCount() { + return 10; + } + + protected int getMaxRows() { + return 100; + } + + private int runCount() { + return 1000; + } + + @Override + protected boolean proxyDataSource() { + return false; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetProjectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetProjectionTest.java new file mode 100644 index 000000000..cbd325c0f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetProjectionTest.java @@ -0,0 +1,207 @@ +package com.vladmihalcea.hpjp.jdbc.fetching; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; + +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.fail; + +/** + * ResultSetColumnSizeTest - Test result set column size + * + * @author Vlad Mihalcea + */ +public class ResultSetProjectionTest extends DatabaseProviderIntegrationTest { + + public static final String INSERT_POST = """ + INSERT INTO post (title, version, id) + VALUES (?, ?, ?) + """; + + public static final String INSERT_POST_COMMENT = """ + INSERT INTO post_comment (post_id, review, version, id) + VALUES (?, ?, ?, ?) + """; + + public static final String INSERT_POST_DETAILS= """ + INSERT INTO post_details (id, created_on, version) + VALUES (?, ?, ?) + """; + + public static final String SELECT_ALL = """ + SELECT * + FROM post_comment pc + LEFT JOIN post p ON p.id = pc.post_id + LEFT JOIN post_details pd ON p.id = pd.id + """; + + public static final String SELECT_ID = """ + SELECT pc.id, pc.review + FROM post_comment pc + LEFT JOIN post p ON p.id = pc.post_id + LEFT JOIN post_details pd ON p.id = pd.id + """; + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Timer fetchAllColumnsTimer = metricRegistry.timer("fetchAllColumnsTimer"); + private Timer fetchProjectionTimer = metricRegistry.timer("fetchProjectionTimer"); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .build(); + + private BlogEntityProvider entityProvider = new BlogEntityProvider(); + + public ResultSetProjectionTest(Database database) { + super(database); + } + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + public void afterInit() { + doInJDBC(connection -> { + LOGGER.info("{} supports CLOSE_CURSORS_AT_COMMIT {}", + dataSourceProvider().database(), + connection.getMetaData().supportsResultSetHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT) + ); + + LOGGER.info("{} supports HOLD_CURSORS_OVER_COMMIT {}", + dataSourceProvider().database(), + connection.getMetaData().supportsResultSetHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT) + ); + + try ( + PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); + PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); + PreparedStatement postDetailsStatement = connection.prepareStatement(INSERT_POST_DETAILS); + ) { + + if (postStatement.getResultSetHoldability() == ResultSet.CLOSE_CURSORS_AT_COMMIT) { + LOGGER.info("{} default holdability CLOSE_CURSORS_AT_COMMIT", + dataSourceProvider().database() + ); + } else if (postStatement.getResultSetHoldability() == ResultSet.HOLD_CURSORS_OVER_COMMIT) { + LOGGER.info("{} default holdability HOLD_CURSORS_OVER_COMMIT", + dataSourceProvider().database() + ); + } else { + fail(); + } + + int postCount = getPostCount(); + int postCommentCount = getPostCommentCount(); + + int index; + + for (int i = 0; i < postCount; i++) { + if (i > 0 && i % 100 == 0) { + postStatement.executeBatch(); + postDetailsStatement.executeBatch(); + } + + index = 0; + postStatement.setString(++index, String.format("Post no. %1$d", i)); + postStatement.setInt(++index, 0); + postStatement.setLong(++index, i); + postStatement.addBatch(); + + index = 0; + postDetailsStatement.setInt(++index, i); + postDetailsStatement.setTimestamp(++index, new Timestamp(System.currentTimeMillis())); + postDetailsStatement.setInt(++index, 0); + postDetailsStatement.addBatch(); + } + postStatement.executeBatch(); + postDetailsStatement.executeBatch(); + + for (int i = 0; i < postCount; i++) { + for (int j = 0; j < postCommentCount; j++) { + index = 0; + postCommentStatement.setLong(++index, i); + postCommentStatement.setString(++index, String.format("Post comment %1$d", j)); + postCommentStatement.setInt(++index, (int) (Math.random() * 1000)); + postCommentStatement.setLong(++index, (postCommentCount * i) + j); + postCommentStatement.addBatch(); + if (j % 100 == 0) { + postCommentStatement.executeBatch(); + } + } + } + postCommentStatement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + executeStatement("CREATE INDEX idx_post_comment_review ON post_comment (review)"); + if(database() == Database.POSTGRESQL) { + executeStatement("VACUUM FULL ANALYZE"); + } + } + + @Test + public void test() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + testInternal(SELECT_ALL, fetchAllColumnsTimer); + testInternal(SELECT_ID, fetchProjectionTimer); + LOGGER.info("{} results:", database()); + logReporter.report(); + } + + public void testInternal(String sql, Timer timer) { + doInJDBC(connection -> { + LOGGER.info("Fetching {} on {}", sql, database()); + for (int i = 0; i < runCount(); i++) { + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.execute(); + long startNanos = System.nanoTime(); + ResultSet resultSet = statement.getResultSet(); + while (resultSet.next()) { + Object[] values = new Object[resultSet.getMetaData().getColumnCount()]; + for (int j = 0; j < values.length; j++) { + values[j] = resultSet.getObject(j + 1); + } + } + timer.update(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); + } catch (SQLException e) { + fail(e.getMessage()); + } + } + }); + } + + private int runCount() { + return 1000; + } + + protected int getPostCount() { + return 100; + } + + protected int getPostCommentCount() { + return 10; + } + + @Override + protected boolean proxyDataSource() { + return false; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetScollabilityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetScollabilityTest.java similarity index 90% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetScollabilityTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetScollabilityTest.java index 73f419a37..a994beb8a 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/ResultSetScollabilityTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/ResultSetScollabilityTest.java @@ -1,11 +1,11 @@ -package com.vladmihalcea.book.hpjp.jdbc.fetching; +package com.vladmihalcea.hpjp.jdbc.fetching; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.Timer; -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; import org.junit.Test; @@ -21,7 +21,7 @@ * * @author Vlad Mihalcea */ -public class ResultSetScollabilityTest extends DataSourceProviderIntegrationTest { +public class ResultSetScollabilityTest extends DatabaseProviderIntegrationTest { public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; @@ -36,8 +36,8 @@ public class ResultSetScollabilityTest extends DataSourceProviderIntegrationTest private BlogEntityProvider entityProvider = new BlogEntityProvider(); - public ResultSetScollabilityTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); + public ResultSetScollabilityTest(Database database) { + super(database); } @Override diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/SQLServerResultSetLimitTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/SQLServerResultSetLimitTest.java similarity index 83% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/SQLServerResultSetLimitTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/SQLServerResultSetLimitTest.java index d25025ee7..8a9a04ca1 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/SQLServerResultSetLimitTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/SQLServerResultSetLimitTest.java @@ -1,11 +1,9 @@ -package com.vladmihalcea.book.hpjp.jdbc.fetching; +package com.vladmihalcea.hpjp.jdbc.fetching; -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; - -import org.hibernate.engine.spi.RowSelection; +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.assertj.core.util.Arrays; import org.junit.Test; import org.junit.runners.Parameterized; @@ -25,7 +23,7 @@ * * @author Vlad Mihalcea */ -public class SQLServerResultSetLimitTest extends DataSourceProviderIntegrationTest { +public class SQLServerResultSetLimitTest extends DatabaseProviderIntegrationTest { public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"; @@ -41,15 +39,15 @@ public class SQLServerResultSetLimitTest extends DataSourceProviderIntegrationTe private BlogEntityProvider entityProvider = new BlogEntityProvider(); - public SQLServerResultSetLimitTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); + public SQLServerResultSetLimitTest(Database database) { + super(database); } @Parameterized.Parameters - public static Collection rdbmsDataSourceProvider() { - List providers = new ArrayList<>(); - providers.add(new DataSourceProvider[]{new SQLServerDataSourceProvider()}); - return providers; + public static Collection databases() { + List databases = new ArrayList<>(); + databases.add(Arrays.array(Database.SQLSERVER)); + return databases; } @Override @@ -104,8 +102,6 @@ public void init() { @Test public void testLimit() { - RowSelection rowSelection = new RowSelection(); - rowSelection.setMaxRows(getMaxRows()); long startNanos = System.nanoTime(); doInJDBC(connection -> { try (PreparedStatement statement1 = connection.prepareStatement(SELECT_POST_COMMENT_1); diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/SQLStandardResultSetLimitTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/SQLStandardResultSetLimitTest.java similarity index 81% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/SQLStandardResultSetLimitTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/SQLStandardResultSetLimitTest.java index 2b15f270a..f7563240d 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/fetching/SQLStandardResultSetLimitTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/fetching/SQLStandardResultSetLimitTest.java @@ -1,12 +1,9 @@ -package com.vladmihalcea.book.hpjp.jdbc.fetching; +package com.vladmihalcea.hpjp.jdbc.fetching; -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; - -import org.hibernate.engine.spi.RowSelection; +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.assertj.core.util.Arrays; import org.junit.Test; import org.junit.runners.Parameterized; @@ -26,7 +23,7 @@ * * @author Vlad Mihalcea */ -public class SQLStandardResultSetLimitTest extends DataSourceProviderIntegrationTest { +public class SQLStandardResultSetLimitTest extends DatabaseProviderIntegrationTest { public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"; @@ -49,16 +46,16 @@ public class SQLStandardResultSetLimitTest extends DataSourceProviderIntegration private BlogEntityProvider entityProvider = new BlogEntityProvider(); - public SQLStandardResultSetLimitTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); + public SQLStandardResultSetLimitTest(Database database) { + super(database); } @Parameterized.Parameters - public static Collection rdbmsDataSourceProvider() { - List providers = new ArrayList<>(); - providers.add(new DataSourceProvider[]{new SQLServerDataSourceProvider()}); - providers.add(new DataSourceProvider[]{new PostgreSQLDataSourceProvider()}); - return providers; + public static Collection databases() { + List databases = new ArrayList<>(); + databases.add(Arrays.array(Database.SQLSERVER)); + databases.add(Arrays.array(Database.POSTGRESQL)); + return databases; } @Override @@ -113,8 +110,6 @@ public void init() { @Test public void testLimit() { - RowSelection rowSelection = new RowSelection(); - rowSelection.setMaxRows(getMaxRows()); long startNanos = System.nanoTime(); doInJDBC(connection -> { try (PreparedStatement statement = connection.prepareStatement(SELECT_POST_COMMENT); diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/index/MySQLIndexSelectivityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/index/MySQLIndexSelectivityTest.java new file mode 100644 index 000000000..ce338c4e9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/index/MySQLIndexSelectivityTest.java @@ -0,0 +1,191 @@ +package com.vladmihalcea.hpjp.jdbc.index; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; +import jakarta.persistence.*; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static org.junit.Assert.fail; + +/** + * PostgresIndexSelectivityTest - Test PostgreSQL index selectivity + * + * @author Vlad Mihalcea + */ +public class MySQLIndexSelectivityTest extends AbstractTest { + + public static final String INSERT_TASK = "INSERT INTO task (id, name, status) VALUES (?, ?, ?)"; + + @Override + protected Class[] entities() { + return new Class[] { + Task.class + }; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new MySQLDataSourceProvider() + .setRewriteBatchedStatements(true); + } + + @Test + public void testInsert() { + AtomicInteger statementCount = new AtomicInteger(); + long startNanos = System.nanoTime(); + doInJDBC(connection -> { + try (PreparedStatement statement = connection.prepareStatement(INSERT_TASK)) { + int taskCount = getTaskCount(); + + for (int i = 0; i < taskCount; i++) { + String status = Task.Status.DONE.name(); + if (i >= (0.99 * taskCount)) { + status = Task.Status.TO_DO.name(); + } else if (i >= (0.95 * taskCount)) { + status = Task.Status.FAILED.name(); + } + statement.setLong(1, i); + statement.setString(2, String.format("Task %d", i)); + statement.setString(3, status); + executeStatement(statement, statementCount); + } + statement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + LOGGER.info("{}.testInsert took {} millis", + getClass().getSimpleName(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + + boolean createIndex = false; + + if (createIndex) { + executeStatement("CREATE INDEX idx_task_status ON task (status)"); + executeStatement("OPTIMIZE TABLE task"); + } + + boolean printExecutionPlans = false; + + doInJDBC(connection -> { + executeQueryWithSkewedFilterPredicate(connection, Task.Status.DONE, printExecutionPlans); + executeQueryWithSkewedFilterPredicate(connection, Task.Status.TO_DO, printExecutionPlans); + }); + } + + private void executeStatement(PreparedStatement statement, AtomicInteger statementCount) throws SQLException { + statement.addBatch(); + int count = statementCount.incrementAndGet(); + if(count % getBatchSize() == 0) { + statement.executeBatch(); + } + } + + protected int getTaskCount() { + return 10_000 * 1_000; + } + + protected int getBatchSize() { + return 1000; + } + + private void executeQueryWithSkewedFilterPredicate(Connection connection, Task.Status status, boolean printExecutionPlans) throws SQLException { + String query = String.format(""" + %sSELECT id, name + FROM task + WHERE status = ? + """, + printExecutionPlans ? "EXPLAIN FORMAT=JSON " : "" + ); + + try (PreparedStatement statement = connection.prepareStatement(query)) { + statement.setString(1, status.name()); + ResultSet resultSet = statement.executeQuery(); + + if (printExecutionPlans) { + List planLines = new ArrayList<>(); + while (resultSet.next()) { + planLines.add(resultSet.getString(1)); + } + LOGGER.info("Execution plan: {}{}", + System.lineSeparator(), + planLines.stream().collect(Collectors.joining(System.lineSeparator())) + ); + } else { + int rowCount = 0; + while (resultSet.next()) { + rowCount++; + } + LOGGER.info("Fetched [{}] records for [{}] status", + rowCount, + status + ); + } + } + } + + @Entity(name = "Task") + @Table(name = "task") + public static class Task { + + @Id + private Long id; + + private String name; + + @Enumerated(EnumType.STRING) + private Status status; + + public Long getId() { + return id; + } + + public Task setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Task setName(String name) { + this.name = name; + return this; + } + + public Status getStatus() { + return status; + } + + public Task setStatus(Status status) { + this.status = status; + return this; + } + + public enum Status { + DONE, + TO_DO, + FAILED; + + public static Status random() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + Status[] values = Status.values(); + return values[random.nextInt(values.length)]; + } + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/index/PostgreSQLIndexSelectivityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/index/PostgreSQLIndexSelectivityTest.java new file mode 100644 index 000000000..c53793bc0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/index/PostgreSQLIndexSelectivityTest.java @@ -0,0 +1,200 @@ +package com.vladmihalcea.hpjp.jdbc.index; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.queries.PostgreSQLQueries; +import jakarta.persistence.*; +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; +import org.junit.Test; +import org.postgresql.util.PGobject; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static org.junit.Assert.*; + +/** + * PostgresIndexSelectivityTest - Test PostgreSQL index selectivity + * + * @author Vlad Mihalcea + */ +public class PostgreSQLIndexSelectivityTest extends AbstractPostgreSQLIntegrationTest { + + public static final String INSERT_TASK = "INSERT INTO task (id, name, status) VALUES (?, ?, ?)"; + + @Override + protected Class[] entities() { + return new Class[] { + Task.class + }; + } + + @Override + protected void beforeInit() { + executeStatement("DROP TYPE IF EXISTS task_status"); + executeStatement("CREATE TYPE task_status AS ENUM ('TO_DO', 'DONE', 'FAILED')"); + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "50"); + } + + @Test + public void testSelectivity() { + AtomicInteger statementCount = new AtomicInteger(); + long startNanos = System.nanoTime(); + doInJDBC(connection -> { + try (PreparedStatement statement = connection.prepareStatement(INSERT_TASK)) { + int taskCount = getTaskCount(); + + for (int i = 1; i <= taskCount; i++) { + Task.Status status = Task.Status.DONE; + if (i > 99000) { + status = Task.Status.TO_DO; + } else if (i > 95000) { + status = Task.Status.FAILED; + } + statement.setLong(1, i); + statement.setString(2, String.format("Task %d", i)); + statement.setObject(3, PostgreSQLQueries.toEnum(status, "task_status"), Types.OTHER); + executeStatement(statement, statementCount); + } + statement.executeBatch(); + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + LOGGER.info("{}.testInsert took {} millis", + getClass().getSimpleName(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + executeStatement("CREATE INDEX IF NOT EXISTS idx_task_status ON task (status)"); + executeStatement("VACUUM ANALYZE"); + + doInJDBC(connection -> { + printExecutionPlanForSelectByStatus(connection, Task.Status.DONE); + printExecutionPlanForSelectByStatus(connection, Task.Status.TO_DO); + }); + + LOGGER.info("Using a Partial Index"); + + executeStatement("DROP INDEX IF EXISTS idx_task_status"); + executeStatement("CREATE INDEX idx_task_status ON task (status) WHERE status <> 'DONE'"); + executeStatement("VACUUM ANALYZE"); + + doInJDBC(connection -> { + printExecutionPlanForSelectByStatus(connection, Task.Status.DONE); + printExecutionPlanForSelectByStatus(connection, Task.Status.TO_DO); + }); + } + + private void executeStatement(PreparedStatement statement, AtomicInteger statementCount) throws SQLException { + statement.addBatch(); + int count = statementCount.incrementAndGet(); + if(count % getBatchSize() == 0) { + statement.executeBatch(); + } + } + + protected int getTaskCount() { + return 100 * 1000; + } + + protected int getBatchSize() { + return 100; + } + + private void printExecutionPlanForSelectByStatus(Connection connection, Task.Status status) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement(""" + EXPLAIN ANALYZE + SELECT * + FROM task + WHERE status = ? + """ + )) { + + assertFalse(PostgreSQLQueries.isUseServerPrepare(statement)); + PostgreSQLQueries.setPrepareThreshold(statement, 1); + statement.setObject(1, PostgreSQLQueries.toEnum(status, "task_status"), Types.OTHER); + ResultSet resultSet = statement.executeQuery(); + + List planLines = new ArrayList<>(); + while (resultSet.next()) { + planLines.add(resultSet.getString(1)); + } + LOGGER.info("Execution plan: {}{}", + System.lineSeparator(), + planLines.stream().collect(Collectors.joining(System.lineSeparator())) + ); + + assertTrue(PostgreSQLQueries.isUseServerPrepare(statement)); + } + } + + @Entity(name = "Task") + @Table(name = "task") + public static class Task { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @Column(length = 50) + private String name; + + @Column(columnDefinition = "task_status") + @JdbcType(PostgreSQLEnumJdbcType.class) + private Task.Status status; + + public Long getId() { + return id; + } + + public Task setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Task setName(String name) { + this.name = name; + return this; + } + + public Task.Status getStatus() { + return status; + } + + public Task setStatus(Task.Status status) { + this.status = status; + return this; + } + + public enum Status { + DONE, + TO_DO, + FAILED; + + public static Task.Status random() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + Task.Status[] values = Task.Status.values(); + return values[random.nextInt(values.length)]; + } + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/index/SQLServerMissingIndexTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/index/SQLServerMissingIndexTest.java new file mode 100644 index 000000000..71eb99d2a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/index/SQLServerMissingIndexTest.java @@ -0,0 +1,81 @@ +package com.vladmihalcea.hpjp.jdbc.index; + +import com.vladmihalcea.hpjp.jdbc.index.providers.IndexEntityProvider; +import com.vladmihalcea.hpjp.jdbc.index.providers.IndexEntityProvider.Task; +import com.vladmihalcea.hpjp.util.AbstractSQLServerIntegrationTest; +import org.junit.Test; + +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertFalse; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerMissingIndexTest extends AbstractSQLServerIntegrationTest { + + private final IndexEntityProvider entityProvider = new IndexEntityProvider(); + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Override + protected void additionalProperties(Properties properties) { + properties.put("hibernate.jdbc.batch_size", "500"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } + + @Test + public void testInsert() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return; + } + long startNanos = System.nanoTime(); + doInJPA(entityManager -> { + int taskCount = getPostCount(); + + for (int i = 1; i <= taskCount; i++) { + Task.Status status = Task.Status.DONE; + if (i >= 99000) { + status = Task.Status.TO_DO; + } else if (i >= 95000) { + status = Task.Status.FAILED; + } + entityManager.persist( + new Task().setStatus(status) + ); + } + }); + LOGGER.info("{}.testInsert took {} millis", + getClass().getSimpleName(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + for (int i = 1; i <= 100; i++) { + doInJPA(entityManager -> { + List tasks = entityManager.createQuery(""" + select t + from task t + where t.status =:status + """, Task.class) + .setParameter("status", Task.Status.random()) + .getResultList(); + + }); + } + + LOGGER.info("Check missing indexes"); + //TODO: Not done + } + + protected int getPostCount() { + return 100 * 1000; + } + + protected int getBatchSize() { + return 100; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/index/providers/IndexEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/index/providers/IndexEntityProvider.java new file mode 100644 index 000000000..561c7ba0f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/index/providers/IndexEntityProvider.java @@ -0,0 +1,62 @@ +package com.vladmihalcea.hpjp.jdbc.index.providers; + +import com.vladmihalcea.hpjp.util.EntityProvider; + +import jakarta.persistence.*; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author Vlad Mihalcea + */ +public class IndexEntityProvider implements EntityProvider { + + @Override + public Class[] entities() { + return new Class[]{ + Task.class + }; + } + + @Entity(name = "Task") + @Table(name = "task") + public static class Task { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Enumerated(EnumType.STRING) + private Status status; + + public Long getId() { + return id; + } + + public Task setId(Long id) { + this.id = id; + return this; + } + + public Status getStatus() { + return status; + } + + public Task setStatus(Status status) { + this.status = status; + return this; + } + + public enum Status { + DONE, + TO_DO, + FAILED; + + public static Status random() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + Status[] values = Status.values(); + return values[random.nextInt(values.length)]; + } + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/AutoCommitTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/AutoCommitTest.java similarity index 90% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/AutoCommitTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/AutoCommitTest.java index b789159e5..8a7bc2868 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/AutoCommitTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/AutoCommitTest.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction; +package com.vladmihalcea.hpjp.jdbc.transaction; -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import com.vladmihalcea.book.hpjp.util.exception.DataAccessException; -import com.vladmihalcea.book.hpjp.util.providers.entity.BankEntityProvider; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.DataAccessException; +import com.vladmihalcea.hpjp.util.providers.entity.BankEntityProvider; import org.junit.Test; import javax.sql.DataSource; @@ -21,8 +21,6 @@ public class AutoCommitTest extends AbstractTest { private BankEntityProvider entityProvider = new BankEntityProvider(); - private DataSource dataSource = newDataSource(); - @Override protected Class[] entities() { return entityProvider.entities(); @@ -56,7 +54,7 @@ public void testAutoCommit() throws SQLException { long fromAccountId = 1; long toAccountId = 2; - DataSource dataSource = newDataSource(); + DataSource dataSource = dataSource(); try(Connection connection = dataSource.getConnection(); PreparedStatement transferStatement = connection.prepareStatement( @@ -81,7 +79,7 @@ public void testManualCommit() throws SQLException { long fromAccountId = 1; long toAccountId = 2; - DataSource dataSource = newDataSource(); + DataSource dataSource = dataSource(); try(Connection connection = dataSource.getConnection()) { connection.setAutoCommit(false); diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/ConnectionReadyOnlyTransactionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/ConnectionReadyOnlyTransactionTest.java similarity index 77% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/ConnectionReadyOnlyTransactionTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/ConnectionReadyOnlyTransactionTest.java index 24965cfe5..87c71adbe 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/ConnectionReadyOnlyTransactionTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/ConnectionReadyOnlyTransactionTest.java @@ -1,9 +1,8 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction; - -import com.vladmihalcea.book.hpjp.util.DataSourceProviderIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.entity.BlogEntityProvider; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; +package com.vladmihalcea.hpjp.jdbc.transaction; +import com.vladmihalcea.hpjp.util.DatabaseProviderIntegrationTest; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; import org.junit.Test; import java.sql.Connection; @@ -15,13 +14,13 @@ * * @author Vlad Mihalcea */ -public class ConnectionReadyOnlyTransactionTest extends DataSourceProviderIntegrationTest { +public class ConnectionReadyOnlyTransactionTest extends DatabaseProviderIntegrationTest { public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; private BlogEntityProvider entityProvider = new BlogEntityProvider(); - public ConnectionReadyOnlyTransactionTest(DataSourceProvider dataSourceProvider) { - super(dataSourceProvider); + public ConnectionReadyOnlyTransactionTest(Database database) { + super(database); } @Override diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/MVCCPostgreSQLTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/MVCCPostgreSQLTest.java similarity index 93% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/MVCCPostgreSQLTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/MVCCPostgreSQLTest.java index 178f41a1a..2d57a27e1 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/MVCCPostgreSQLTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/MVCCPostgreSQLTest.java @@ -1,14 +1,14 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction; +package com.vladmihalcea.hpjp.jdbc.transaction; -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import org.hibernate.query.Query; import org.hibernate.transform.AliasToBeanResultTransformer; import org.junit.Test; -import javax.persistence.Entity; -import javax.persistence.EntityManager; -import javax.persistence.Id; -import javax.persistence.Table; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -166,12 +166,6 @@ private PostWithXminAndXmax getPost(EntityManager entityManager, Integer id) { return !result.isEmpty() ? result.get(0) : null; } - private String transactionId(EntityManager entityManager) { - return (String) entityManager.createNativeQuery( - "SELECT CAST(txid_current() AS text) ") - .getSingleResult(); - } - @Entity(name = "Post") @Table(name = "post") public static class Post { diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/OracleConnectionReadyOnlyTransactionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/OracleConnectionReadyOnlyTransactionTest.java new file mode 100644 index 000000000..da5c43336 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/OracleConnectionReadyOnlyTransactionTest.java @@ -0,0 +1,41 @@ +package com.vladmihalcea.hpjp.jdbc.transaction; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.OracleDataSourceProvider; +import org.assertj.core.util.Arrays; +import org.junit.runners.Parameterized; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * OracleConnectionReadyOnlyTransactionTest - Test to verify Oracle driver supports read-only transactions + * + * @author Vlad Mihalcea + */ +public class OracleConnectionReadyOnlyTransactionTest extends ConnectionReadyOnlyTransactionTest { + + public OracleConnectionReadyOnlyTransactionTest(Database database) { + super(database); + } + + @Parameterized.Parameters + public static Collection databases() { + List databases = new ArrayList<>(); + databases.add(Arrays.array(Database.ORACLE)); + return databases; + } + + protected void setReadOnly(Connection connection) throws SQLException { + connection.setAutoCommit(false); + try(CallableStatement statement = connection.prepareCall("begin set transaction read only; end;")) { + statement.execute(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/SQLServerConnectionReadyOnlyTransactionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/SQLServerConnectionReadyOnlyTransactionTest.java new file mode 100644 index 000000000..1fb748687 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/SQLServerConnectionReadyOnlyTransactionTest.java @@ -0,0 +1,43 @@ +package com.vladmihalcea.hpjp.jdbc.transaction; + +import com.microsoft.sqlserver.jdbc.SQLServerDataSource; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.SQLServerDataSourceProvider; +import org.assertj.core.util.Arrays; +import org.junit.runners.Parameterized; + +import javax.sql.DataSource; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * SQLServerConnectionReadyOnlyTransactionTest - Test to verify SQL Server driver supports read-only transactions + * + * @author Vlad Mihalcea + */ +public class SQLServerConnectionReadyOnlyTransactionTest extends ConnectionReadyOnlyTransactionTest { + + public SQLServerConnectionReadyOnlyTransactionTest(Database database) { + super(database); + } + + @Parameterized.Parameters + public static Collection databases() { + List databases = new ArrayList<>(); + databases.add(Arrays.array(Database.SQLSERVER)); + return databases; + } + + protected DataSourceProvider dataSourceProvider() { + return new SQLServerDataSourceProvider() { + @Override + public DataSource dataSource() { + SQLServerDataSource dataSource = (SQLServerDataSource) super.dataSource(); + dataSource.setURL(dataSource.getURL() + ";ApplicationIntent=ReadOnly"); + return dataSource; + } + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/AbstractPredicateLockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/AbstractPredicateLockTest.java new file mode 100644 index 000000000..722806f88 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/AbstractPredicateLockTest.java @@ -0,0 +1,280 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.locking; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import jakarta.persistence.*; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Session; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author Vlad Mihalcea + */ +public abstract class AbstractPredicateLockTest extends AbstractTest { + + public static final int WAIT_MILLIS = 500; + + protected final CountDownLatch aliceLatch = new CountDownLatch(1); + protected final CountDownLatch bobLatch = new CountDownLatch(1); + + protected final AtomicLong POST_COMMENT_ID = new AtomicLong(); + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + for (long i = 1; i <= 3; i++) { + entityManager.persist( + new PostComment() + .setId(POST_COMMENT_ID.incrementAndGet()) + .setReview(String.format("Comment nr. %d", i)) + .setPost(post) + ); + } + }); + + } + + @Test + public void testRangeLockPreventsInsert() throws SQLException { + AtomicBoolean prevented = new AtomicBoolean(); + + doInHibernate( session -> { + session.doWork(this::prepareConnection); + List comments = session.createQuery( + "select c " + + "from PostComment c " + + "where c.post.id = :id", PostComment.class) + .setParameter("id", 1L) + .setLockOptions(new LockOptions(LockMode.PESSIMISTIC_WRITE)) + .getResultList(); + + executeAsync(() -> { + try { + doInHibernate(_session -> { + _session.doWork(this::prepareConnection); + + Post post = _session.getReference(Post.class, 1L); + + PostComment comment = new PostComment(); + comment.setId((long) comments.size() + 1); + comment.setReview(String.format("Comment nr. %d", comments.size() + 1)); + comment.setPost(post); + + _session.persist(comment); + + aliceLatch.countDown(); + _session.flush(); + LOGGER.info("Insert {} prevented by explicit lock", prevented.get() ? "was" : "was not"); + bobLatch.countDown(); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + prevented.set(true); + LOGGER.info("Insert {} prevented by explicit lock", prevented.get() ? "was" : "was not"); + bobLatch.countDown(); + } + } + }); + + awaitOnLatch(aliceLatch); + sleep(WAIT_MILLIS); + LOGGER.info("Alice woke up!"); + prevented.set(true); + } ); + awaitOnLatch(bobLatch); + } + + @Override + protected boolean nativeHibernateSessionFactoryBootstrap() { + return true; + } + + @Test + public void testRangeLockPreventsDelete() throws SQLException { + AtomicBoolean prevented = new AtomicBoolean(); + + doInHibernate( session -> { + session.unwrap(Session.class).doWork(this::prepareConnection); + + List comments = session.createQuery( + "select c " + + "from PostComment c " + + "where c.post.id = :id", PostComment.class) + .setParameter("id", 1L) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .getResultList(); + + executeAsync(() -> { + try { + doInHibernate(_session -> { + _session.unwrap(Session.class).doWork(this::prepareConnection); + + aliceLatch.countDown(); + _session.createNativeQuery( + "delete from post_comment where id = :id ") + .setParameter("id", 1L) + .executeUpdate(); + + LOGGER.info("Delete {} prevented by explicit lock", prevented.get() ? "was" : "was not"); + bobLatch.countDown(); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + prevented.set(true); + LOGGER.info("Delete {} prevented by explicit lock", prevented.get() ? "was" : "was not"); + bobLatch.countDown(); + } + } + }); + + awaitOnLatch(aliceLatch); + sleep(WAIT_MILLIS); + LOGGER.info("Alice woke up!"); + prevented.set(true); + } ); + awaitOnLatch(bobLatch); + } + + @Test + public void testRangeLockPreventsUpdate() throws SQLException { + AtomicBoolean prevented = new AtomicBoolean(); + + doInHibernate( session -> { + session.unwrap(Session.class).doWork(this::prepareConnection); + + List comments = session.createQuery( + "select c " + + "from PostComment c " + + "where c.post.id = :id", PostComment.class) + .setParameter("id", 1L) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .getResultList(); + + executeAsync(() -> { + try { + doInHibernate(_session -> { + _session.unwrap(Session.class).doWork(this::prepareConnection); + + aliceLatch.countDown(); + _session.createQuery( + "update PostComment " + + "set review = :review " + + "where id = :id") + .setParameter("review", "Great") + .setParameter("id", 1L) + .executeUpdate(); + + LOGGER.info("Update {} prevented by explicit lock", prevented.get() ? "was" : "was not"); + bobLatch.countDown(); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + prevented.set(true); + LOGGER.info("Update {} prevented by explicit lock", prevented.get() ? "was" : "was not"); + bobLatch.countDown(); + } + } + }); + + awaitOnLatch(aliceLatch); + sleep(WAIT_MILLIS); + LOGGER.info("Alice woke up!"); + prevented.set(true); + } ); + awaitOnLatch(bobLatch); + } + + protected void prepareConnection(Connection connection) { + setJdbcTimeout(connection); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/AbstractTableLockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/AbstractTableLockTest.java similarity index 98% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/AbstractTableLockTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/AbstractTableLockTest.java index b9d2c6cc9..7fd98d1a3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/AbstractTableLockTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/AbstractTableLockTest.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking; +package com.vladmihalcea.hpjp.jdbc.transaction.locking; -import com.vladmihalcea.book.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; import org.junit.Test; -import javax.persistence.*; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/MySQLPredicateLockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/MySQLPredicateLockTest.java new file mode 100644 index 000000000..1aef1b193 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/MySQLPredicateLockTest.java @@ -0,0 +1,29 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.locking; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; + +import java.sql.Connection; +import java.sql.SQLException; + +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +public class MySQLPredicateLockTest extends AbstractPredicateLockTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new MySQLDataSourceProvider(); + } + + protected void prepareConnection(Connection connection) { + try { + connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + } catch (SQLException e) { + fail(e.getMessage()); + } + executeStatement(connection, "SET GLOBAL innodb_lock_wait_timeout = 1"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/MySQLTableLockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/MySQLTableLockTest.java new file mode 100644 index 000000000..70c05a486 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/MySQLTableLockTest.java @@ -0,0 +1,32 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.locking; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; + +import java.sql.Connection; + +/** + * @author Vlad Mihalcea + */ +public class MySQLTableLockTest extends AbstractTableLockTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new MySQLDataSourceProvider(); + } + + @Override + protected String lockEmployeeTableSql() { + return "SELECT 1 FROM employee WHERE department_id = 1 FOR UPDATE"; + } + + @Override + protected void prepareConnection(Connection connection) { + /*try { + connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + } catch (SQLException e) { + fail(e.getMessage()); + }*/ + executeStatement(connection, "SET GLOBAL innodb_lock_wait_timeout = 1"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/MySQLTableLockWithEmployeesInFirstDepartmentOnlyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/MySQLTableLockWithEmployeesInFirstDepartmentOnlyTest.java similarity index 90% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/MySQLTableLockWithEmployeesInFirstDepartmentOnlyTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/MySQLTableLockWithEmployeesInFirstDepartmentOnlyTest.java index 368e2c69f..44817f337 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/MySQLTableLockWithEmployeesInFirstDepartmentOnlyTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/MySQLTableLockWithEmployeesInFirstDepartmentOnlyTest.java @@ -1,13 +1,12 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking; +package com.vladmihalcea.hpjp.jdbc.transaction.locking; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; -import com.vladmihalcea.book.hpjp.jdbc.transaction.locking.AbstractTableLockTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; - import static org.junit.Assert.fail; /** diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/OraclePredicateLockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/OraclePredicateLockTest.java new file mode 100644 index 000000000..42c0a314a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/OraclePredicateLockTest.java @@ -0,0 +1,15 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.locking; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.OracleDataSourceProvider; + +/** + * @author Vlad Mihalcea + */ +public class OraclePredicateLockTest extends AbstractPredicateLockTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new OracleDataSourceProvider(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/OracleTableLockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/OracleTableLockTest.java new file mode 100644 index 000000000..123ea7b5d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/OracleTableLockTest.java @@ -0,0 +1,20 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.locking; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.OracleDataSourceProvider; + +/** + * @author Vlad Mihalcea + */ +public class OracleTableLockTest extends AbstractTableLockTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new OracleDataSourceProvider(); + } + + @Override + protected String lockEmployeeTableSql() { + return "LOCK TABLE employee IN SHARE MODE"; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/PostgreSQLForNoKeyUpdateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/PostgreSQLForNoKeyUpdateTest.java new file mode 100644 index 000000000..e39ae8aad --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/PostgreSQLForNoKeyUpdateTest.java @@ -0,0 +1,189 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.locking; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLForNoKeyUpdateTest extends AbstractTest { + + protected final AtomicLong POST_COMMENT_ID = new AtomicLong(); + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class + }; + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + public void afterInit() { + doInJPA(entityManager -> { + Post post = new Post() + .setId(1L) + .setTitle("Transactions"); + + entityManager.persist(post); + }); + } + + @Test + public void testParentForUpdatePreventsChildInsert(){ + AtomicBoolean prevented = new AtomicBoolean(); + + doInJPA(entityManager -> { + final Post _post = (Post) entityManager.createNativeQuery(""" + SELECT id, title + FROM post p + WHERE id = :id + FOR UPDATE + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + executeSync(() -> { + try { + doInStatelessSession(session -> { + session.doWork(this::setJdbcTimeout); + + session.insert( + new PostComment() + .setId(POST_COMMENT_ID.incrementAndGet()) + .setReview(String.format("Comment nr. %d", POST_COMMENT_ID.get())) + .setPost(_post) + ); + }); + } catch (Exception e) { + prevented.set(ExceptionUtil.isLockTimeout(e)); + } + }); + }); + + assertTrue(prevented.get()); + LOGGER.info("Insert was prevented by the explicit parent lock"); + } + + @Test + public void testParentForNoKeyUpdateAllowsChildInsert() { + AtomicBoolean prevented = new AtomicBoolean(); + + doInJPA(entityManager -> { + final Post _post = (Post) entityManager.createNativeQuery(""" + SELECT id, title + FROM post p + WHERE id = :id + FOR NO KEY UPDATE + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + executeSync(() -> { + try { + doInStatelessSession(session -> { + session.doWork(this::setJdbcTimeout); + + session.insert( + new PostComment() + .setId(POST_COMMENT_ID.incrementAndGet()) + .setReview(String.format("Comment nr. %d", POST_COMMENT_ID.get())) + .setPost(_post) + ); + }); + } catch (Exception e) { + prevented.set(ExceptionUtil.isLockTimeout(e)); + } + }); + }); + + assertFalse(prevented.get()); + LOGGER.info("Insert was not prevented by the explicit parent lock"); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + @Column(length = 100) + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @Column(length = 250) + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/PostgreSQLPredicateLockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/PostgreSQLPredicateLockTest.java new file mode 100644 index 000000000..0e906fc17 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/PostgreSQLPredicateLockTest.java @@ -0,0 +1,299 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.locking; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.PostgreSQLDataSourceProvider; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Session; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLPredicateLockTest extends AbstractPredicateLockTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider(); + } + + @Test + public void testParentPessimisticWriteChildInsert() { + AtomicBoolean prevented = new AtomicBoolean(); + + doInHibernate( session -> { + session.unwrap(Session.class).doWork(this::prepareConnection); + Post _post = session.createQuery( + "select p " + + "from Post p " + + "where p.id = :id", Post.class) + .setParameter("id", 1L) + .setLockOptions(new LockOptions(LockMode.PESSIMISTIC_WRITE)) + .getSingleResult(); + + executeAsync(() -> { + doInHibernate(_session -> { + _session.unwrap(Session.class).doWork(this::prepareConnection); + + Post post = _session.getReference(Post.class, 1L); + + PostComment comment = new PostComment(); + comment.setId(POST_COMMENT_ID.incrementAndGet()); + comment.setReview(String.format("Comment nr. %d", comment.getId())); + comment.setPost(post); + + _session.persist(comment); + + aliceLatch.countDown(); + _session.flush(); + LOGGER.info("Insert {} prevented by explicit parent lock", prevented.get() ? "was" : "was not"); + bobLatch.countDown(); + }); + }); + + awaitOnLatch(aliceLatch); + sleep(WAIT_MILLIS); + LOGGER.info("Alice woke up!"); + prevented.set(true); + } ); + awaitOnLatch(bobLatch); + } + + @Test + public void testParentForNoKeyUpdatePreventsConcurrentUpdate() { + AtomicBoolean prevented = new AtomicBoolean(); + + doInHibernate( session -> { + session.unwrap(Session.class).doWork(this::prepareConnection); + Number _postId = (Number) session.createNativeQuery( + "select id " + + "from Post " + + "where id = :id " + + "for no key update") + .setParameter("id", 1L) + .getSingleResult(); + + executeAsync(() -> { + doInHibernate(_session -> { + _session.unwrap(Session.class).doWork(this::prepareConnection); + + Post post = _session.getReference(Post.class, 1L); + + post.setTitle("High-Performance Hibernate"); + + aliceLatch.countDown(); + _session.flush(); + LOGGER.info("Update on non-conflicting column {} prevented by explicit parent lock", prevented.get() ? "was" : "was not"); + bobLatch.countDown(); + }); + }); + + awaitOnLatch(aliceLatch); + sleep(WAIT_MILLIS); + LOGGER.info("Alice woke up!"); + prevented.set(true); + } ); + awaitOnLatch(bobLatch); + } + + @Test + public void testParentForKeyShareNonConflictingConcurrentUpdate() { + AtomicBoolean prevented = new AtomicBoolean(); + + doInHibernate( session -> { + session.unwrap(Session.class).doWork(this::prepareConnection); + Number _postId = (Number) session.createNativeQuery( + "select id " + + "from Post " + + "where id = :id " + + "for key share") + .setParameter("id", 1L) + .getSingleResult(); + + executeAsync(() -> { + doInHibernate(_session -> { + _session.unwrap(Session.class).doWork(this::prepareConnection); + + Post post = _session.getReference(Post.class, 1L); + + post.setTitle("High-Performance Hibernate"); + + aliceLatch.countDown(); + _session.flush(); + LOGGER.info("Update on non-conflicting column {} prevented by explicit parent lock", prevented.get() ? "was" : "was not"); + bobLatch.countDown(); + }); + }); + + awaitOnLatch(aliceLatch); + sleep(WAIT_MILLIS); + LOGGER.info("Alice woke up!"); + prevented.set(true); + } ); + awaitOnLatch(bobLatch); + } + + @Test + public void testParentForKeyShareConflictingNoFkConcurrentUpdate() { + AtomicBoolean prevented = new AtomicBoolean(); + + doInHibernate( session -> { + session.unwrap(Session.class).doWork(this::prepareConnection); + String _postTitle = (String) session.createNativeQuery( + "select title " + + "from post " + + "where id = :id " + + "for key share") + .setParameter("id", 1L) + .getSingleResult(); + + executeAsync(() -> { + doInHibernate(_session -> { + _session.unwrap(Session.class).doWork(this::prepareConnection); + + Post post = _session.getReference(Post.class, 1L); + + post.setTitle("High-Performance Hibernate"); + + aliceLatch.countDown(); + _session.flush(); + LOGGER.info("Update on conflicting column {} prevented by explicit parent lock", prevented.get() ? "was" : "was not"); + bobLatch.countDown(); + }); + }); + + awaitOnLatch(aliceLatch); + sleep(WAIT_MILLIS); + LOGGER.info("Alice woke up!"); + prevented.set(true); + } ); + awaitOnLatch(bobLatch); + } + + @Test + public void testParentForKeyShareConflictingFkConcurrentUpdate() { + AtomicBoolean prevented = new AtomicBoolean(); + + doInHibernate( session -> { + session.unwrap(Session.class).doWork(this::prepareConnection); + Number _postCommentId = (Number) session.createNativeQuery( + "select post_id " + + "from post_comment " + + "where id = :id " + + "for update") + .setParameter("id", 1L) + .getSingleResult(); + + executeAsync(() -> { + doInHibernate(_session -> { + _session.unwrap(Session.class).doWork(this::prepareConnection); + + PostComment postComment = _session.getReference(PostComment.class, _postCommentId.longValue()); + + postComment.setPost(null); + + aliceLatch.countDown(); + _session.flush(); + LOGGER.info("Update on conflicting FK column {} prevented by explicit parent lock", prevented.get() ? "was" : "was not"); + bobLatch.countDown(); + }); + }); + + awaitOnLatch(aliceLatch); + sleep(WAIT_MILLIS); + LOGGER.info("Alice woke up!"); + prevented.set(true); + } ); + awaitOnLatch(bobLatch); + } + + @Test + public void testParentForUpdateConflictingConcurrentDelete() { + AtomicBoolean prevented = new AtomicBoolean(); + + doInHibernate( session -> { + session.unwrap(Session.class).doWork(this::prepareConnection); + Number _postCommentId = (Number) session.createNativeQuery( + "select post_id " + + "from post_comment " + + "where id = :id " + + "for update") + .setParameter("id", 1L) + .getSingleResult(); + + executeAsync(() -> { + try { + doInHibernate(_session -> { + _session.unwrap(Session.class).doWork(this::prepareConnection); + + int updateCount = _session.createNativeQuery( + "delete from post_comment " + + "where id = :id ") + .setParameter("id", _postCommentId) + .executeUpdate(); + + assertEquals(1, updateCount); + }); + } catch (Exception expected) { + prevented.set(true); + + aliceLatch.countDown(); + LOGGER.info("Update on conflicting FK column {} prevented by explicit parent lock", prevented.get() ? "was" : "was not"); + bobLatch.countDown(); + } + }); + + awaitOnLatch(aliceLatch); + sleep(WAIT_MILLIS); + LOGGER.info("Alice woke up!"); + prevented.set(true); + } ); + awaitOnLatch(bobLatch); + } + + @Test + public void testParentReadLockPreventsInsert() { + AtomicBoolean prevented = new AtomicBoolean(); + + doInHibernate( session -> { + session.unwrap(Session.class).doWork(this::prepareConnection); + Post _post = session.createQuery( + "select p " + + "from Post p " + + "where p.id = :id", Post.class) + .setParameter("id", 1L) + .setLockOptions(new LockOptions(LockMode.PESSIMISTIC_READ)) + .getSingleResult(); + + executeAsync(() -> { + doInHibernate(_session -> { + _session.unwrap(Session.class).doWork(this::prepareConnection); + + Post post = _session.getReference(Post.class, 1L); + + PostComment comment = new PostComment(); + comment.setId(POST_COMMENT_ID.incrementAndGet()); + comment.setReview(String.format("Comment nr. %d", comment.getId())); + comment.setPost(post); + + _session.persist(comment); + + aliceLatch.countDown(); + _session.flush(); + LOGGER.info("Insert {} prevented by explicit parent lock", prevented.get() ? "was" : "was not"); + bobLatch.countDown(); + }); + }); + + awaitOnLatch(aliceLatch); + sleep(WAIT_MILLIS); + LOGGER.info("Alice woke up!"); + prevented.set(true); + } ); + awaitOnLatch(bobLatch); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/PostgreSQLTableLockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/PostgreSQLTableLockTest.java new file mode 100644 index 000000000..81e4a19c4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/PostgreSQLTableLockTest.java @@ -0,0 +1,27 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.locking; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.PostgreSQLDataSourceProvider; + +import java.sql.Connection; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLTableLockTest extends AbstractTableLockTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider(); + } + + @Override + protected String lockEmployeeTableSql() { + return "LOCK TABLE employee IN SHARE ROW EXCLUSIVE MODE"; + } + + @Override + protected void prepareConnection(Connection connection) { + executeStatement(connection, "SET statement_timeout TO 1000"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/SQLServerPredicateLockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/SQLServerPredicateLockTest.java new file mode 100644 index 000000000..c6bc06ac9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/SQLServerPredicateLockTest.java @@ -0,0 +1,15 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.locking; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.SQLServerDataSourceProvider; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerPredicateLockTest extends AbstractPredicateLockTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new SQLServerDataSourceProvider(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/SQLServerTableLockMultipleEntriesTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/SQLServerTableLockMultipleEntriesTest.java similarity index 91% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/SQLServerTableLockMultipleEntriesTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/SQLServerTableLockMultipleEntriesTest.java index eb3ee3958..b4de87175 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/SQLServerTableLockMultipleEntriesTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/SQLServerTableLockMultipleEntriesTest.java @@ -1,12 +1,11 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking; +package com.vladmihalcea.hpjp.jdbc.transaction.locking; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.SQLServerDataSourceProvider; import java.sql.PreparedStatement; import java.sql.SQLException; -import com.vladmihalcea.book.hpjp.jdbc.transaction.locking.AbstractTableLockTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; - import static org.junit.Assert.fail; /** diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/SQLServerTableLockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/SQLServerTableLockTest.java new file mode 100644 index 000000000..aa07c43e8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/SQLServerTableLockTest.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.locking; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.SQLServerDataSourceProvider; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerTableLockTest extends AbstractTableLockTest { + + @Override + protected DataSourceProvider dataSourceProvider() { + return new SQLServerDataSourceProvider(); + } + + @Override + protected String lockEmployeeTableSql() { + return "SELECT 1 FROM employee WITH (HOLDLOCK) WHERE department_id = 1"; + } + + protected String insertEmployeeSql() { + return "INSERT INTO employee WITH(NOWAIT) (department_id, name, salary, id) VALUES (?, ?, ?, ?)"; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/AbstractPostgreSQLAdvisoryLocksTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/AbstractPostgreSQLAdvisoryLocksTest.java similarity index 96% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/AbstractPostgreSQLAdvisoryLocksTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/AbstractPostgreSQLAdvisoryLocksTest.java index 17bcdb800..9bf884816 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/AbstractPostgreSQLAdvisoryLocksTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/AbstractPostgreSQLAdvisoryLocksTest.java @@ -1,4 +1,7 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking.advisory; +package com.vladmihalcea.hpjp.jdbc.transaction.locking.advisory; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; import java.io.IOException; import java.nio.file.Files; @@ -13,10 +16,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; - import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.file.StandardOpenOption.APPEND; import static org.junit.Assert.assertEquals; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/PostgreSQLNoAdvisoryLocksTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/PostgreSQLNoAdvisoryLocksTest.java new file mode 100644 index 000000000..ff0a9a793 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/PostgreSQLNoAdvisoryLocksTest.java @@ -0,0 +1,20 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.locking.advisory; + +import java.sql.Connection; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLNoAdvisoryLocksTest extends AbstractPostgreSQLAdvisoryLocksTest { + + @Override + protected int acquireLock(Connection connection, int logIndex, int workerId) { + LOGGER.info( "Worker {} writes to log {}", workerId, logIndex ); + return logIndex; + } + + @Override + protected void releaseLock(Connection connection, int logIndex, int workerId) { + + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/PostgreSQLReadWriteAdvisoryLocksTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/PostgreSQLReadWriteAdvisoryLocksTest.java similarity index 97% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/PostgreSQLReadWriteAdvisoryLocksTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/PostgreSQLReadWriteAdvisoryLocksTest.java index 71752a998..b19478343 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/PostgreSQLReadWriteAdvisoryLocksTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/PostgreSQLReadWriteAdvisoryLocksTest.java @@ -1,4 +1,7 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking.advisory; +package com.vladmihalcea.hpjp.jdbc.transaction.locking.advisory; + +import com.vladmihalcea.hpjp.util.AbstractPostgreSQLIntegrationTest; +import org.junit.Test; import java.io.IOException; import java.nio.file.Files; @@ -14,10 +17,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import org.junit.Test; - -import com.vladmihalcea.book.hpjp.util.AbstractPostgreSQLIntegrationTest; - import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.file.StandardOpenOption.APPEND; import static org.junit.Assert.assertTrue; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/PostgreSQLSessionAdvisoryLocksTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/PostgreSQLSessionAdvisoryLocksTest.java similarity index 86% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/PostgreSQLSessionAdvisoryLocksTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/PostgreSQLSessionAdvisoryLocksTest.java index 96efa131c..20ac081c3 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/PostgreSQLSessionAdvisoryLocksTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/PostgreSQLSessionAdvisoryLocksTest.java @@ -1,11 +1,9 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking.advisory; +package com.vladmihalcea.hpjp.jdbc.transaction.locking.advisory; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; -import com.vladmihalcea.book.hpjp.jdbc.transaction.locking.advisory.AbstractPostgreSQLAdvisoryLocksTest; - /** * @author Vlad Mihalcea */ diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/PostgreSQLSessionTryAdvisoryLocksTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/PostgreSQLSessionTryAdvisoryLocksTest.java similarity index 89% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/PostgreSQLSessionTryAdvisoryLocksTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/PostgreSQLSessionTryAdvisoryLocksTest.java index bf7c245ad..9f2438b1d 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/locking/advisory/PostgreSQLSessionTryAdvisoryLocksTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/locking/advisory/PostgreSQLSessionTryAdvisoryLocksTest.java @@ -1,12 +1,10 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.locking.advisory; +package com.vladmihalcea.hpjp.jdbc.transaction.locking.advisory; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import com.vladmihalcea.book.hpjp.jdbc.transaction.locking.advisory.AbstractPostgreSQLAdvisoryLocksTest; - /** * @author Vlad Mihalcea */ diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/AbstractPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/AbstractPhenomenaTest.java new file mode 100644 index 000000000..6a3bcc46f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/AbstractPhenomenaTest.java @@ -0,0 +1,515 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.phenomena; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.entity.BlogEntityProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * PhenomenaTest - Test to validate what phenomena does a certain isolation level prevents + * + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public abstract class AbstractPhenomenaTest extends AbstractTest { + + public static final String INSERT_POST = "insert into post (title, version, id) values (?, ?, ?)"; + + public static final String INSERT_POST_COMMENT = "insert into post_comment (post_id, review, version, id) values (?, ?, ?, ?)"; + + public static final String INSERT_POST_DETAILS = "insert into post_details (id, created_by, version) values (?, ?, ?)"; + + protected final String isolationLevelName; + + protected final int isolationLevel; + + private final CountDownLatch bobLatch = new CountDownLatch(1); + + private BlogEntityProvider entityProvider = new BlogEntityProvider(); + + protected AbstractPhenomenaTest(String isolationLevelName, int isolationLevel) { + this.isolationLevelName = isolationLevelName; + this.isolationLevel = isolationLevel; + } + + public String getIsolationLevelName() { + return isolationLevelName; + } + + public int getIsolationLevel() { + return isolationLevel; + } + + @Override + protected Class[] entities() { + return entityProvider.entities(); + } + + @Parameterized.Parameters + public static Collection isolationLevels() { + List levels = new ArrayList<>(); + levels.add(new Object[]{"Read Uncommitted", Connection.TRANSACTION_READ_UNCOMMITTED}); + levels.add(new Object[]{"Read Committed", Connection.TRANSACTION_READ_COMMITTED}); + levels.add(new Object[]{"Repeatable Read", Connection.TRANSACTION_REPEATABLE_READ}); + levels.add(new Object[]{"Serializable", Connection.TRANSACTION_SERIALIZABLE}); + return levels; + } + + @Override + public void afterInit() { + doInJDBC(connection -> { + try ( + PreparedStatement postStatement = connection.prepareStatement(INSERT_POST); + PreparedStatement postCommentStatement = connection.prepareStatement(INSERT_POST_COMMENT); + PreparedStatement postDetailsStatement = connection.prepareStatement(INSERT_POST_DETAILS); + ) { + int index = 0; + postStatement.setString(++index, "Transactions"); + postStatement.setInt(++index, 0); + postStatement.setLong(++index, 1); + postStatement.executeUpdate(); + + index = 0; + postDetailsStatement.setInt(++index, 1); + postDetailsStatement.setString(++index, "None"); + postDetailsStatement.setInt(++index, 0); + postDetailsStatement.executeUpdate(); + + for (int i = 0; i < 3; i++) { + index = 0; + postCommentStatement.setLong(++index, 1); + postCommentStatement.setString(++index, String.format("Post comment %1$d", i)); + postCommentStatement.setInt(++index, 0); + postCommentStatement.setLong(++index, i); + postCommentStatement.executeUpdate(); + } + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + } + + @Test + public void testDirtyWrite() { + String firstTitle = "Alice"; + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + update(aliceConnection, updatePostTitleParamSql(), new Object[]{firstTitle}); + try { + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + update(bobConnection, updatePostTitleParamSql(), new Object[]{"Bob"}); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else { + throw new IllegalStateException(e); + } + } + }); + }); + } catch (Exception e) { + if (!ExceptionUtil.isConnectionClose(e)) { + fail(e.getMessage()); + } + } + }); + } catch (Exception e) { + if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } + } + + doInJDBC(aliceConnection -> { + String title = selectStringColumn(aliceConnection, selectPostTitleSql()); + LOGGER.info("Isolation level {} {} Dirty Write", isolationLevelName, !title.equals(firstTitle) ? "allows" : "prevents"); + if (preventedByLocking.get()) { + LOGGER.info("Isolation level {} prevents Dirty Write by locking", isolationLevelName); + } else if (Boolean.TRUE.equals(preventedByMVCC.get())) { + LOGGER.info("Isolation level {} prevents Dirty Write by MVCC", isolationLevelName); + } + }); + } + + @Test + public void testDirtyRead() { + final AtomicBoolean dirtyRead = new AtomicBoolean(); + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + try (Statement aliceStatement = aliceConnection.createStatement()) { + aliceStatement.executeUpdate(updatePostTitleSql()); + executeSync(() -> { + try { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + String title = selectStringColumn(bobConnection, selectPostTitleSql()); + if ("Transactions".equals(title)) { + LOGGER.info("No Dirty Read, uncommitted data is not viewable"); + } else if ("ACID".equals(title)) { + dirtyRead.set(true); + } else { + fail("Unknown title: " + title); + } + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else { + throw new IllegalStateException(e); + } + } + }); + } catch (Exception e) { + if (!ExceptionUtil.isConnectionClose(e)) { + fail(e.getMessage()); + } + } + }); + } + }); + } catch (Exception e) { + if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } + } + + LOGGER.info("Isolation level {} {} Dirty Read", isolationLevelName, dirtyRead.get() ? "allows" : "prevents"); + if (preventedByLocking.get()) { + LOGGER.info("Isolation level {} prevents Dirty Read by locking", isolationLevelName); + } else if (Boolean.TRUE.equals(preventedByMVCC.get())) { + LOGGER.info("Isolation level {} prevents Dirty Read Read by MVCC", isolationLevelName); + } + } + + @Test + public void testNonRepeatableRead() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + String firstTitle = selectStringColumn(aliceConnection, selectPostTitleSql()); + try { + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + assertEquals(1, update(bobConnection, updatePostTitleSql())); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else { + throw new IllegalStateException(e); + } + } + }); + }); + } catch (Exception e) { + if (!ExceptionUtil.isConnectionClose(e)) { + fail(e.getMessage()); + } + } + String secondTitle = selectStringColumn(aliceConnection, selectPostTitleSql()); + + LOGGER.info("Isolation level {} {} Non-Repeatable Read", isolationLevelName, !firstTitle.equals(secondTitle) ? "allows" : "prevents"); + if (preventedByLocking.get()) { + LOGGER.info("Isolation level {} prevents Non-Repeatable Read by locking", isolationLevelName); + } + }); + } catch (Exception e) { + if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } + } + + if (Boolean.TRUE.equals(preventedByMVCC.get())) { + LOGGER.info("Isolation level {} prevents Non-Repeatable Read by MVCC", isolationLevelName); + } + } + + @Test + public void testPhantomRead() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + int commentsCount = count(aliceConnection, countCommentsSql()); + assertEquals(3, commentsCount); + update(aliceConnection, updateCommentsSql()); + try { + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + assertEquals(1, update(bobConnection, insertCommentSql())); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else { + throw new IllegalStateException(e); + } + } + }); + }); + } catch (Exception e) { + if (!ExceptionUtil.isConnectionClose(e)) { + fail(e.getMessage()); + } + } + int secondCommentsCount = count(aliceConnection, countCommentsSql()); + + LOGGER.info("Isolation level {} {} Phantom Reads", isolationLevelName, secondCommentsCount != commentsCount ? "allows" : "prevents"); + if (preventedByLocking.get()) { + LOGGER.info("Isolation level {} prevents Phantom Read by locking", isolationLevelName); + } + }); + } catch (Exception e) { + if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } + } + + if (Boolean.TRUE.equals(preventedByMVCC.get())) { + LOGGER.info("Isolation level {} prevents Phantom Read by MVCC", isolationLevelName); + } + } + + @Test + public void testLostUpdate() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + String title = selectStringColumn(aliceConnection, selectPostTitleSql()); + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + update(bobConnection, updatePostTitleParamSql(), new Object[]{"Bob"}); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else { + throw new IllegalStateException(e); + } + } + }); + }); + update(aliceConnection, updatePostTitleParamSql(), new Object[]{"Alice"}); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else if (!ExceptionUtil.isConnectionClose(e)) { + fail(e.getMessage()); + } + } + doInJDBC(aliceConnection -> { + String title = selectStringColumn(aliceConnection, selectPostTitleSql()); + LOGGER.info("Isolation level {} {} Lost Update", isolationLevelName, "Alice".equals(title) ? "allows" : "prevents"); + + if (Boolean.TRUE.equals(preventedByLocking.get())) { + LOGGER.info("Isolation level {} prevents Lost Update by locking", isolationLevelName); + } else if (Boolean.TRUE.equals(preventedByMVCC.get())) { + LOGGER.info("Isolation level {} prevents Lost Update by MVCC", isolationLevelName); + } + }); + } + + @Test + public void testReadSkew() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + String title = selectStringColumn(aliceConnection, selectPostTitleSql()); + + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + update(bobConnection, updatePostTitleParamSql(), new Object[]{"Bob"}); + update(bobConnection, updatePostDetailsAuthorParamSql(), new Object[]{"Bob"}); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else { + throw new IllegalStateException(e); + } + } + }); + }); + + String createdBy = selectStringColumn(aliceConnection, selectPostDetailsAuthorSql()); + LOGGER.info("Isolation level {} {} Read Skew", isolationLevelName, "Bob".equals(createdBy) ? "allows" : "prevents"); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else if (!ExceptionUtil.isConnectionClose(e)) { + throw new IllegalStateException(e); + } + } + doInJDBC(aliceConnection -> { + if (Boolean.TRUE.equals(preventedByLocking.get())) { + LOGGER.info("Isolation level {} prevents Read Skew by locking", isolationLevelName); + } else if (Boolean.TRUE.equals(preventedByMVCC.get())) { + LOGGER.info("Isolation level {} prevents Read Skew by MVCC", isolationLevelName); + } + }); + } + + @Test + public void testWriteSkew() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + String title = selectStringColumn(aliceConnection, selectPostTitleSql()); + String createdBy = selectStringColumn(aliceConnection, selectPostDetailsAuthorSql()); + + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + String bobTitle = selectStringColumn(bobConnection, selectPostTitleSql()); + String bonCreatedBy = selectStringColumn(bobConnection, selectPostDetailsAuthorSql()); + update(bobConnection, updatePostTitleParamSql(), new Object[]{"Bob"}); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else { + throw new IllegalStateException(e); + } + } + }); + }); + update(aliceConnection, updatePostDetailsAuthorParamSql(), new Object[]{"Alice"}); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else if (!ExceptionUtil.isConnectionClose(e)) { + fail(e.getMessage()); + } + } + if (Boolean.TRUE.equals(preventedByLocking.get())) { + LOGGER.info("Isolation level {} prevents Write Skew by locking", isolationLevelName); + } else if (Boolean.TRUE.equals(preventedByMVCC.get())) { + LOGGER.info("Isolation level {} prevents Write Skew by MVCC", isolationLevelName); + } else { + LOGGER.info("Isolation level {} allows Write Skew", isolationLevelName); + } + } + + protected void prepareConnection(Connection connection) throws SQLException { + connection.setTransactionIsolation(isolationLevel); + setJdbcTimeout(connection); + } + + protected String selectPostTitleSql() { + return "SELECT title FROM post WHERE id = 1"; + } + + protected String selectPostDetailsAuthorSql() { + return "SELECT created_by FROM post_details WHERE id = 1"; + } + + protected String updatePostTitleSql() { + return "UPDATE post SET title = 'ACID' WHERE id = 1"; + } + + protected String updatePostTitleParamSql() { + return "UPDATE post SET title = ? WHERE id = 1"; + } + + protected String updatePostDetailsAuthorParamSql() { + return "UPDATE post_details SET created_by = ? WHERE id = 1"; + } + + protected String countCommentsSql() { + return "SELECT COUNT(*) FROM post_comment where post_id = 1"; + } + + protected String updateCommentsSql() { + return "UPDATE post_comment SET version = 100 WHERE post_id = 1"; + } + + int nextId = 100; + + protected String insertCommentSql() { + return String.format("INSERT INTO post_comment (post_id, review, version, id) VALUES (1, 'Phantom', 0, %d)", nextId++); + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/MySQLPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/MySQLPhenomenaTest.java new file mode 100644 index 000000000..ef4fa8abb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/MySQLPhenomenaTest.java @@ -0,0 +1,21 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.phenomena; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.MySQLDataSourceProvider; + +/** + * MySQLPhenomenaTest - Test to validate MySQL phenomena + * + * @author Vlad Mihalcea + */ +public class MySQLPhenomenaTest extends AbstractPhenomenaTest { + + public MySQLPhenomenaTest(String isolationLevelName, int isolationLevel) { + super(isolationLevelName, isolationLevel); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new MySQLDataSourceProvider(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/OraclePhenomenaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/OraclePhenomenaTest.java similarity index 81% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/OraclePhenomenaTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/OraclePhenomenaTest.java index 6ee0ca41c..9feb0b7cd 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/OraclePhenomenaTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/OraclePhenomenaTest.java @@ -1,15 +1,14 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.phenomena; +package com.vladmihalcea.hpjp.jdbc.transaction.phenomena; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.OracleDataSourceProvider; +import org.junit.runners.Parameterized; import java.sql.Connection; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import org.junit.runners.Parameterized; - -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; - /** * OraclePhenomenaTest - Test to validate Oracle phenomena * diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/PostgreSQLPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/PostgreSQLPhenomenaTest.java new file mode 100644 index 000000000..2a4e72907 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/PostgreSQLPhenomenaTest.java @@ -0,0 +1,21 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.phenomena; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.PostgreSQLDataSourceProvider; + +/** + * PostgreSQLPhenomenaTest - Test to validate PostgreSQL phenomena + * + * @author Vlad Mihalcea + */ +public class PostgreSQLPhenomenaTest extends AbstractPhenomenaTest { + + public PostgreSQLPhenomenaTest(String isolationLevelName, int isolationLevel) { + super(isolationLevelName, isolationLevel); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/SQLServerPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/SQLServerPhenomenaTest.java similarity index 93% rename from core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/SQLServerPhenomenaTest.java rename to core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/SQLServerPhenomenaTest.java index b9633b43b..c8dc6147d 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/jdbc/transaction/phenomena/SQLServerPhenomenaTest.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/SQLServerPhenomenaTest.java @@ -1,9 +1,8 @@ -package com.vladmihalcea.book.hpjp.jdbc.transaction.phenomena; +package com.vladmihalcea.hpjp.jdbc.transaction.phenomena; import com.microsoft.sqlserver.jdbc.SQLServerConnection; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; - +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.SQLServerDataSourceProvider; import org.junit.runners.Parameterized; import java.sql.Connection; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/YugabyteDBPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/YugabyteDBPhenomenaTest.java new file mode 100644 index 000000000..5c8b72dc7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/YugabyteDBPhenomenaTest.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.phenomena; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.YugabyteDBDataSourceProvider; +import org.junit.runners.Parameterized; + +import java.util.Collection; +import java.util.Collections; + +/** + * YugabyteDBPhenomenaTest - Test to validate YugabyteDB phenomena + * + * @author Vlad Mihalcea + */ +public class YugabyteDBPhenomenaTest extends AbstractPhenomenaTest { + + public YugabyteDBPhenomenaTest(String isolationLevelName, int isolationLevel) { + super(isolationLevelName, isolationLevel); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new YugabyteDBDataSourceProvider(); + } + + @Parameterized.Parameters + public static Collection isolationLevels() { + if(!ENABLE_LONG_RUNNING_TESTS) { + return Collections.emptyList(); + } + return AbstractPhenomenaTest.isolationLevels(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/AbstractDepartmentEmployeePhenomenaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/AbstractDepartmentEmployeePhenomenaTest.java new file mode 100644 index 000000000..6270360eb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/AbstractDepartmentEmployeePhenomenaTest.java @@ -0,0 +1,203 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.phenomena.writeskew; + +import com.vladmihalcea.hpjp.util.AbstractTest; +import jakarta.persistence.*; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public abstract class AbstractDepartmentEmployeePhenomenaTest extends AbstractTest { + + public static final String INSERT_DEPARTMENT = "insert into department (name, budget, id) values (?, ?, ?)"; + + public static final String INSERT_EMPLOYEE = "insert into employee (department_id, name, salary, id) values (?, ?, ?, ?)"; + + + protected final String isolationLevelName; + + protected final int isolationLevel; + + protected AbstractDepartmentEmployeePhenomenaTest(String isolationLevelName, int isolationLevel) { + this.isolationLevelName = isolationLevelName; + this.isolationLevel = isolationLevel; + } + + @Override + protected Class[] entities() { + List> classes = new ArrayList<>(Arrays.asList(super.entities())); + classes.add(Department.class); + classes.add(Employee.class); + return classes.toArray(new Class[]{}); + } + + @Parameterized.Parameters + public static Collection isolationLevels() { + List levels = new ArrayList<>(); + levels.add(new Object[]{"Read Committed", Connection.TRANSACTION_READ_COMMITTED}); + levels.add(new Object[]{"Repeatable Read", Connection.TRANSACTION_REPEATABLE_READ}); + levels.add(new Object[]{"Serializable", Connection.TRANSACTION_SERIALIZABLE}); + return levels; + } + + protected String sumEmployeeSalarySql() { + return "SELECT SUM(salary) FROM employee where department_id = 1"; + } + + protected String allEmployeeSalarySql() { + return "SELECT salary FROM employee where department_id = 1"; + } + + protected String insertEmployeeSql() { + return INSERT_EMPLOYEE; + } + + protected String updateEmployeeSalarySql() { + return "UPDATE employee SET salary = salary * 1.1 WHERE department_id = 1"; + } + + @Override + public void afterInit() { + doInJDBC(connection -> { + try ( + PreparedStatement departmentStatement = connection.prepareStatement(INSERT_DEPARTMENT); + PreparedStatement employeeStatement = connection.prepareStatement(INSERT_EMPLOYEE); + ) { + int index = 0; + departmentStatement.setString(++index, "IT"); + departmentStatement.setLong(++index, 100_000); + departmentStatement.setLong(++index, 1); + departmentStatement.executeUpdate(); + + index = 0; + + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Alice"); + employeeStatement.setLong(++index, 40_000); + employeeStatement.setLong(++index, 1); + employeeStatement.executeUpdate(); + + index = 0; + + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Bob"); + employeeStatement.setLong(++index, 30_000); + employeeStatement.setLong(++index, 2); + employeeStatement.executeUpdate(); + + index = 0; + + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Carol"); + employeeStatement.setLong(++index, 20_000); + employeeStatement.setLong(++index, 3); + employeeStatement.executeUpdate(); + + } catch (SQLException e) { + fail(e.getMessage()); + } + }); + } + + protected void prepareConnection(Connection connection) throws SQLException { + connection.setTransactionIsolation(isolationLevel); + setJdbcTimeout(connection); + } + + @Entity(name = "Department") + @Table(name = "department") + public static class Department { + + @Id + private Long id; + + private String name; + + private long budget; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getBudget() { + return budget; + } + + public void setBudget(long budget) { + this.budget = budget; + } + } + + @Entity(name = "Employee") + @Table(name = "employee", indexes = @Index(name = "IDX_Employee", columnList = "department_id")) + public static class Employee { + + @Id + private Long id; + + @Column(name = "name") + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + private Department department; + + private long salary; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Department getDepartment() { + return department; + } + + public void setDepartment(Department department) { + this.department = department; + } + + public long getSalary() { + return salary; + } + + public void setSalary(long salary) { + this.salary = salary; + } + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/AbstractRangeBasedWriteSkewPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/AbstractRangeBasedWriteSkewPhenomenaTest.java new file mode 100644 index 000000000..032d8fa04 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/AbstractRangeBasedWriteSkewPhenomenaTest.java @@ -0,0 +1,337 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.phenomena.writeskew; + +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.sql.PreparedStatement; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public abstract class AbstractRangeBasedWriteSkewPhenomenaTest extends AbstractDepartmentEmployeePhenomenaTest { + + protected AbstractRangeBasedWriteSkewPhenomenaTest(String isolationLevelName, int isolationLevel) { + super(isolationLevelName, isolationLevel); + } + + @Test + public void testWriteSkewAggregateWriteSkewAggregate() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + assertEquals(90_000, salaryCount); + + try { + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + long _salaryCount = selectColumn(bobConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + assertEquals(90_000, _salaryCount); + + try ( + PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); + ) { + int employeeId = 4; + int index = 0; + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Carol"); + employeeStatement.setLong(++index, 9_000); + employeeStatement.setLong(++index, employeeId); + employeeStatement.executeUpdate(); + } + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { + preventedByMVCC.set( true ); + } else { + throw new IllegalStateException( e ); + } + } + }); + }); + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { + preventedByMVCC.set( true ); + } else { + throw new IllegalStateException( e ); + } + } + update(aliceConnection, "UPDATE employee SET salary = salary * 1.1 WHERE department_id = 1"); + }); + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { + preventedByMVCC.set( true ); + } else { + throw new IllegalStateException( e ); + } + } + doInJDBC(aliceConnection -> { + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + if(99_000 != salaryCount) { + LOGGER.info("Isolation level {} allows Write Skew since the salary count is {} instead of 99000", isolationLevelName, salaryCount); + } + else { + LOGGER.info("Isolation level {} prevents Write Skew due to {}", isolationLevelName, preventedByLocking.get() ? "locking" : preventedByMVCC.get() ? "MVCC" : "unknown"); + } + }); + } + + @Test + public void testWriteSkewAggregateWithInsert() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + assertEquals(90_000, salaryCount); + + try { + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + long _salaryCount = selectColumn(bobConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + assertEquals(90_000, _salaryCount); + + try ( + PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); + ) { + int employeeId = 4; + int index = 0; + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Carol"); + employeeStatement.setLong(++index, 9_000); + employeeStatement.setLong(++index, employeeId); + employeeStatement.executeUpdate(); + } + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { + preventedByMVCC.set( true ); + } else { + throw new IllegalStateException( e ); + } + } + }); + }); + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { + preventedByMVCC.set( true ); + } else { + throw new IllegalStateException( e ); + } + } + try ( + PreparedStatement employeeStatement = aliceConnection.prepareStatement(insertEmployeeSql()); + ) { + int employeeId = 5; + int index = 0; + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Dave"); + employeeStatement.setLong(++index, 9_000); + employeeStatement.setLong(++index, employeeId); + employeeStatement.executeUpdate(); + } + }); + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { + preventedByMVCC.set( true ); + } else { + throw new IllegalStateException( e ); + } + } + doInJDBC(aliceConnection -> { + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + if(99_000 != salaryCount) { + LOGGER.info("Isolation level {} allows Write Skew since the salary count is {} instead of 99000", isolationLevelName, salaryCount); + } + else { + LOGGER.info("Isolation level {} prevents Write Skew due to {}", isolationLevelName, preventedByLocking.get() ? "locking" : preventedByMVCC.get() ? "MVCC" : "unknown"); + } + }); + } + + @Test + public void testWriteSkewSelectColumn() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + + List salaries = selectColumnList(aliceConnection, allEmployeeSalarySql(), Number.class); + assertEquals(90_000, salaries.stream().mapToInt(Number::intValue).sum()); + + try { + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + List _salaries = selectColumnList(bobConnection, allEmployeeSalarySql(), Number.class); + assertEquals(90_000, _salaries.stream().mapToInt(Number::intValue).sum()); + + try ( + PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); + ) { + int employeeId = 4; + int index = 0; + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Carol"); + employeeStatement.setLong(++index, 9_000); + employeeStatement.setLong(++index, employeeId); + employeeStatement.executeUpdate(); + } + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { + preventedByMVCC.set( true ); + } else { + throw new IllegalStateException( e ); + } + } + }); + }); + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { + preventedByMVCC.set( true ); + } else { + throw new IllegalStateException( e ); + } + } + update(aliceConnection, updateEmployeeSalarySql()); + }); + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { + preventedByMVCC.set( true ); + } else { + throw new IllegalStateException( e ); + } + } + doInJDBC(aliceConnection -> { + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + if(99_000 != salaryCount) { + LOGGER.info("Isolation level {} allows Write Skew since the salary count is {} instead of 99000", isolationLevelName, salaryCount); + } + else { + LOGGER.info("Isolation level {} prevents Write Skew due to {}", isolationLevelName, preventedByLocking.get() ? "locking" : preventedByMVCC.get() ? "MVCC" : "unknown"); + } + }); + } + + @Test + public void testWriteSkewSelectColumnInOneTx() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + + List salaries = selectColumnList(aliceConnection, allEmployeeSalarySql(), Number.class); + assertEquals(90_000, salaries.stream().mapToInt(Number::intValue).sum()); + + try { + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + try ( + PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); + ) { + int employeeId = 4; + int index = 0; + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Carol"); + employeeStatement.setLong(++index, 9_000); + employeeStatement.setLong(++index, employeeId); + employeeStatement.executeUpdate(); + } + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { + preventedByMVCC.set( true ); + } else { + throw new IllegalStateException( e ); + } + } + }); + }); + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { + preventedByMVCC.set( true ); + } else { + throw new IllegalStateException( e ); + } + } + update(aliceConnection, updateEmployeeSalarySql()); + }); + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } else if( ExceptionUtil.isMVCCAnomalyDetection( e )) { + preventedByMVCC.set( true ); + } else { + throw new IllegalStateException( e ); + } + } + doInJDBC(aliceConnection -> { + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + if(99_000 != salaryCount) { + LOGGER.info("Isolation level {} allows Write Skew since the salary count is {} instead of 99000", isolationLevelName, salaryCount); + } + else { + LOGGER.info("Isolation level {} prevents Write Skew due to {}", isolationLevelName, preventedByLocking.get() ? "locking" : preventedByMVCC.get() ? "MVCC" : "unknown"); + } + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/OracleRangeBasedWriteSkewPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/OracleRangeBasedWriteSkewPhenomenaTest.java new file mode 100644 index 000000000..4d4e2d2ea --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/OracleRangeBasedWriteSkewPhenomenaTest.java @@ -0,0 +1,127 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.phenomena.writeskew; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.OracleDataSourceProvider; +import org.junit.Test; +import org.junit.runners.Parameterized; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class OracleRangeBasedWriteSkewPhenomenaTest extends AbstractRangeBasedWriteSkewPhenomenaTest { + + public OracleRangeBasedWriteSkewPhenomenaTest(String isolationLevelName, int isolationLevel) { + super(isolationLevelName, isolationLevel); + } + + @Parameterized.Parameters + public static Collection isolationLevels() { + List levels = new ArrayList<>(); + levels.add(new Object[]{"Read Committed", Connection.TRANSACTION_READ_COMMITTED}); + levels.add(new Object[]{"Serializable", Connection.TRANSACTION_SERIALIZABLE}); + return levels; + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return new OracleDataSourceProvider(); + } + + @Override + public void init() { + super.init(); + doInJDBC(aliceConnection -> { + executeStatement(aliceConnection, "alter table employee initrans 100"); + }); + } + + @Test + public void testWriteSkewAggregateNTimes() { + if (isolationLevel != Connection.TRANSACTION_SERIALIZABLE) { + return; + } + + int sleepMillis = 100; + + AtomicInteger ok = new AtomicInteger(); + AtomicInteger fail = new AtomicInteger(); + for (int i = 0; i < 10; i++) { + AtomicReference preventedByLocking = new AtomicReference<>(); + + doInJDBC(aliceConnection -> { + executeStatement(aliceConnection, "delete from employee where id = 4"); + executeStatement(aliceConnection, "update employee set salary = 30000"); + }); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + assertEquals(90_000, salaryCount); + + try { + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + long _salaryCount = selectColumn(bobConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + assertEquals(90_000, _salaryCount); + + try ( + PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); + ) { + int employeeId = 4; + int index = 0; + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Carol"); + employeeStatement.setLong(++index, 9_000); + employeeStatement.setLong(++index, employeeId); + employeeStatement.executeUpdate(); + } + } catch (Exception e) { + LOGGER.info("Exception thrown", e); + preventedByLocking.set(true); + } + }); + }); + } catch (Exception e) { + LOGGER.info("Exception thrown", e); + preventedByLocking.set(true); + } + sleep(sleepMillis); + update(aliceConnection, "UPDATE employee SET salary = salary * 1.1 WHERE department_id = 1 and id < 4"); + }); + } catch (Exception e) { + LOGGER.info("Exception thrown", e); + preventedByLocking.set(true); + } + doInJDBC(aliceConnection -> { + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + if(99_000 != salaryCount) { + LOGGER.info("Isolation level {} allows Write Skew since the salary count is {} instead of 99000", isolationLevelName, salaryCount); + fail.incrementAndGet(); + } + else { + LOGGER.info("Isolation level {} prevents Write Skew {}", isolationLevelName, preventedByLocking.get() ? "due to locking" : ""); + ok.incrementAndGet(); + } + }); + LOGGER.info("Success: {}, fail: {}", ok.get(), fail.get()); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/PostgreSQLRangeBasedWriteSkewPhenomenaConstraintTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/PostgreSQLRangeBasedWriteSkewPhenomenaConstraintTest.java new file mode 100644 index 000000000..1fde1c7ee --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/PostgreSQLRangeBasedWriteSkewPhenomenaConstraintTest.java @@ -0,0 +1,241 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.phenomena.writeskew; + +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLRangeBasedWriteSkewPhenomenaConstraintTest extends AbstractDepartmentEmployeePhenomenaTest { + + public PostgreSQLRangeBasedWriteSkewPhenomenaConstraintTest(String isolationLevelName, int isolationLevel) { + super(isolationLevelName, isolationLevel); + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + public void afterInit() { + super.afterInit(); + + doInJDBC(connection -> { + executeStatement("DROP TRIGGER IF EXISTS check_department_budget_trigger ON employee;"); + executeStatement("DROP FUNCTION check_department_budget();"); + + executeStatement("CREATE OR REPLACE FUNCTION check_department_budget() RETURNS trigger AS $$ " + + "DECLARE " + + " allowed_budget BIGINT; " + + " new_budget BIGINT; " + + "BEGIN " + + " SELECT INTO allowed_budget budget FROM department where id = NEW.department_id; " + + " SELECT INTO new_budget SUM(salary) FROM employee where department_id = NEW.department_id; " + + " IF new_budget > allowed_budget THEN " + + " RAISE EXCEPTION 'Overbudget department [id:%] by [%]', " + + " NEW.department_id, " + + " (new_budget - allowed_budget);" + + " END IF; " + + " RETURN NEW;" + + "END; " + + "$$ LANGUAGE plpgsql;" + ); + + executeStatement("CREATE TRIGGER check_department_budget_trigger " + + "AFTER INSERT OR UPDATE ON employee " + + "FOR EACH ROW EXECUTE PROCEDURE check_department_budget();" + ); + }); + } + + @Override + public void destroy() { + executeStatement("DROP TRIGGER IF EXISTS check_department_budget_trigger ON employee;"); + executeStatement("DROP FUNCTION check_department_budget();"); + super.destroy(); + } + + @Test + public void testWriteSkewCheckConstraintInsertUpdate() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + final AtomicBoolean preventedByConstraint = new AtomicBoolean(); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + + try { + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + try ( + PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); + ) { + int employeeId = 4; + int index = 0; + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Dave"); + employeeStatement.setLong(++index, 9_000); + employeeStatement.setLong(++index, employeeId); + employeeStatement.executeUpdate(); + } + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else { + throw new IllegalStateException(e); + } + } + }); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else { + throw new IllegalStateException(e); + } + } + update(aliceConnection, updateEmployeeSalarySql()); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else if (isCheckConstraintException(e)) { + preventedByConstraint.set(true); + } else { + throw new IllegalStateException(e); + } + } + doInJDBC(aliceConnection -> { + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + if (99_000 != salaryCount) { + LOGGER.info("Isolation level {} allows overbudgeting since the salary count is {} instead of 99000", isolationLevelName, salaryCount); + } else { + LOGGER.info( + "Isolation level {} prevents overbudgeting due to {}", + isolationLevelName, + preventedByLocking.get() ? + "locking" : + preventedByMVCC.get() ? + "MVCC" : + preventedByConstraint.get() ? + "check constraint" : + "unknown" + ); + } + }); + } + + @Test + public void testWriteSkewCheckConstraintSelectInsertUpdate() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + final AtomicBoolean preventedByConstraint = new AtomicBoolean(); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + assertEquals(90_000, salaryCount); + + try { + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + try ( + PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); + ) { + int employeeId = 4; + int index = 0; + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Dave"); + employeeStatement.setLong(++index, 9_000); + employeeStatement.setLong(++index, employeeId); + employeeStatement.executeUpdate(); + } + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else { + throw new IllegalStateException(e); + } + } + }); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else { + throw new IllegalStateException(e); + } + } + update(aliceConnection, updateEmployeeSalarySql()); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else if (isCheckConstraintException(e)) { + preventedByConstraint.set(true); + } else { + throw new IllegalStateException(e); + } + } + doInJDBC(aliceConnection -> { + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + if (99_000 != salaryCount) { + LOGGER.info("Isolation level {} allows overbudgeting since the salary count is {} instead of 99000", isolationLevelName, salaryCount); + } else { + LOGGER.info( + "Isolation level {} prevents overbudgeting due to {}", + isolationLevelName, + preventedByLocking.get() ? + "locking" : + preventedByMVCC.get() ? + "MVCC" : + preventedByConstraint.get() ? + "check constraint" : + "unknown" + ); + } + }); + } + + private boolean isCheckConstraintException(Exception e) { + boolean constraintViolation = ExceptionUtil.rootCause(e).getMessage().contains("Overbudget"); + if(constraintViolation) { + LOGGER.error("Constraint violation", e); + } + return constraintViolation; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/PostgreSQLRangeBasedWriteSkewPhenomenaTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/PostgreSQLRangeBasedWriteSkewPhenomenaTest.java new file mode 100644 index 000000000..9e98bd4fd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/PostgreSQLRangeBasedWriteSkewPhenomenaTest.java @@ -0,0 +1,198 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.phenomena.writeskew; + +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; + +import java.sql.PreparedStatement; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLRangeBasedWriteSkewPhenomenaTest extends AbstractRangeBasedWriteSkewPhenomenaTest { + + public PostgreSQLRangeBasedWriteSkewPhenomenaTest(String isolationLevelName, int isolationLevel) { + super(isolationLevelName, isolationLevel); + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Test + public void testWriteSkewCheckConstraintInsertUpdate() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + final AtomicBoolean preventedByConstraint = new AtomicBoolean(); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + + try { + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + try ( + PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); + ) { + int employeeId = 4; + int index = 0; + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Carol"); + employeeStatement.setLong(++index, 9_000); + employeeStatement.setLong(++index, employeeId); + employeeStatement.executeUpdate(); + } + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else { + throw new IllegalStateException(e); + } + } + }); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else { + throw new IllegalStateException(e); + } + } + update(aliceConnection, updateEmployeeSalarySql()); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else if (isCheckConstraintException(e)) { + preventedByConstraint.set(true); + } else { + throw new IllegalStateException(e); + } + } + doInJDBC(aliceConnection -> { + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + if (99_000 != salaryCount) { + LOGGER.info("Isolation level {} allows overbudgeting since the salary count is {} instead of 99000", isolationLevelName, salaryCount); + } else { + LOGGER.info( + "Isolation level {} prevents overbudgeting due to {}", + isolationLevelName, + preventedByLocking.get() ? + "locking" : + preventedByMVCC.get() ? + "MVCC" : + preventedByConstraint.get() ? + "check constraint" : + "unknown" + ); + } + }); + } + + @Test + public void testWriteSkewCheckConstraintSelectInsertUpdate() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + final AtomicBoolean preventedByConstraint = new AtomicBoolean(); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + + try { + executeSync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + try ( + PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); + ) { + int employeeId = 4; + int index = 0; + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Carol"); + employeeStatement.setLong(++index, 9_000); + employeeStatement.setLong(++index, employeeId); + employeeStatement.executeUpdate(); + } + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else { + throw new IllegalStateException(e); + } + } + }); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else { + throw new IllegalStateException(e); + } + } + update(aliceConnection, updateEmployeeSalarySql()); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else if (isCheckConstraintException(e)) { + preventedByConstraint.set(true); + } else { + throw new IllegalStateException(e); + } + } + doInJDBC(aliceConnection -> { + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + if (99_000 != salaryCount) { + LOGGER.info("Isolation level {} allows overbudgeting since the salary count is {} instead of 99000", isolationLevelName, salaryCount); + } else { + LOGGER.info( + "Isolation level {} prevents overbudgeting due to {}", + isolationLevelName, + preventedByLocking.get() ? + "locking" : + preventedByMVCC.get() ? + "MVCC" : + preventedByConstraint.get() ? + "check constraint" : + "unknown" + ); + } + }); + } + + private boolean isCheckConstraintException(Exception e) { + boolean constraintViolation = ExceptionUtil.rootCause(e).getMessage().contains("Overbudget"); + if(constraintViolation) { + LOGGER.error("Constraint violation", e); + } + return constraintViolation; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/PostgreSQLRangeBasedWriteSkewReadCommittedTest.java b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/PostgreSQLRangeBasedWriteSkewReadCommittedTest.java new file mode 100644 index 000000000..720c7de66 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/jdbc/transaction/phenomena/writeskew/PostgreSQLRangeBasedWriteSkewReadCommittedTest.java @@ -0,0 +1,177 @@ +package com.vladmihalcea.hpjp.jdbc.transaction.phenomena.writeskew; + +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.junit.Test; +import org.junit.runners.Parameterized; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLRangeBasedWriteSkewReadCommittedTest extends AbstractDepartmentEmployeePhenomenaTest { + + public PostgreSQLRangeBasedWriteSkewReadCommittedTest(String isolationLevelName, int isolationLevel) { + super(isolationLevelName, isolationLevel); + } + + @Override + protected Database database() { + return Database.POSTGRESQL; + } + + @Override + public void afterInit() { + super.afterInit(); + + doInJDBC(connection -> { + executeStatement("DROP TRIGGER IF EXISTS check_department_budget_trigger ON employee;"); + executeStatement("DROP FUNCTION check_department_budget();"); + + executeStatement("CREATE OR REPLACE FUNCTION check_department_budget() RETURNS trigger AS $$ " + + "DECLARE " + + " allowed_budget BIGINT; " + + " new_budget BIGINT; " + + "BEGIN " + + " SELECT INTO allowed_budget budget FROM department where id = NEW.department_id; " + + " SELECT INTO new_budget SUM(salary) FROM employee where department_id = NEW.department_id; " + + " IF new_budget > allowed_budget THEN " + + " RAISE EXCEPTION 'Overbudget department [id:%] by [%]', " + + " NEW.department_id, " + + " (new_budget - allowed_budget);" + + " END IF; " + + " RETURN NEW;" + + "END; " + + "$$ LANGUAGE plpgsql;" + ); + + executeStatement("CREATE TRIGGER check_department_budget_trigger " + + "AFTER INSERT OR UPDATE ON employee " + + "FOR EACH ROW EXECUTE PROCEDURE check_department_budget();" + ); + }); + } + + @Override + public void destroy() { + executeStatement("DROP TRIGGER IF EXISTS check_department_budget_trigger ON employee;"); + executeStatement("DROP FUNCTION check_department_budget();"); + super.destroy(); + } + + @Parameterized.Parameters + public static Collection isolationLevels() { + List levels = new ArrayList<>(); + levels.add(new Object[]{"Read Committed", Connection.TRANSACTION_READ_COMMITTED}); + return levels; + } + + private final CountDownLatch bobLatch = new CountDownLatch(1); + private final CountDownLatch aliceLatch = new CountDownLatch(1); + + @Test + public void testWriteSkewCheckConstraintWithoutBobCommit() { + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + final AtomicBoolean preventedByMVCC = new AtomicBoolean(); + final AtomicBoolean preventedByConstraint = new AtomicBoolean(); + + try { + doInJDBC(aliceConnection -> { + if (!aliceConnection.getMetaData().supportsTransactionIsolationLevel(isolationLevel)) { + LOGGER.info("Database {} doesn't support {}", dataSourceProvider().database(), isolationLevelName); + return; + } + prepareConnection(aliceConnection); + + try { + executeAsync(() -> { + doInJDBC(bobConnection -> { + prepareConnection(bobConnection); + try { + try ( + PreparedStatement employeeStatement = bobConnection.prepareStatement(insertEmployeeSql()); + ) { + int employeeId = 4; + int index = 0; + employeeStatement.setLong(++index, 1); + employeeStatement.setString(++index, "Dave"); + employeeStatement.setLong(++index, 9_000); + employeeStatement.setLong(++index, employeeId); + employeeStatement.executeUpdate(); + + bobLatch.countDown(); + awaitOnLatch(aliceLatch); + } + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else { + throw new IllegalStateException(e); + } + } + }); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else { + throw new IllegalStateException(e); + } + } + awaitOnLatch(bobLatch); + + update(aliceConnection, updateEmployeeSalarySql()); + + aliceLatch.countDown(); + }); + } catch (Exception e) { + if (ExceptionUtil.isLockTimeout(e)) { + preventedByLocking.set(true); + } else if (ExceptionUtil.isMVCCAnomalyDetection(e)) { + preventedByMVCC.set(true); + } else if (isCheckConstraintException(e)) { + preventedByConstraint.set(true); + } else { + throw new IllegalStateException(e); + } + } + doInJDBC(aliceConnection -> { + long salaryCount = selectColumn(aliceConnection, sumEmployeeSalarySql(), Number.class, Duration.ofSeconds(1)).longValue(); + if (99_000 != salaryCount) { + LOGGER.info("Isolation level {} allows overbudgeting since the salary count is {} instead of 99000", isolationLevelName, salaryCount); + } else { + LOGGER.info( + "Isolation level {} prevents overbudgeting due to {}", + isolationLevelName, + preventedByLocking.get() ? + "locking" : + preventedByMVCC.get() ? + "MVCC" : + preventedByConstraint.get() ? + "check constraint" : + "unknown" + ); + } + }); + } + + private boolean isCheckConstraintException(Exception e) { + boolean constraintViolation = ExceptionUtil.rootCause(e).getMessage().contains("Overbudget"); + if(constraintViolation) { + LOGGER.error("Constraint violation", e); + } + return constraintViolation; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/SpringBatchTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/SpringBatchTest.java new file mode 100644 index 000000000..47b57e7e3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/SpringBatchTest.java @@ -0,0 +1,143 @@ +package com.vladmihalcea.hpjp.spring.batch; + +import com.vladmihalcea.hpjp.spring.batch.config.SpringBatchConfiguration; +import com.vladmihalcea.hpjp.spring.batch.domain.Post; +import com.vladmihalcea.hpjp.spring.batch.domain.PostStatus; +import com.vladmihalcea.hpjp.spring.batch.service.ForumService; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.LongStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = SpringBatchConfiguration.class) +public class SpringBatchTest extends AbstractSpringTest { + + public static final int POST_COUNT = 5 * 1000; + + @Autowired + private ForumService forumService; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testBatchWrite() { + List posts = LongStream.rangeClosed(1, POST_COUNT) + .mapToObj(postId -> new Post() + .setId(postId) + .setTitle( + String.format("High-Performance Java Persistence - Page %d", + postId + ) + ) + .setStatus(PostStatus.PENDING) + ) + .toList(); + + forumService.createPosts(posts); + + LongStream.rangeClosed(1, 1000).boxed().forEach(id -> assertNotNull(forumService.findById(id))); + + List matchedPosts = forumService.findByIds( + LongStream.rangeClosed(1, 1000).boxed().toList() + ); + assertEquals(1000, matchedPosts.size()); + } + + private int threadCount = 6; + + private long threadExecutionSeconds = TimeUnit.MINUTES.toSeconds(3); + + private ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + + @Test + @Ignore + public void testRead() throws InterruptedException { + int POST_COUNT = 1000; + + List posts = LongStream.rangeClosed(1, POST_COUNT) + .mapToObj(postId -> new Post() + .setId(postId) + .setTitle( + String.format("High-Performance Java Persistence - Part %d", + postId + ) + ) + .setStatus(PostStatus.PENDING) + ) + .toList(); + + forumService.createPosts(posts); + + long startNanos = System.nanoTime(); + long endNanos = startNanos + TimeUnit.SECONDS.toNanos(threadExecutionSeconds); + + CountDownLatch awaitTermination = new CountDownLatch(threadCount); + List> tasks = new ArrayList<>(); + + ThreadLocalRandom random = ThreadLocalRandom.current(); + + final AtomicBoolean failed = new AtomicBoolean(); + + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + tasks.add( + () -> { + while (endNanos > System.nanoTime()) { + try { + Long id = random.nextLong(1, POST_COUNT); + LOGGER.info("Fetching entity by id [{}]", id); + Post post = forumService.findById(id); + assertNotNull(post); + + sleep(250, TimeUnit.MILLISECONDS); + } catch (Exception e) { + LOGGER.warn( + String.format( + "Thread [%d] execution failed", threadId + ), + e + ); + failed.set(true); + } + } + awaitTermination.countDown(); + return null; + } + ); + } + + executorService.invokeAll(tasks); + awaitTermination.await(); + LOGGER.info("Finished processing"); + } + + private void sleep(long duration, TimeUnit timeUnit) { + try { + Thread.sleep(timeUnit.toMillis(duration)); + } catch (InterruptedException e) { + Thread.interrupted(); + } + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/config/SpringBatchConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/config/SpringBatchConfiguration.java new file mode 100644 index 000000000..f51112101 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/config/SpringBatchConfiguration.java @@ -0,0 +1,134 @@ +package com.vladmihalcea.hpjp.spring.batch.config; + +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import jakarta.persistence.EntityManagerFactory; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +@EnableTransactionManagement +@EnableAspectJAutoProxy +@EnableJpaRepositories( + value = "com.vladmihalcea.hpjp.spring.batch.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +@ComponentScan( + value = { + "com.vladmihalcea.hpjp.spring.batch.service", + "io.hypersistence.utils.spring.aop" + } +) +public class SpringBatchConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + private int maxConnections = 64; + + @Bean + public static PropertySourcesPlaceholderConfigurer properties() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Bean + public Database database() { + //return Database.YUGABYTEDB_CLUSTER; + return Database.POSTGRESQL; + } + + @Bean + public DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + public HikariDataSource poolingDataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setMaximumPoolSize(maxConnections); + hikariConfig.setAutoCommit(false); + hikariConfig.setDataSource(dataSourceProvider().dataSource()); + return new HikariDataSource(hikariConfig); + } + + @Bean + public DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + return ProxyDataSourceBuilder + .create(poolingDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory( + @Autowired DataSource dataSource) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setPackagesToScan(packagesToScan()); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + entityManagerFactoryBean.setJpaProperties(additionalProperties()); + return entityManagerFactoryBean; + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { + return new TransactionTemplate(transactionManager(entityManagerFactory)); + } + + @Bean + public Integer batchProcessingSize() { + return 100; + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + properties.setProperty("hibernate.jdbc.batch_size", String.valueOf(batchProcessingSize())); + properties.setProperty("hibernate.order_inserts", "true"); + properties.setProperty("hibernate.order_updates", "true"); + return properties; + } + + protected String[] packagesToScan() { + return new String[]{ + "com.vladmihalcea.hpjp.spring.batch.domain" + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/domain/Post.java new file mode 100644 index 000000000..ec5cb8a76 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/domain/Post.java @@ -0,0 +1,46 @@ +package com.vladmihalcea.hpjp.spring.batch.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @Enumerated(EnumType.ORDINAL) + private PostStatus status; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/domain/PostStatus.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/domain/PostStatus.java new file mode 100644 index 000000000..c0379d8af --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/domain/PostStatus.java @@ -0,0 +1,10 @@ +package com.vladmihalcea.hpjp.spring.batch.domain; + +/** + * @author Vlad Mihalcea + */ +public enum PostStatus { + PENDING, + APPROVED, + SPAM; +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/repository/PostRepository.java new file mode 100644 index 000000000..9f6957283 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/repository/PostRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.batch.repository; + +import com.vladmihalcea.hpjp.spring.batch.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/service/ForumService.java new file mode 100644 index 000000000..a90e528ae --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/batch/service/ForumService.java @@ -0,0 +1,86 @@ +package com.vladmihalcea.hpjp.spring.batch.service; + +import com.vladmihalcea.hpjp.spring.batch.domain.Post; +import com.vladmihalcea.hpjp.spring.batch.repository.PostRepository; +import com.vladmihalcea.hpjp.util.CollectionUtils; +import io.hypersistence.utils.spring.annotation.Retry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; + +import java.net.SocketTimeoutException; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * @author Vlad Mihalcea + */ +@Service +public class ForumService { + + private static final Logger LOGGER = LoggerFactory.getLogger(ForumService.class); + + private static final ExecutorService executorService = Executors.newFixedThreadPool( + Runtime.getRuntime().availableProcessors() + ); + + private final PostRepository postRepository; + + private final TransactionTemplate transactionTemplate; + + private final int batchProcessingSize; + + public ForumService( + @Autowired PostRepository postRepository, + @Autowired TransactionTemplate transactionTemplate, + @Autowired int batchProcessingSize) { + this.postRepository = postRepository; + this.transactionTemplate = transactionTemplate; + this.batchProcessingSize = batchProcessingSize; + } + + @Transactional(propagation = Propagation.NEVER) + public void createPosts(List posts) { + CollectionUtils.spitInBatches(posts, batchProcessingSize) + .map(postBatch -> executorService.submit(() -> { + try { + transactionTemplate.execute((status) -> postRepository.persistAll(postBatch)); + } catch (TransactionException e) { + LOGGER.error("Batch transaction failure", e); + } + })) + .forEach(future -> { + try { + future.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + LOGGER.error("Batch execution failure", e); + } + }); + } + + @Transactional(readOnly = true) + public List findByIds(List ids) { + return postRepository.findAllById(ids); + } + + @Retry( + times = 3, + on = { + SocketTimeoutException.class, + TransactionSystemException.class + } + ) + public Post findById(Long id) { + return postRepository.findById(id).orElse(null); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/SpringBlazePersistenceKeysetPaginationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/SpringBlazePersistenceKeysetPaginationTest.java new file mode 100644 index 000000000..0d38749d4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/SpringBlazePersistenceKeysetPaginationTest.java @@ -0,0 +1,101 @@ +package com.vladmihalcea.hpjp.spring.blaze; + +import com.blazebit.persistence.PagedList; +import com.vladmihalcea.hpjp.spring.blaze.config.SpringBlazePersistenceConfiguration; +import com.vladmihalcea.hpjp.spring.blaze.domain.*; +import com.vladmihalcea.hpjp.spring.blaze.service.ForumService; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringBlazePersistenceConfiguration.class) +public class SpringBlazePersistenceKeysetPaginationTest extends AbstractSpringTest { + + public static final int POST_COUNT = 50; + public static final int PAGE_SIZE = 25; + + @Autowired + private ForumService forumService; + + @Override + protected Class[] entities() { + return new Class[] { + UserVote.class, + PostComment.class, + Post.class, + Tag.class, + User.class, + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + LocalDateTime timestamp = LocalDateTime.of( + 2021, 12, 30, 12, 0, 0, 0 + ); + + LongStream.rangeClosed(1, POST_COUNT).forEach(postId -> { + Post post = new Post() + .setId(postId) + .setTitle( + String.format("High-Performance Java Persistence - Chapter %d", + postId) + ) + .setCreatedOn( + Timestamp.valueOf(timestamp.plusMinutes(postId)) + ); + + entityManager.persist(post); + }); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void test() { + PagedList topPage = forumService.firstLatestPosts(PAGE_SIZE); + + assertEquals(POST_COUNT, topPage.getTotalSize()); + assertEquals(POST_COUNT / PAGE_SIZE, topPage.getTotalPages()); + assertEquals(1, topPage.getPage()); + List topIds = topPage.stream() + .map(Post::getId) + .toList(); + assertEquals(Long.valueOf(50), topIds.get(0)); + assertEquals(Long.valueOf(49), topIds.get(1)); + + LOGGER.info("Top ids: {}", topIds); + + PagedList nextPage = forumService.findNextLatestPosts(topPage); + + assertEquals(2, nextPage.getPage()); + + List nextIds = nextPage.stream() + .map(Post::getId) + .toList(); + assertEquals(Long.valueOf(25), nextIds.get(0)); + assertEquals(Long.valueOf(24), nextIds.get(1)); + + LOGGER.info("Next ids: {}", nextIds); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/SpringBlazePersistenceMockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/SpringBlazePersistenceMockTest.java new file mode 100644 index 000000000..721c59a46 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/SpringBlazePersistenceMockTest.java @@ -0,0 +1,40 @@ +package com.vladmihalcea.hpjp.spring.blaze; + +import com.blazebit.persistence.PagedList; +import com.vladmihalcea.hpjp.spring.blaze.domain.Post; +import com.vladmihalcea.hpjp.spring.blaze.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.blaze.service.ForumService; +import org.junit.Test; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Sort; + +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * @author Vlad Mihalcea + */ +public class SpringBlazePersistenceMockTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Test + public void testWithMocks() { + final int PAGE_SIZE = 25; + + PostRepository postRepository = Mockito.mock(PostRepository.class); + ForumService forumService = new ForumService(postRepository); + + PagedList pagedList = Mockito.mock(PagedList.class); + when(postRepository.findTopN(any(Sort.class), eq(PAGE_SIZE))).thenReturn(pagedList); + + PagedList topPage = forumService.firstLatestPosts(PAGE_SIZE); + + assertSame(pagedList, topPage); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/SpringBlazePersistenceMultisetTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/SpringBlazePersistenceMultisetTest.java new file mode 100644 index 000000000..f78a80153 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/SpringBlazePersistenceMultisetTest.java @@ -0,0 +1,178 @@ +package com.vladmihalcea.hpjp.spring.blaze; + +import com.vladmihalcea.hpjp.spring.blaze.config.SpringBlazePersistenceConfiguration; +import com.vladmihalcea.hpjp.spring.blaze.domain.*; +import com.vladmihalcea.hpjp.spring.blaze.domain.views.PostCommentView; +import com.vladmihalcea.hpjp.spring.blaze.domain.views.PostWithCommentsAndTagsView; +import com.vladmihalcea.hpjp.spring.blaze.service.ForumService; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import org.hibernate.loader.MultipleBagFetchException; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringBlazePersistenceConfiguration.class) +public class SpringBlazePersistenceMultisetTest extends AbstractSpringTest { + + public static final int POST_COUNT = 50; + public static final int POST_COMMENT_COUNT = 20; + public static final int TAG_COUNT = 10; + public static final int VOTE_COUNT = 5; + + @Autowired + private ForumService forumService; + + @Override + protected Class[] entities() { + return new Class[] { + UserVote.class, + PostComment.class, + Post.class, + Tag.class, + User.class, + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + User alice = new User() + .setId(1L) + .setFirstName("Alice") + .setLastName("Smith"); + + User bob = new User() + .setId(2L) + .setFirstName("Bob") + .setLastName("Johnson"); + + entityManager.persist(alice); + entityManager.persist(bob); + + List tags = new ArrayList<>(); + + for (long i = 1; i <= TAG_COUNT; i++) { + Tag tag = new Tag() + .setId(i) + .setName(String.format("Tag nr. %d", i)); + + entityManager.persist(tag); + tags.add(tag); + } + + long commentId = 0; + long voteId = 0; + + for (long postId = 1; postId <= POST_COUNT; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post nr. %d", postId)); + + + for (long i = 0; i < POST_COMMENT_COUNT; i++) { + PostComment comment = new PostComment() + .setId(++commentId) + .setReview("Excellent!"); + + for (int j = 0; j < VOTE_COUNT; j++) { + comment.addVote( + new UserVote() + .setId(++voteId) + .setScore(Math.random() > 0.5 ? 1 : -1) + .setUser(Math.random() > 0.5 ? alice : bob) + ); + } + + post.addComment(comment); + + } + + for (int i = 0; i < TAG_COUNT; i++) { + post.getTags().add(tags.get(i)); + } + + entityManager.persist(post); + } + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void testWithCartesianProduct() { + transactionTemplate.execute(transactionStatus -> { + try { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.tags t + left join fetch p.comments pc + left join fetch pc.votes v + left join fetch v.user u + where p.id between :minId and :maxId + """, Post.class) + .setParameter("minId", 1L) + .setParameter("maxId", 50L) + .getResultList(); + + fail("Should have thrown MultipleBagFetchException"); + } catch (IllegalArgumentException e) { + LOGGER.info("Expected", e); + assertEquals(MultipleBagFetchException.class, ExceptionUtil.rootCause(e).getClass()); + } + return null; + }); + } + + @Test + public void testWithSuccessiveJoinFetch() { + List posts = forumService.findWithCommentsAndTagsByIds( + 1L, 50L + ); + + assertEquals(POST_COUNT, posts.size()); + + for (Post post : posts) { + assertEquals(POST_COMMENT_COUNT, post.getComments().size()); + for(PostComment comment : post.getComments()) { + assertEquals(VOTE_COUNT, comment.getVotes().size()); + } + assertEquals(TAG_COUNT, post.getTags().size()); + } + } + + @Test + public void testWithMultiset() { + List posts = forumService.findPostWithCommentsAndTagsViewByIds( + 1L, 50L + ); + + assertEquals(POST_COUNT, posts.size()); + + for (PostWithCommentsAndTagsView post : posts) { + assertEquals(POST_COMMENT_COUNT, post.getComments().size()); + for(PostCommentView comment : post.getComments()) { + assertEquals(VOTE_COUNT, comment.getVotes().size()); + } + assertEquals(TAG_COUNT, post.getTags().size()); + } + } + +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/config/SpringBlazePersistenceConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/config/SpringBlazePersistenceConfiguration.java new file mode 100644 index 000000000..1be8ca1fb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/config/SpringBlazePersistenceConfiguration.java @@ -0,0 +1,150 @@ +package com.vladmihalcea.hpjp.spring.blaze.config; + +import com.blazebit.persistence.Criteria; +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.spi.CriteriaBuilderConfiguration; +import com.blazebit.persistence.view.EntityViewManager; +import com.blazebit.persistence.view.EntityViews; +import com.blazebit.persistence.view.spi.EntityViewConfiguration; +import com.vladmihalcea.hpjp.spring.blaze.domain.Post; +import com.vladmihalcea.hpjp.spring.blaze.domain.views.PostView; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import jakarta.persistence.EntityManagerFactory; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.blaze", + } +) +@EnableTransactionManagement +@EnableAspectJAutoProxy +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.blaze.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringBlazePersistenceConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + @Bean + public Database database() { + return Database.POSTGRESQL; + } + + @Bean + public DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + public DataSource poolingDataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setMaximumPoolSize(64); + hikariConfig.setAutoCommit(false); + hikariConfig.setDataSource(dataSourceProvider().dataSource()); + return new HikariDataSource(hikariConfig); + } + + @Bean + public DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + DataSource dataSource = ProxyDataSourceBuilder + .create(poolingDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + return dataSource; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory( + @Autowired DataSource dataSource) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setPackagesToScan(packagesToScan()); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + entityManagerFactoryBean.setJpaProperties(additionalProperties()); + return entityManagerFactoryBean; + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { + return new TransactionTemplate(transactionManager(entityManagerFactory)); + } + + @Bean + public CriteriaBuilderFactory criteriaBuilderFactory(EntityManagerFactory entityManagerFactory) { + CriteriaBuilderConfiguration config = Criteria.getDefault(); + return config.createCriteriaBuilderFactory(entityManagerFactory); + } + + @Bean + public EntityViewConfiguration entityViewConfiguration(CriteriaBuilderFactory criteriaBuilderFactory) { + EntityViewConfiguration entityViewConfiguration = EntityViews.createDefaultConfiguration(); + for(Class entityViewClass : ReflectionUtils.getClassesByPackage(PostView.class.getPackageName())) { + entityViewConfiguration.addEntityView(entityViewClass); + } + return entityViewConfiguration; + } + + @Bean + public EntityViewManager entityViewManager(CriteriaBuilderFactory cbf, EntityViewConfiguration entityViewConfiguration) { + return entityViewConfiguration.createEntityViewManager(cbf); + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + properties.setProperty("hibernate.jdbc.batch_size", "50"); + properties.setProperty("hibernate.order_inserts", "true"); + properties.setProperty("hibernate.order_updates", "true"); + return properties; + } + + protected String[] packagesToScan() { + return new String[]{ + Post.class.getPackage().getName() + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/Post.java new file mode 100644 index 000000000..3eded97cc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/Post.java @@ -0,0 +1,78 @@ +package com.vladmihalcea.hpjp.spring.blaze.domain; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on") + private Date createdOn; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/PostComment.java new file mode 100644 index 000000000..dbb8a3419 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/PostComment.java @@ -0,0 +1,62 @@ +package com.vladmihalcea.hpjp.spring.blaze.domain; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + @OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true) + private List votes = new ArrayList<>(); + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public List getVotes() { + return votes; + } + + public PostComment addVote(UserVote vote) { + votes.add(vote); + vote.setComment(this); + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/Tag.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/Tag.java new file mode 100644 index 000000000..529be4893 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/Tag.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.spring.blaze.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Tag") +@Table(name = "tag") +public class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/User.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/User.java new file mode 100644 index 000000000..f73128bc9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/User.java @@ -0,0 +1,50 @@ +package com.vladmihalcea.hpjp.spring.blaze.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "User") +@Table(name = "blog_user") +public class User { + + @Id + private Long id; + + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + public Long getId() { + return id; + } + + public User setId(Long id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public User setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public User setLastName(String lastName) { + this.lastName = lastName; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/UserVote.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/UserVote.java new file mode 100644 index 000000000..2e6c11e08 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/UserVote.java @@ -0,0 +1,58 @@ +package com.vladmihalcea.hpjp.spring.blaze.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "UserVote") +@Table(name = "user_vote") +public class UserVote { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + private PostComment comment; + + private int score; + + public Long getId() { + return id; + } + + public UserVote setId(Long id) { + this.id = id; + return this; + } + + public User getUser() { + return user; + } + + public UserVote setUser(User user) { + this.user = user; + return this; + } + + public PostComment getComment() { + return comment; + } + + public UserVote setComment(PostComment comment) { + this.comment = comment; + return this; + } + + public int getScore() { + return score; + } + + public UserVote setScore(int score) { + this.score = score; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/PostCommentView.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/PostCommentView.java new file mode 100644 index 000000000..172ee1f84 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/PostCommentView.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.spring.blaze.domain.views; + +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; +import com.blazebit.persistence.view.Mapping; +import com.vladmihalcea.hpjp.spring.blaze.domain.PostComment; + +import java.util.List; + +import static com.blazebit.persistence.view.FetchStrategy.MULTISET; + +/** + * @author Vlad Mihalcea + */ +@EntityView(PostComment.class) +public interface PostCommentView { + @IdMapping + Long getId(); + + String getReview(); + + @Mapping(fetch = MULTISET) + List getVotes(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/PostView.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/PostView.java new file mode 100644 index 000000000..646042d71 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/PostView.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.spring.blaze.domain.views; + +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; +import com.vladmihalcea.hpjp.spring.blaze.domain.Post; + +/** + * @author Vlad Mihalcea + */ +@EntityView(Post.class) +public interface PostView { + @IdMapping + Long getId(); + + String getTitle(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/PostWithCommentsAndTagsView.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/PostWithCommentsAndTagsView.java new file mode 100644 index 000000000..f0f95035a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/PostWithCommentsAndTagsView.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.spring.blaze.domain.views; + +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.Mapping; +import com.vladmihalcea.hpjp.spring.blaze.domain.Post; + +import java.util.List; + +import static com.blazebit.persistence.view.FetchStrategy.MULTISET; + +/** + * @author Vlad Mihalcea + */ +@EntityView(Post.class) +public interface PostWithCommentsAndTagsView extends PostView { + + @Mapping(fetch = MULTISET) + List getComments(); + + @Mapping(fetch = MULTISET) + List getTags(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/TagView.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/TagView.java new file mode 100644 index 000000000..2db1e7999 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/TagView.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.spring.blaze.domain.views; + +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; +import com.vladmihalcea.hpjp.spring.blaze.domain.Tag; + +/** + * @author Vlad Mihalcea + */ +@EntityView(Tag.class) +public interface TagView { + @IdMapping + Long getId(); + + String getName(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/UserView.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/UserView.java new file mode 100644 index 000000000..350f33303 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/UserView.java @@ -0,0 +1,18 @@ +package com.vladmihalcea.hpjp.spring.blaze.domain.views; + +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; +import com.vladmihalcea.hpjp.spring.blaze.domain.User; + +/** + * @author Vlad Mihalcea + */ +@EntityView(User.class) +public interface UserView { + @IdMapping + Long getId(); + + String getFirstName(); + + String getLastName(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/UserVoteView.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/UserVoteView.java new file mode 100644 index 000000000..1f3dcce5e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/domain/views/UserVoteView.java @@ -0,0 +1,18 @@ +package com.vladmihalcea.hpjp.spring.blaze.domain.views; + +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; +import com.vladmihalcea.hpjp.spring.blaze.domain.UserVote; + +/** + * @author Vlad Mihalcea + */ +@EntityView(UserVote.class) +public interface UserVoteView { + @IdMapping + Long getId(); + + UserView getUser(); + + int getScore(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/repository/CustomPostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/repository/CustomPostRepository.java new file mode 100644 index 000000000..81aa7b173 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/repository/CustomPostRepository.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.spring.blaze.repository; + +import com.blazebit.persistence.PagedList; +import com.vladmihalcea.hpjp.spring.blaze.domain.Post; +import com.vladmihalcea.hpjp.spring.blaze.domain.views.PostWithCommentsAndTagsView; +import org.springframework.data.domain.Sort; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface CustomPostRepository { + + PagedList findTopN(Sort sortBy, int pageSize); + + PagedList findNextN(Sort sortBy, PagedList previousPage); + + List findWithCommentsAndTagsByIds(Long minId, Long maxId); + + List findPostWithCommentsAndTagsViewByIds(Long minId, Long maxId); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/repository/CustomPostRepositoryImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/repository/CustomPostRepositoryImpl.java new file mode 100644 index 000000000..630ca6910 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/repository/CustomPostRepositoryImpl.java @@ -0,0 +1,116 @@ +package com.vladmihalcea.hpjp.spring.blaze.repository; + +import com.blazebit.persistence.CriteriaBuilder; +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.PagedList; +import com.blazebit.persistence.view.EntityViewManager; +import com.blazebit.persistence.view.EntityViewSetting; +import com.vladmihalcea.hpjp.hibernate.forum.Post_; +import com.vladmihalcea.hpjp.spring.blaze.domain.Post; +import com.vladmihalcea.hpjp.spring.blaze.domain.PostComment; +import com.vladmihalcea.hpjp.spring.blaze.domain.views.PostWithCommentsAndTagsView; +import jakarta.persistence.EntityManager; +import org.springframework.data.domain.Sort; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class CustomPostRepositoryImpl implements CustomPostRepository { + + private final EntityManager entityManager; + + private final CriteriaBuilderFactory criteriaBuilderFactory; + + private final EntityViewManager entityViewManager; + + public CustomPostRepositoryImpl( + EntityManager entityManager, + CriteriaBuilderFactory criteriaBuilderFactory, + EntityViewManager entityViewManager) { + this.entityManager = entityManager; + this.criteriaBuilderFactory = criteriaBuilderFactory; + this.entityViewManager = entityViewManager; + } + + @Override + public PagedList findTopN(Sort sortBy, int pageSize) { + return sortedCriteriaBuilder(sortBy) + .page(0, pageSize) + .withKeysetExtraction(true) + .getResultList(); + } + + @Override + public PagedList findNextN(Sort sortBy, PagedList previousPage) { + return sortedCriteriaBuilder(sortBy) + .page( + previousPage.getKeysetPage(), + previousPage.getPage() * previousPage.getMaxResults(), + previousPage.getMaxResults() + ) + .getResultList(); + } + + @Override + public List findWithCommentsAndTagsByIds(Long minId, Long maxId) { + List posts = entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + where p.id between :minId and :maxId + """, Post.class) + .setParameter("minId", minId) + .setParameter("maxId", maxId) + .getResultList(); + + entityManager.createQuery(""" + select p + from Post p + left join fetch p.tags t + where p.id between :minId and :maxId + """, Post.class) + .setParameter("minId", minId) + .setParameter("maxId", maxId) + .getResultList(); + + entityManager.createQuery(""" + select pc + from PostComment pc + left join fetch pc.votes v + left join fetch v.user u + join pc.post p + where p.id between :minId and :maxId + """, PostComment.class) + .setParameter("minId", minId) + .setParameter("maxId", maxId) + .getResultList(); + + return posts; + } + + @Override + public List findPostWithCommentsAndTagsViewByIds( + Long minId, Long maxId) { + return entityViewManager.applySetting( + EntityViewSetting.create(PostWithCommentsAndTagsView.class), + criteriaBuilderFactory.create(entityManager, Post.class) + ) + .where(Post_.ID) + .betweenExpression(":minId") + .andExpression(":maxId") + .setParameter("minId", minId) + .setParameter("maxId", maxId) + .getResultList(); + } + + private CriteriaBuilder sortedCriteriaBuilder(Sort sortBy) { + CriteriaBuilder criteriaBuilder = criteriaBuilderFactory + .create(entityManager, Post.class); + sortBy.forEach(order -> { + criteriaBuilder.orderBy(order.getProperty(), order.isAscending()); + }); + return criteriaBuilder; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/repository/PostRepository.java new file mode 100644 index 000000000..f1ee05e50 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/repository/PostRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.blaze.repository; + +import com.vladmihalcea.hpjp.spring.blaze.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository, CustomPostRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/service/ForumService.java new file mode 100644 index 000000000..6e58b45cc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/blaze/service/ForumService.java @@ -0,0 +1,48 @@ +package com.vladmihalcea.hpjp.spring.blaze.service; + +import com.blazebit.persistence.PagedList; +import com.vladmihalcea.hpjp.spring.blaze.domain.Post; +import com.vladmihalcea.hpjp.spring.blaze.domain.Post_; +import com.vladmihalcea.hpjp.spring.blaze.domain.views.PostWithCommentsAndTagsView; +import com.vladmihalcea.hpjp.spring.blaze.repository.PostRepository; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumService { + + private final PostRepository postRepository; + + public ForumService(PostRepository postRepository) { + this.postRepository = postRepository; + } + + public PagedList firstLatestPosts(int pageSize) { + return postRepository.findTopN( + Sort.by(Post_.CREATED_ON).descending().and(Sort.by(Post_.ID).descending()), + pageSize + ); + } + + public PagedList findNextLatestPosts(PagedList previousPage) { + return postRepository.findNextN( + Sort.by(Post_.CREATED_ON).descending().and(Sort.by(Post_.ID).descending()), + previousPage + ); + } + + public List findWithCommentsAndTagsByIds(Long minId, Long maxId) { + return postRepository.findWithCommentsAndTagsByIds(minId, maxId); + } + + public List findPostWithCommentsAndTagsViewByIds(Long minId, Long maxId) { + return postRepository.findPostWithCommentsAndTagsViewByIds(minId, maxId); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/common/AbstractSpringTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/AbstractSpringTest.java new file mode 100644 index 000000000..eb38e5622 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/AbstractSpringTest.java @@ -0,0 +1,50 @@ +package com.vladmihalcea.hpjp.spring.common; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.Root; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.support.TransactionTemplate; + +/** + * @author Vlad Mihalcea + */ +@RunWith(SpringJUnit4ClassRunner.class) +public abstract class AbstractSpringTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Autowired + protected TransactionTemplate transactionTemplate; + + @PersistenceContext + protected EntityManager entityManager; + + @Before + public void init() { + transactionTemplate.execute(transactionStatus -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + for(Class entityClass : entities()) { + CriteriaDelete criteria = builder.createCriteriaDelete(entityClass); + criteria.from(entityClass); + entityManager.createQuery(criteria).executeUpdate(); + } + return null; + }); + afterInit(); + } + + protected abstract Class[] entities(); + + protected void afterInit() { + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/common/config/CommonSpringConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/config/CommonSpringConfiguration.java new file mode 100644 index 000000000..632b7a1ce --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/config/CommonSpringConfiguration.java @@ -0,0 +1,127 @@ +package com.vladmihalcea.hpjp.spring.common.config; + +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import jakarta.persistence.EntityManagerFactory; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.common", + } +) +@EnableTransactionManagement +@EnableAspectJAutoProxy +@EnableJpaRepositories( + value = { + "io.hypersistence.utils.spring.repository", + "com.vladmihalcea.hpjp.spring.common.repository", + }, + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class CommonSpringConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + @Bean + public Database database() { + return Database.POSTGRESQL; + } + + @Bean + public DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + public DataSource poolingDataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setMaximumPoolSize(64); + hikariConfig.setAutoCommit(false); + hikariConfig.setDataSource(dataSourceProvider().dataSource()); + return new HikariDataSource(hikariConfig); + } + + @Bean + public DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + DataSource dataSource = ProxyDataSourceBuilder + .create(poolingDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + return dataSource; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory( + @Autowired DataSource dataSource) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setPackagesToScan(packagesToScan()); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + entityManagerFactoryBean.setJpaProperties(properties()); + return entityManagerFactoryBean; + } + + protected String[] packagesToScan() { + return new String[]{ + "com.vladmihalcea.hpjp.spring.common.domain" + }; + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { + return new TransactionTemplate(transactionManager(entityManagerFactory)); + } + + protected Properties properties() { + Properties properties = new Properties(); + properties.setProperty(AvailableSettings.HBM2DDL_AUTO, "create-drop"); + properties.setProperty(AvailableSettings.CONNECTION_PROVIDER_DISABLES_AUTOCOMMIT, Boolean.TRUE.toString()); + additionalProperties(properties); + return properties; + } + + protected void additionalProperties(Properties properties) { + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/common/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/domain/Post.java new file mode 100644 index 000000000..45cb65aa6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/domain/Post.java @@ -0,0 +1,53 @@ +package com.vladmihalcea.hpjp.spring.common.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.NaturalId; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table( + name = "post", + uniqueConstraints = @UniqueConstraint( + name = "UK_POST_SLUG", + columnNames = "slug" + ) +) +public class Post { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @NaturalId + private String slug; + + @Version + private short version; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Post setSlug(String slug) { + this.slug = slug; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/common/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/domain/PostComment.java new file mode 100644 index 000000000..632170779 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/domain/PostComment.java @@ -0,0 +1,48 @@ +package com.vladmihalcea.hpjp.spring.common.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post_comment") +public class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(foreignKey = @ForeignKey(name = "FK_POST_COMMENT_POST_ID")) + private Post post; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/common/repository/PostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/repository/PostCommentRepository.java new file mode 100644 index 000000000..2b1817258 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/repository/PostCommentRepository.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.common.repository; + +import com.vladmihalcea.hpjp.spring.common.domain.PostComment; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostCommentRepository extends BaseJpaRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/common/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/repository/PostRepository.java new file mode 100644 index 000000000..f8490a908 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/repository/PostRepository.java @@ -0,0 +1,14 @@ +package com.vladmihalcea.hpjp.spring.common.repository; + +import com.vladmihalcea.hpjp.spring.common.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { + + Post findBySlug(String slug); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/common/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/service/ForumService.java new file mode 100644 index 000000000..dd0573466 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/common/service/ForumService.java @@ -0,0 +1,61 @@ +package com.vladmihalcea.hpjp.spring.common.service; + +import com.vladmihalcea.hpjp.spring.common.domain.Post; +import com.vladmihalcea.hpjp.spring.common.domain.PostComment; +import com.vladmihalcea.hpjp.spring.common.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.common.repository.PostRepository; +import jakarta.persistence.LockModeType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumService { + + private final PostRepository postRepository; + + private final PostCommentRepository postCommentRepository; + + public ForumService( + PostRepository postRepository, + PostCommentRepository postCommentRepository) { + this.postRepository = postRepository; + this.postCommentRepository = postCommentRepository; + } + + @Transactional + public Post createPost(String title, String slug) { + return postRepository.persist( + new Post() + .setTitle(title) + .setSlug(slug) + ); + } + + public Post findBySlug(String slug){ + return postRepository.findBySlug(slug); + } + + @Transactional + public void updatePostTitle(String slug, String title) { + Post post = findBySlug(slug); + post.setTitle(title); + postRepository.flush(); + } + + @Transactional + public void addComment(Long postId, String review) { + Post post = postRepository.lockById(postId, LockModeType.OPTIMISTIC); + + postCommentRepository.persist( + new PostComment() + .setReview(review) + .setPost(post) + ); + + postRepository.lockById(postId, LockModeType.PESSIMISTIC_READ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/SpringDataJPAAssignedTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/SpringDataJPAAssignedTest.java new file mode 100644 index 000000000..0d1a5c150 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/SpringDataJPAAssignedTest.java @@ -0,0 +1,59 @@ +package com.vladmihalcea.hpjp.spring.data.assigned; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.assigned.config.SpringDataJPAAssignedConfiguration; +import com.vladmihalcea.hpjp.spring.data.assigned.domain.Book; +import com.vladmihalcea.hpjp.spring.data.assigned.repository.BookBaseJpaRepository; +import com.vladmihalcea.hpjp.spring.data.assigned.repository.BookRepository; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAAssignedConfiguration.class) +public class SpringDataJPAAssignedTest extends AbstractSpringTest { + + @Autowired + private BookRepository bookRepository; + + @Autowired + private BookBaseJpaRepository bookBaseJpaRepository; + + @Override + protected Class[] entities() { + return new Class[] { + Book.class + }; + } + + @Test + public void testJpaRepositorySave() { + transactionTemplate.execute(status -> { + bookRepository.save( + new Book() + .setIsbn(9789730228236L) + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea") + ); + + return null; + }); + } + + @Test + public void testBaseJpaRepositoryPersist() { + transactionTemplate.execute(status -> { + bookBaseJpaRepository.persist( + new Book() + .setIsbn(9789730228236L) + .setTitle("High-Performance Java Persistence") + .setAuthor("Vlad Mihalcea") + ); + + return null; + }); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/config/SpringDataJPAAssignedConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/config/SpringDataJPAAssignedConfiguration.java new file mode 100644 index 000000000..96703dea4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/config/SpringDataJPAAssignedConfiguration.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.spring.data.assigned.config; + +import com.vladmihalcea.hpjp.spring.data.assigned.domain.Book; +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.assigned", + } +) +@EnableJpaRepositories( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.assigned.repository", + }, + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPAAssignedConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Book.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/domain/Book.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/domain/Book.java new file mode 100644 index 000000000..4243bd442 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/domain/Book.java @@ -0,0 +1,47 @@ +package com.vladmihalcea.hpjp.spring.data.assigned.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Book") +@Table(name = "book") +public class Book { + + @Id + private Long isbn; + + private String title; + + private String author; + + public Long getIsbn() { + return isbn; + } + + public Book setIsbn(Long isbn) { + this.isbn = isbn; + return this; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/repository/BookBaseJpaRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/repository/BookBaseJpaRepository.java new file mode 100644 index 000000000..c121c38f1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/repository/BookBaseJpaRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.assigned.repository; + +import com.vladmihalcea.hpjp.spring.data.assigned.domain.Book; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface BookBaseJpaRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/repository/BookRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/repository/BookRepository.java new file mode 100644 index 000000000..c84b7a6a8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/assigned/repository/BookRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.assigned.repository; + +import com.vladmihalcea.hpjp.spring.data.assigned.domain.Book; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface BookRepository extends JpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/SpringDataJPAAuditTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/SpringDataJPAAuditTest.java new file mode 100644 index 000000000..75f044a25 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/SpringDataJPAAuditTest.java @@ -0,0 +1,128 @@ +package com.vladmihalcea.hpjp.spring.data.audit; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.audit.config.SpringDataJPAAuditConfiguration; +import com.vladmihalcea.hpjp.spring.data.audit.domain.Post; +import com.vladmihalcea.hpjp.spring.data.audit.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.audit.domain.PostStatus; +import com.vladmihalcea.hpjp.spring.data.audit.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.data.audit.service.PostService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.history.Revision; +import org.springframework.data.history.RevisionSort; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAAuditConfiguration.class) +public class SpringDataJPAAuditTest extends AbstractSpringTest { + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostService postService; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + PostComment.class + }; + } + + @Test + public void test() { + Post post = new Post() + .setTitle("High-Performance Java Persistence 1st edition") + .setSlug("high-performance-java-persistence") + .setStatus(PostStatus.APPROVED); + + postService.savePostAndComments( + post, + new PostComment() + .setPost(post) + .setReview("A must-read for every Java developer!"), + new PostComment() + .setPost(post) + .setReview("Best book on JPA and Hibernate!") + ); + + post.setTitle("High-Performance Java Persistence 2nd edition"); + postService.savePost(post); + + postService.deletePost(post); + + Revision latestRevision = postRepository + .findLastChangeRevision(post.getId()) + .orElseThrow(); + + LOGGER.info( + "The latest Post entity operation was [{}] at revision [{}]", + latestRevision.getMetadata().getRevisionType(), + latestRevision.getRevisionNumber().orElseThrow() + ); + + for(Revision revision : postRepository.findRevisions(post.getId())) { + LOGGER.info( + "At revision [{}], the Post entity state was: [{}]", + revision.getRevisionNumber().orElseThrow(), + revision.getEntity() + ); + } + + testPagination(); + } + + private void testPagination() { + Post post = new Post() + .setTitle("Hypersistence Optimizer, version 1.0.0") + .setSlug("hypersistence-optimizer") + .setStatus(PostStatus.APPROVED); + postService.savePost(post); + + for (int i = 1; i < 20; i++) { + post.setTitle( + String.format( + "Hypersistence Optimizer, version 1.%d.%d", + i/10, + i%10 + ) + ); + postService.savePost(post); + } + + int pageSize = 10; + + Page> firstPage = postRepository.findRevisions( + post.getId(), + PageRequest.of(0, pageSize, RevisionSort.desc()) + ); + + logPage(firstPage); + + Page> secondPage = postRepository.findRevisions( + post.getId(), + PageRequest.of(1, pageSize, RevisionSort.desc()) + ); + + logPage(secondPage); + } + + private void logPage(Page> revisionPage) { + for(Revision revision : revisionPage) { + LOGGER.info( + String.format( + "At revision [%02d], the Post entity state was: [%s]", + revision.getRevisionNumber().orElseThrow(), + revision.getEntity() + ) + ); + } + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/config/SpringDataJPAAuditConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/config/SpringDataJPAAuditConfiguration.java new file mode 100644 index 000000000..e007666c4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/config/SpringDataJPAAuditConfiguration.java @@ -0,0 +1,47 @@ +package com.vladmihalcea.hpjp.spring.data.audit.config; + +import com.vladmihalcea.hpjp.spring.data.audit.domain.Post; +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import org.hibernate.envers.configuration.EnversSettings; +import org.hibernate.envers.strategy.internal.ValidityAuditStrategy; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.audit", + } +) +@EnableJpaRepositories( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.audit.service", + "com.vladmihalcea.hpjp.spring.data.audit.repository", + "io.hypersistence.utils.spring.repository" + }, + repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class +) +public class SpringDataJPAAuditConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + properties.setProperty( + EnversSettings.AUDIT_STRATEGY, + ValidityAuditStrategy.class.getName() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/domain/Post.java new file mode 100644 index 000000000..6781fc4a6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/domain/Post.java @@ -0,0 +1,81 @@ +package com.vladmihalcea.hpjp.spring.data.audit.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.NaturalId; +import org.hibernate.envers.Audited; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table( + name = "post", + uniqueConstraints = @UniqueConstraint( + name = "UK_POST_SLUG", + columnNames = "slug" + ) +) +@Audited +public class Post { + + @Id + @GeneratedValue + private Long id; + + @Column(length = 100) + private String title; + + @NaturalId + @Column(length = 75) + private String slug; + + @Enumerated(EnumType.ORDINAL) + @Column(columnDefinition = "NUMERIC(2)") + private PostStatus status; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public String getSlug() { + return slug; + } + + public Post setSlug(String slug) { + this.slug = slug; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } + + @Override + public String toString() { + return "Post{" + + "id=" + id + + ", title='" + title + '\'' + + ", slug='" + slug + '\'' + + ", status=" + status + + '}'; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/domain/PostComment.java new file mode 100644 index 000000000..4bd094770 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/domain/PostComment.java @@ -0,0 +1,53 @@ +package com.vladmihalcea.hpjp.spring.data.audit.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.envers.Audited; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post_comment") +@Audited +public class PostComment { + + @Id + @GeneratedValue + private Long id; + + @Column(length = 250) + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(foreignKey = @ForeignKey(name = "FK_POST_COMMENT_POST_ID")) + private Post post; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/domain/PostStatus.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/domain/PostStatus.java new file mode 100644 index 000000000..bf0b55ab0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/domain/PostStatus.java @@ -0,0 +1,11 @@ +package com.vladmihalcea.hpjp.spring.data.audit.domain; + +/** + * @author Vlad Mihalcea + */ +public enum PostStatus { + PENDING, + APPROVED, + SPAM, + REQUIRES_MODERATOR_INTERVENTION +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/repository/PostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/repository/PostCommentRepository.java new file mode 100644 index 000000000..e3b9904e6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/repository/PostCommentRepository.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hpjp.spring.data.audit.repository; + +import com.vladmihalcea.hpjp.spring.data.audit.domain.Post; +import com.vladmihalcea.hpjp.spring.data.audit.domain.PostComment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.history.RevisionRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostCommentRepository extends JpaRepository, + RevisionRepository { + + void deleteByPost(Post post); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/repository/PostRepository.java new file mode 100644 index 000000000..0f5cec3be --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/repository/PostRepository.java @@ -0,0 +1,15 @@ +package com.vladmihalcea.hpjp.spring.data.audit.repository; + +import com.vladmihalcea.hpjp.spring.data.audit.domain.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.history.RevisionRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends JpaRepository, + RevisionRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/service/PostService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/service/PostService.java new file mode 100644 index 000000000..dac6980ac --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/audit/service/PostService.java @@ -0,0 +1,49 @@ +package com.vladmihalcea.hpjp.spring.data.audit.service; + +import com.vladmihalcea.hpjp.spring.data.audit.domain.Post; +import com.vladmihalcea.hpjp.spring.data.audit.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.audit.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.audit.repository.PostRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class PostService { + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @Transactional + public Post savePost(Post post) { + return postRepository.save(post); + } + + @Transactional + public Post savePostAndComments(Post post, PostComment... comments) { + post = postRepository.save(post); + + if(comments.length > 0) { + postCommentRepository.saveAll(Arrays.asList(comments)); + } + + return post; + } + + @Transactional + public void deletePost(Post post) { + postCommentRepository.deleteByPost(post); + postRepository.delete(post); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/SpringDataJPABaseRepositoryTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/SpringDataJPABaseRepositoryTest.java new file mode 100644 index 000000000..0ea2653b7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/SpringDataJPABaseRepositoryTest.java @@ -0,0 +1,44 @@ +package com.vladmihalcea.hpjp.spring.data.base; + +import com.vladmihalcea.hpjp.hibernate.forum.Post; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseRepositoryConfiguration; +import com.vladmihalcea.hpjp.spring.data.base.service.ForumService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPABaseRepositoryConfiguration.class) +public class SpringDataJPABaseRepositoryTest extends AbstractSpringTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class + }; + } + + @Autowired + private ForumService forumService; + + @Test + public void test() { + Long postId = forumService.createPost( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ).getId(); + + Post post = forumService.findById(postId); + assertEquals("High-Performance Java Persistence", post.getTitle()); + + post.setTitle("High-Performance Java Persistence, 2nd edition"); + forumService.updatePost(post); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/config/SpringDataJPABaseConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/config/SpringDataJPABaseConfiguration.java new file mode 100644 index 000000000..dd7f8c168 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/config/SpringDataJPABaseConfiguration.java @@ -0,0 +1,137 @@ +package com.vladmihalcea.hpjp.spring.data.base.config; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import jakarta.persistence.EntityManagerFactory; +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.listener.DataSourceQueryCountListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.Arrays; +import java.util.Collections; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +@EnableTransactionManagement +@EnableAspectJAutoProxy +public abstract class SpringDataJPABaseConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + @Bean + public Database database() { + return Database.POSTGRESQL; + } + + @Bean + public DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + public DataSource poolingDataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setAutoCommit(false); + hikariConfig.setDataSource(dataSourceProvider().dataSource()); + return new HikariDataSource(hikariConfig); + } + + @Bean + public DataSource dataSource() { + ChainListener listener = new ChainListener(); + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + listener.addListener(loggingListener); + listener.addListener(new DataSourceQueryCountListener()); + return ProxyDataSourceBuilder + .create(poolingDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(listener) + .build(); + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory( + @Autowired DataSource dataSource) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + entityManagerFactoryBean.setPackagesToScan(packagesToScan()); + entityManagerFactoryBean.setDataSource(dataSource); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + entityManagerFactoryBean.setJpaProperties(properties()); + return entityManagerFactoryBean; + } + + /*@Bean + public HypersistenceOptimizer hypersistenceOptimizer(EntityManagerFactory entityManagerFactory) { + return new HypersistenceOptimizer( + new JpaConfig( + entityManagerFactory + ) + ); + }*/ + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { + return new TransactionTemplate(transactionManager(entityManagerFactory)); + } + + protected Properties properties() { + Properties properties = new Properties(); + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator(Arrays.asList(PostDTO.class)) + ) + ); + additionalProperties(properties); + return properties; + } + + protected void additionalProperties(Properties properties) { + properties.setProperty(AvailableSettings.STATEMENT_BATCH_SIZE, "10"); + } + + protected String[] packagesToScan() { + return new String[]{ + packageToScan() + }; + } + + protected String packageToScan() { + return "com.vladmihalcea.hpjp.hibernate.forum"; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/config/SpringDataJPABaseRepositoryConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/config/SpringDataJPABaseRepositoryConfiguration.java new file mode 100644 index 000000000..1102a7563 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/config/SpringDataJPABaseRepositoryConfiguration.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.spring.data.base.config; + +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.base.service", + } +) +@EnableJpaRepositories( + value = "com.vladmihalcea.hpjp.spring.data.base.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPABaseRepositoryConfiguration extends SpringDataJPABaseConfiguration { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/repository/PostRepository.java new file mode 100644 index 000000000..eb6d24cf7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/repository/PostRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.base.repository; + +import com.vladmihalcea.hpjp.hibernate.forum.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/service/ForumService.java new file mode 100644 index 000000000..96050f603 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/service/ForumService.java @@ -0,0 +1,15 @@ +package com.vladmihalcea.hpjp.spring.data.base.service; + +import com.vladmihalcea.hpjp.hibernate.forum.Post; + +/** + * @author Vlad Mihalcea + */ +public interface ForumService { + + Post findById(Long id); + + Post createPost(Post post); + + Post updatePost(Post post); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/service/ForumServiceImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/service/ForumServiceImpl.java new file mode 100644 index 000000000..5517bdea8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/base/service/ForumServiceImpl.java @@ -0,0 +1,38 @@ +package com.vladmihalcea.hpjp.spring.data.base.service; + +import com.vladmihalcea.hpjp.hibernate.forum.Post; +import com.vladmihalcea.hpjp.spring.data.base.repository.PostRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumServiceImpl implements ForumService { + + private PostRepository postRepository; + + public ForumServiceImpl(@Autowired PostRepository postRepository) { + this.postRepository = postRepository; + } + + public Post findById(Long id) { + return postRepository.findById(id).orElse(null); + } + + @Transactional + @Override + public Post createPost(Post post) { + return postRepository.persist(post); + } + + @Transactional + @Override + public Post updatePost(Post post) { + postRepository.update(post); + return post; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/SpringDataJPABidirectionalTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/SpringDataJPABidirectionalTest.java new file mode 100644 index 000000000..69eafec6d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/SpringDataJPABidirectionalTest.java @@ -0,0 +1,119 @@ +package com.vladmihalcea.hpjp.spring.data.bidirectional; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.bidirectional.config.SpringDataJPABidirectionalConfiguration; +import com.vladmihalcea.hpjp.spring.data.bidirectional.domain.Post; +import com.vladmihalcea.hpjp.spring.data.bidirectional.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.bidirectional.domain.PostDetails; +import com.vladmihalcea.hpjp.spring.data.bidirectional.domain.Tag; +import com.vladmihalcea.hpjp.spring.data.bidirectional.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.bidirectional.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.data.bidirectional.service.ForumService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.support.TransactionCallback; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPABidirectionalConfiguration.class) +public class SpringDataJPABidirectionalTest extends AbstractSpringTest { + + @Autowired + private ForumService forumService; + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @Override + protected Class[] entities() { + return new Class[] { + PostComment.class, + PostDetails.class, + Post.class, + Tag.class + }; + } + + @Override + public void afterInit() { + postRepository.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setDetails(new PostDetails().setCreatedBy("Vlad Mihalcea")) + .addComment( + new PostComment() + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setReview("A must-read for every Java developer!") + ) + .addTag(new Tag().setName("JDBC")) + .addTag(new Tag().setName("Hibernate")) + .addTag(new Tag().setName("jOOQ")) + ); + } + + @Test + public void testPersistPostCommentWithoutPostFetching() { + LOGGER.info("Persisting PostComment without fetching the Post entity"); + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + postCommentRepository.persist( + new PostComment() + .setPost(postRepository.getReferenceById(1L)) + .setReview("Very informative. Learned a lot, applied every day.") + ); + + return null; + }); + } + + @Test + public void testPersistPostCommentWithPostFetching() { + LOGGER.info("Persisting PostComment when the Post entity was already fetched"); + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Post post = postRepository.findByIdWithDetailsAndComments(1L); + + post.addComment( + new PostComment() + .setReview("Very informative. Learned a lot, applied every day.") + ); + + return null; + }); + } + + @Test + public void testDelete() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + postRepository.deleteById(1L); + + return null; + }); + } + + @Test + public void testSaveAndRemoveChildEntityWithoutParentFetching() { + LOGGER.info("Add PostComment to Post"); + PostComment comment = forumService.addPostComment("Best book on JPA and Hibernate!", 1L); + + LOGGER.info("Remove PostComment from Post"); + forumService.removePostComment(comment.getId()); + } + + @Test + public void testSaveAndRemoveChildEntityAntiPattern() { + LOGGER.info("Add PostComment to Post"); + PostComment comment = forumService.addPostCommentAntiPattern("Best book on JPA and Hibernate!", 1L); + + LOGGER.info("Remove PostComment from Post"); + forumService.removePostCommentAntiPattern(comment.getId()); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/config/SpringDataJPABidirectionalConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/config/SpringDataJPABidirectionalConfiguration.java new file mode 100644 index 000000000..28fab58ed --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/config/SpringDataJPABidirectionalConfiguration.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.data.bidirectional.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.bidirectional.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.bidirectional" + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.bidirectional.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPABidirectionalConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/domain/Post.java new file mode 100644 index 000000000..996cff4a4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/domain/Post.java @@ -0,0 +1,101 @@ +package com.vladmihalcea.hpjp.spring.data.bidirectional.domain; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + @OneToOne( + mappedBy = "post", + fetch = FetchType.LAZY, + cascade = CascadeType.ALL + ) + private PostDetails details; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public Post addTag(Tag tag) { + tags.add(tag); + tag.getPosts().add(this); + return this; + } + + public void removeTag(Tag tag) { + tags.remove(tag); + tag.getPosts().remove(this); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/domain/PostComment.java new file mode 100644 index 000000000..a0a0d373d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/domain/PostComment.java @@ -0,0 +1,47 @@ +package com.vladmihalcea.hpjp.spring.data.bidirectional.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/domain/PostDetails.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/domain/PostDetails.java new file mode 100644 index 000000000..6f95a1571 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/domain/PostDetails.java @@ -0,0 +1,62 @@ +package com.vladmihalcea.hpjp.spring.data.bidirectional.domain; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostDetails") +@Table(name = "post_details") +public class PostDetails { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/domain/Tag.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/domain/Tag.java new file mode 100644 index 000000000..83d29008f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/domain/Tag.java @@ -0,0 +1,47 @@ +package com.vladmihalcea.hpjp.spring.data.bidirectional.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.NaturalId; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Tag") +@Table(name = "tag") +public class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + @ManyToMany(mappedBy = "tags") + private List posts = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + + public List getPosts() { + return posts; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/repository/PostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/repository/PostCommentRepository.java new file mode 100644 index 000000000..d54c2814c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/repository/PostCommentRepository.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.data.bidirectional.repository; + +import com.vladmihalcea.hpjp.spring.data.bidirectional.domain.PostComment; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostCommentRepository extends BaseJpaRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/repository/PostDetailsRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/repository/PostDetailsRepository.java new file mode 100644 index 000000000..da3252468 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/repository/PostDetailsRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.bidirectional.repository; + +import com.vladmihalcea.hpjp.spring.data.bidirectional.domain.PostDetails; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostDetailsRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/repository/PostRepository.java new file mode 100644 index 000000000..818f32783 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/repository/PostRepository.java @@ -0,0 +1,23 @@ +package com.vladmihalcea.hpjp.spring.data.bidirectional.repository; + +import com.vladmihalcea.hpjp.spring.data.bidirectional.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { + + @Query(""" + select p + from Post p + join fetch p.details + join fetch p.comments + where p.id = :id + """) + Post findByIdWithDetailsAndComments(@Param("id") Long id); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/repository/TagRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/repository/TagRepository.java new file mode 100644 index 000000000..89dbaf1b3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/repository/TagRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.bidirectional.repository; + +import com.vladmihalcea.hpjp.spring.data.bidirectional.domain.Tag; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface TagRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/service/ForumService.java new file mode 100644 index 000000000..fcbb58d0c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bidirectional/service/ForumService.java @@ -0,0 +1,56 @@ +package com.vladmihalcea.hpjp.spring.data.bidirectional.service; + +import com.vladmihalcea.hpjp.spring.data.bidirectional.domain.Post; +import com.vladmihalcea.hpjp.spring.data.bidirectional.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.bidirectional.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.bidirectional.repository.PostRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumService { + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @Transactional + public PostComment addPostComment(String review, Long postId) { + PostComment comment = new PostComment() + .setReview(review) + .setPost(postRepository.getReferenceById(postId)); + + postCommentRepository.persist(comment); + return comment; + } + + @Transactional + public void removePostComment(Long id) { + postCommentRepository.deleteById(id); + } + + @Transactional + public PostComment addPostCommentAntiPattern(String review, Long postId) { + Post post = postRepository.findById(postId).orElseThrow(); + + PostComment comment = new PostComment() + .setReview(review) + .setPost(postRepository.getReferenceById(postId)); + + post.addComment(comment); + return comment; + } + + @Transactional + public void removePostCommentAntiPattern(Long id) { + PostComment postComment = postCommentRepository.findById(id).orElseThrow(); + postComment.getPost().removeComment(postComment); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bytecode/AttachmentLazyLoading.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bytecode/AttachmentLazyLoading.java new file mode 100644 index 000000000..3ef699998 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bytecode/AttachmentLazyLoading.java @@ -0,0 +1,100 @@ +package com.vladmihalcea.hpjp.spring.data.bytecode; + +import com.vladmihalcea.hpjp.hibernate.forum.Attachment; +import com.vladmihalcea.hpjp.hibernate.forum.MediaType; +import com.vladmihalcea.hpjp.spring.data.bytecode.repository.AttachmentRepository; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import io.hypersistence.utils.common.ReflectionUtils; +import org.hibernate.LazyInitializationException; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.transaction.support.TransactionTemplate; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import static org.aspectj.bridge.MessageUtil.fail; +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class AttachmentLazyLoading { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + private final TransactionTemplate transactionTemplate; + + private final AttachmentRepository attachmentRepository; + + public AttachmentLazyLoading( + TransactionTemplate transactionTemplate, + AttachmentRepository attachmentRepository) { + this.transactionTemplate = transactionTemplate; + this.attachmentRepository = attachmentRepository; + } + + public void test() throws URISyntaxException { + final String bookFilePath = "ehcache.xml"; + final String videoFilePath = "spy.properties"; + + transactionTemplate.execute(status -> { + attachmentRepository.save( + new Attachment() + .setId(1L) + .setName("High-Performance Java Persistence") + .setMediaType(MediaType.PDF) + .setContent(readBytes(bookFilePath)) + ); + + attachmentRepository.save( + new Attachment() + .setId(2L) + .setName("High-Performance Java Persistence - Mach 2") + .setMediaType(MediaType.MPEG_VIDEO) + .setContent(readBytes(videoFilePath)) + ); + + return null; + }); + + transactionTemplate.execute(status -> { + Attachment book = attachmentRepository.findById(1L).orElseThrow(null); + LOGGER.debug("Fetched book: {}", book.getName()); + assertArrayEquals(readBytes(bookFilePath), book.getContent()); + + Attachment video = attachmentRepository.findById(2L).orElseThrow(null); + LOGGER.debug("Fetched video: {}", video.getName()); + assertArrayEquals(readBytes(videoFilePath), video.getContent()); + + assertNotNull(ReflectionUtils.getFieldValue(book, "$$_hibernate_entityEntryHolder")); + return null; + }); + + Attachment book = transactionTemplate.execute( + status -> attachmentRepository.findById(1L).orElse(null) + ); + + try { + book.getContent(); + + fail("Should throw LazyInitializationException"); + } catch (Exception expected) { + assertTrue(LazyInitializationException.class.isInstance(ExceptionUtil.rootCause(expected))); + } + } + + private byte[] readBytes(String path) { + try { + return Files.readAllBytes( + Paths.get(Thread.currentThread().getContextClassLoader().getResource(path).toURI()) + ); + } catch (IOException|URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bytecode/SpringDataJPARuntimeBytecodeEnhancementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bytecode/SpringDataJPARuntimeBytecodeEnhancementTest.java new file mode 100644 index 000000000..9a2fbdc82 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bytecode/SpringDataJPARuntimeBytecodeEnhancementTest.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.spring.data.bytecode; + +import com.vladmihalcea.hpjp.hibernate.forum.Attachment; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.bytecode.config.SpringDataJPARuntimeBytecodeEnhancementConfiguration; +import com.vladmihalcea.hpjp.spring.data.bytecode.repository.AttachmentRepository; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; + +import java.net.URISyntaxException; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPARuntimeBytecodeEnhancementConfiguration.class) +public class SpringDataJPARuntimeBytecodeEnhancementTest extends AbstractSpringTest { + + @Autowired + private AttachmentRepository attachmentRepository; + + @Override + protected Class[] entities() { + return new Class[] { + Attachment.class + }; + } + + @Test + public void test() throws URISyntaxException { + AttachmentLazyLoading logic = new AttachmentLazyLoading( + transactionTemplate, + attachmentRepository + ); + //Needed in order to delay the Attachment class loging + logic.test(); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bytecode/config/SpringDataJPARuntimeBytecodeEnhancementConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bytecode/config/SpringDataJPARuntimeBytecodeEnhancementConfiguration.java new file mode 100644 index 000000000..d9d6873b5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bytecode/config/SpringDataJPARuntimeBytecodeEnhancementConfiguration.java @@ -0,0 +1,45 @@ +package com.vladmihalcea.hpjp.spring.data.bytecode.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.cfg.BytecodeSettings; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.EnableLoadTimeWeaving; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.bytecode", + } +) +@EnableJpaRepositories( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.bytecode.repository", + } +) +@EnableLoadTimeWeaving +public class SpringDataJPARuntimeBytecodeEnhancementConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return "com.vladmihalcea.hpjp.hibernate.forum"; + } + + public Database database() { + return Database.MYSQL; + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + properties.put(BytecodeSettings.ENHANCER_ENABLE_DIRTY_TRACKING, "false"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bytecode/repository/AttachmentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bytecode/repository/AttachmentRepository.java new file mode 100644 index 000000000..029639c24 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/bytecode/repository/AttachmentRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.bytecode.repository; + +import com.vladmihalcea.hpjp.hibernate.forum.Attachment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface AttachmentRepository extends JpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/SpringDataJPACascadeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/SpringDataJPACascadeTest.java new file mode 100644 index 000000000..69000d8f6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/SpringDataJPACascadeTest.java @@ -0,0 +1,310 @@ +package com.vladmihalcea.hpjp.spring.data.cascade; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.cascade.config.SpringDataJPACascadeConfiguration; +import com.vladmihalcea.hpjp.spring.data.cascade.domain.Post; +import com.vladmihalcea.hpjp.spring.data.cascade.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.cascade.domain.PostDetails; +import com.vladmihalcea.hpjp.spring.data.cascade.domain.Tag; +import com.vladmihalcea.hpjp.spring.data.cascade.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.cascade.repository.PostDetailsRepository; +import com.vladmihalcea.hpjp.spring.data.cascade.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.data.cascade.repository.TagRepository; +import org.hibernate.Session; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.support.TransactionCallback; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPACascadeConfiguration.class) +public class SpringDataJPACascadeTest extends AbstractSpringTest { + + @Autowired + private TagRepository tagRepository; + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @Autowired + private PostDetailsRepository postDetailsRepository; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + PostDetails.class, + Post.class, + Tag.class + }; + } + + @Test + public void testSavePostAndComments() { + postRepository.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setReview("A must-read for every Java developer!") + ) + ); + + transactionTemplate.execute(transactionStatus -> { + Post post = postRepository.findByIdWithComments(1L); + postRepository.delete(post); + + return null; + }); + } + + @Test + public void testSavePostWithDetailsAndComments() { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setDetails( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + ) + .addComment( + new PostComment() + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setReview("A must-read for every Java developer!") + ); + + List postCommentIds = transactionTemplate.execute(transactionStatus -> { + postRepository.persist(post); + return post.getComments().stream().map(PostComment::getId).toList(); + }); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Long minId = Collections.min(postCommentIds); + Long maxId = Collections.max(postCommentIds); + + List postComments = postCommentRepository.findAllWithPostAndDetailsByIds( + minId, + maxId + ); + + assertEquals(postCommentIds.size(), postComments.size()); + return null; + }); + } + + @Test + public void testSavePostAndPostDetails() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setDetails( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + ); + + postRepository.persist(post); + return null; + }); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Post post = postRepository.getReferenceById(1L); + + PostDetails postDetails = postDetailsRepository.findById(post.getId()).orElseThrow(); + assertEquals("Vlad Mihalcea", postDetails.getCreatedBy()); + + return null; + }); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Post post = postRepository.findById(1L).orElseThrow(); + + return null; + }); + } + + @Test + public void testSavePostAndTags() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + tagRepository.persist(new Tag().setName("JPA")); + tagRepository.persist(new Tag().setName("Hibernate")); + + return null; + }); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Session session = entityManager.unwrap(Session.class); + + postRepository.persist( + new Post() + .setId(1L) + .setTitle("JPA with Hibernate") + .addTag(session.bySimpleNaturalId(Tag.class).getReference("JPA")) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + ); + + postRepository.persist( + new Post() + .setId(2L) + .addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate")) + ); + + return null; + }); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Session session = entityManager.unwrap(Session.class); + + Post post = entityManager.createQuery(""" + select p + from Post p + join fetch p.tags + where p.id = :id + """, Post.class) + .setParameter("id", 1L) + .getSingleResult(); + + post.getTags().remove(session.bySimpleNaturalId(Tag.class).getReference("JPA")); + + return null; + }); + } + + @Test + public void testBatchingPersistPostAndComments() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + for (long i = 1; i <= 3; i++) { + postRepository.persist( + new Post() + .setId(i) + .setTitle(String.format("Post no. %d", i)) + .addComment(new PostComment().setReview("Good")) + ); + } + return null; + }); + } + + @Test + public void testBatchingUpdatePost() { + testBatchingPersistPostAndComments(); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + List posts = postRepository.findAllByTitleLike("Post no.%"); + + posts.forEach(post -> post.setTitle(post.getTitle().replaceAll("no", "nr"))); + return null; + }); + } + + @Test + public void testBatchingUpdatePostAndComments() { + testBatchingPersistPostAndComments(); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + List comments = postCommentRepository.findAllWithPostTitleLike("Post no.%"); + + comments.forEach(c -> { + c.setReview(c.getReview().replaceAll("Good", "Very good")); + Post post = c.getPost(); + post.setTitle(post.getTitle().replaceAll("no", "nr")); + }); + return null; + }); + } + + @Test + public void testBatchingDeletePost() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + for (long i = 1; i <= 3; i++) { + postRepository.persist( + new Post() + .setId(i) + .setTitle(String.format("Post no. %d", i)) + ); + } + return null; + }); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + List posts = postRepository.findAllByTitleLike("Post no.%"); + + posts.forEach(post -> postRepository.delete(post)); + return null; + }); + } + + @Test + public void testBatchingDeletePostAndComments() { + testBatchingPersistPostAndComments(); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + List posts = postRepository.findAllByTitleLike("Post no.%"); + + posts.forEach(postRepository::delete); + return null; + }); + } + + @Test + public void testBatchingDeletePostAndCommentsManualOrdering() { + testBatchingPersistPostAndComments(); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + List posts = postRepository.findAllByTitleLike("Post no.%"); + + posts.forEach(post -> { + Iterator it = post.getComments().iterator(); + while (it.hasNext()) { + it.next().setPost(null); + it.remove(); + } + }); + + entityManager.flush(); + + posts.forEach(postRepository::delete); + + return null; + }); + } + + @Test + @Ignore(""" + Requires the comments collection to use + @OneToMany(mappedBy = \"post\", cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + """) + public void testBatchingDeletePostAndCommentsBulkDelete() { + testBatchingPersistPostAndComments(); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + List posts = postRepository.findAllByTitleLike("Post no.%"); + + postCommentRepository.deleteAllByPost(posts); + posts.forEach(postRepository::delete); + + return null; + }); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/config/SpringDataJPACascadeConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/config/SpringDataJPACascadeConfiguration.java new file mode 100644 index 000000000..9fbe67f35 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/config/SpringDataJPACascadeConfiguration.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.data.cascade.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.cascade.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.cascade" + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.cascade.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPACascadeConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/domain/Post.java new file mode 100644 index 000000000..e1071b2b9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/domain/Post.java @@ -0,0 +1,95 @@ +package com.vladmihalcea.hpjp.spring.data.cascade.domain; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + @OneToOne( + mappedBy = "post", + fetch = FetchType.LAZY, + cascade = CascadeType.ALL + ) + private PostDetails details; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public Post addTag(Tag tag) { + tags.add(tag); + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/domain/PostComment.java new file mode 100644 index 000000000..7970358e8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/domain/PostComment.java @@ -0,0 +1,102 @@ +package com.vladmihalcea.hpjp.spring.data.cascade.domain; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + private PostComment parent; + + private String review; + + @Enumerated(EnumType.ORDINAL) + private Status status; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + private int votes; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public PostComment getParent() { + return parent; + } + + public PostComment setParent(PostComment parent) { + this.parent = parent; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Status getStatus() { + return status; + } + + public PostComment setStatus(Status status) { + this.status = status; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public int getVotes() { + return votes; + } + + public PostComment setVotes(int votes) { + this.votes = votes; + return this; + } + + public enum Status { + PENDING, + APPROVED, + SPAM; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/domain/PostDetails.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/domain/PostDetails.java new file mode 100644 index 000000000..de08e4d61 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/domain/PostDetails.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.spring.data.cascade.domain; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostDetails") +@Table(name = "post_details") +public class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "id") + private Post post; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/domain/Tag.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/domain/Tag.java new file mode 100644 index 000000000..ca800b7c7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/domain/Tag.java @@ -0,0 +1,40 @@ +package com.vladmihalcea.hpjp.spring.data.cascade.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.NaturalId; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Tag") +@Table(name = "tag") +public class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/repository/PostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/repository/PostCommentRepository.java new file mode 100644 index 000000000..29cda3ea7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/repository/PostCommentRepository.java @@ -0,0 +1,46 @@ +package com.vladmihalcea.hpjp.spring.data.cascade.repository; + +import com.vladmihalcea.hpjp.spring.data.cascade.domain.Post; +import com.vladmihalcea.hpjp.spring.data.cascade.domain.PostComment; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostCommentRepository extends BaseJpaRepository { + + @Query(""" + select pc + from PostComment pc + join fetch pc.post p + join fetch p.comments + where p.title like :titlePrefix + """) + List findAllWithPostTitleLike(@Param("titlePrefix") String titlePrefix); + + @Query(""" + select pc + from PostComment pc + join fetch pc.post p + join fetch p.details d + where pc.id between :minId and :maxId + """) + List findAllWithPostAndDetailsByIds( + @Param("minId") Long minId, + @Param("maxId") Long maxId + ); + + @Modifying + @Query(""" + delete from PostComment c + where c.post in :posts + """) + void deleteAllByPost(@Param("posts") List posts); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/repository/PostDetailsRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/repository/PostDetailsRepository.java new file mode 100644 index 000000000..347498bdd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/repository/PostDetailsRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.cascade.repository; + +import com.vladmihalcea.hpjp.spring.data.cascade.domain.PostDetails; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostDetailsRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/repository/PostRepository.java new file mode 100644 index 000000000..ec92ae4ec --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/repository/PostRepository.java @@ -0,0 +1,34 @@ +package com.vladmihalcea.hpjp.spring.data.cascade.repository; + +import com.vladmihalcea.hpjp.spring.data.cascade.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { + + @Query(""" + select p + from Post p + left join fetch p.details + left join fetch p.comments + where p.title like :titlePrefix + """) + List findAllByTitleLike(@Param("titlePrefix") String titlePrefix); + + @Query(""" + select p + from Post p + left join fetch p.details + left join fetch p.comments + where p.id = :id + """) + Post findByIdWithComments(@Param("id") Long id); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/repository/TagRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/repository/TagRepository.java new file mode 100644 index 000000000..8ad562325 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/cascade/repository/TagRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.cascade.repository; + +import com.vladmihalcea.hpjp.spring.data.cascade.domain.Tag; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface TagRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/SpringDataJPACrudTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/SpringDataJPACrudTest.java new file mode 100644 index 000000000..d8e12075b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/SpringDataJPACrudTest.java @@ -0,0 +1,263 @@ +package com.vladmihalcea.hpjp.spring.data.crud; + +import com.vladmihalcea.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.crud.config.SpringDataJPACrudConfiguration; +import com.vladmihalcea.hpjp.spring.data.crud.domain.Post; +import com.vladmihalcea.hpjp.spring.data.crud.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.crud.domain.PostStatus; +import com.vladmihalcea.hpjp.spring.data.crud.repository.DefaultPostRepository; +import com.vladmihalcea.hpjp.spring.data.crud.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.crud.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.data.crud.service.PostService; +import org.hibernate.SessionFactory; +import org.hibernate.StatelessSession; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.support.TransactionCallback; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPACrudConfiguration.class) +public class SpringDataJPACrudTest extends AbstractSpringTest { + + @Autowired + private PostRepository postRepository; + + @Autowired + private DefaultPostRepository defaultPostRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @Autowired + private PostService postService; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class + }; + } + + @Test + public void testPersistAndMerge() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + postRepository.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setSlug("high-performance-java-persistence") + ); + + postRepository.persistAndFlush( + new Post() + .setId(2L) + .setTitle("Hypersistence Optimizer") + .setSlug("hypersistence-optimizer") + ); + + postRepository.persistAllAndFlush( + LongStream.range(3, 1000) + .mapToObj(i -> new Post() + .setId(i) + .setTitle(String.format("Post %d", i)) + .setSlug(String.format("post-%d", i)) + ) + .collect(Collectors.toList()) + ); + + return null; + }); + + List posts = transactionTemplate.execute(transactionStatus -> + entityManager.createQuery(""" + select p + from Post p + where p.id < 10 + """, Post.class) + .getResultList() + ); + + posts.forEach(post -> post.setTitle(post.getTitle() + " rocks!")); + + transactionTemplate.execute(transactionStatus -> + postRepository.updateAll(posts) + ); + } + + @Test + public void testSave() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + postRepository.save( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setSlug("high-performance-java-persistence") + ); + return null; + }); + + fail("Should throw UnsupportedOperationException!"); + } catch (UnsupportedOperationException expected) { + LOGGER.warn("You shouldn't call the JpaRepository save method!"); + } + } + + @Test + public void testSaveWithFindById() { + Long postId = transactionTemplate.execute(transactionStatus -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setSlug("high-performance-java-persistence"); + + postRepository.persist(post); + + return post.getId(); + }); + + LOGGER.info("Save PostComment"); + SQLStatementCountValidator.reset(); + postService.addNewPostComment("Best book on JPA and Hibernate!", postId); + + //The sequence call + SQLStatementCountValidator.assertSelectCount(1); + SQLStatementCountValidator.assertInsertCount(1); + + PostComment comment = postCommentRepository.findById(1L).orElseThrow(); + assertNotNull(comment); + } + + @Test + public void testSaveWithFindByIdRepeatableRead() { + Long postId = transactionTemplate.execute(transactionStatus -> { + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setSlug("high-performance-java-persistence"); + + postRepository.persist(post); + + return post.getId(); + }); + + LOGGER.info("Save PostComment"); + + try { + postService.addNewPostCommentRaceCondition("Best book on JPA and Hibernate!", postId); + } catch (DataIntegrityViolationException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void testBatch() { + SQLStatementCountValidator.reset(); + + transactionTemplate.execute(transactionStatus -> { + for (long i = 1; i <= 10; i++) { + postRepository.persist( + new Post() + .setId(i) + .setTitle("High-Performance Java Persistence") + .setSlug( + String.format( + "high-performance-java-persistence-%d", + i + ) + ) + ); + } + + return null; + }); + SQLStatementCountValidator.assertInsertCount(1); + List posts = postRepository.findAllById(List.of(1L, 2L, 3L)); + assertSame(3, posts.size()); + posts = postRepository.findAllById(List.of(1L, 2L, 3L, 4L)); + assertSame(4, posts.size()); + } + + @Test + public void testSaveSpam() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + postRepository.persist( + new Post() + .setId(1L) + .setTitle("Check out my website") + .setSlug("spam") + .setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION) + ); + return null; + }); + } + + @Test + public void testDeleteProxy() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + defaultPostRepository.save( + new Post() + .setId(1L) + .setTitle("Check out my website") + .setSlug("spam") + .setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION) + ); + return null; + }); + + LOGGER.info("Delete Post"); + + SQLStatementCountValidator.reset(); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Post post = defaultPostRepository.getReferenceById(1L); + defaultPostRepository.delete(post); + return null; + }); + SQLStatementCountValidator.assertSelectCount(1); + SQLStatementCountValidator.assertDeleteCount(1); + } + + @Test + public void testDeleteWithoutSelect() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + defaultPostRepository.save( + new Post() + .setId(1L) + .setTitle("Check out my website") + .setSlug("spam") + .setStatus(PostStatus.REQUIRES_MODERATOR_INTERVENTION) + ); + return null; + }); + + LOGGER.info("Delete Post"); + + SQLStatementCountValidator.reset(); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + try(StatelessSession session = entityManager.getEntityManagerFactory().unwrap(SessionFactory.class) + .withStatelessOptions() + .openStatelessSession()) { + Post post = new Post().setId(1L); + session.delete(post); + } + return null; + }); + SQLStatementCountValidator.assertSelectCount(0); + SQLStatementCountValidator.assertDeleteCount(1); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/config/SpringDataJPACrudConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/config/SpringDataJPACrudConfiguration.java new file mode 100644 index 000000000..ee7a1c16f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/config/SpringDataJPACrudConfiguration.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.spring.data.crud.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.crud.domain.Post; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.crud", + } +) +@EnableJpaRepositories( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.crud.service", + "com.vladmihalcea.hpjp.spring.data.crud.repository", + "io.hypersistence.utils.spring.repository" + } +) +public class SpringDataJPACrudConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/domain/Post.java new file mode 100644 index 000000000..a9de86073 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/domain/Post.java @@ -0,0 +1,66 @@ +package com.vladmihalcea.hpjp.spring.data.crud.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.NaturalId; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table( + name = "post", + uniqueConstraints = @UniqueConstraint( + name = "UK_POST_SLUG", + columnNames = "slug" + ) +) +public class Post { + + @Id + private Long id; + + private String title; + + @NaturalId + private String slug; + + @Enumerated(EnumType.ORDINAL) + @Column(columnDefinition = "NUMERIC(2)") + private PostStatus status; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public String getSlug() { + return slug; + } + + public Post setSlug(String slug) { + this.slug = slug; + return this; + } + + public PostStatus getStatus() { + return status; + } + + public Post setStatus(PostStatus status) { + this.status = status; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/domain/PostComment.java new file mode 100644 index 000000000..c76a154a5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/domain/PostComment.java @@ -0,0 +1,48 @@ +package com.vladmihalcea.hpjp.spring.data.crud.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post_comment") +public class PostComment { + + @Id + @GeneratedValue + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(foreignKey = @ForeignKey(name = "FK_POST_COMMENT_POST_ID")) + private Post post; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/domain/PostStatus.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/domain/PostStatus.java new file mode 100644 index 000000000..be8fbfec8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/domain/PostStatus.java @@ -0,0 +1,11 @@ +package com.vladmihalcea.hpjp.spring.data.crud.domain; + +/** + * @author Vlad Mihalcea + */ +public enum PostStatus { + PENDING, + APPROVED, + SPAM, + REQUIRES_MODERATOR_INTERVENTION +} \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/repository/DefaultPostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/repository/DefaultPostRepository.java new file mode 100644 index 000000000..c59797471 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/repository/DefaultPostRepository.java @@ -0,0 +1,14 @@ +package com.vladmihalcea.hpjp.spring.data.crud.repository; + +import com.vladmihalcea.hpjp.spring.data.crud.domain.Post; +import io.hypersistence.utils.spring.repository.HibernateRepository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface DefaultPostRepository extends JpaRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/repository/PostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/repository/PostCommentRepository.java new file mode 100644 index 000000000..bf250029b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/repository/PostCommentRepository.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.data.crud.repository; + +import com.vladmihalcea.hpjp.spring.data.crud.domain.PostComment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostCommentRepository extends JpaRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/repository/PostRepository.java new file mode 100644 index 000000000..b1837025f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/repository/PostRepository.java @@ -0,0 +1,14 @@ +package com.vladmihalcea.hpjp.spring.data.crud.repository; + +import com.vladmihalcea.hpjp.spring.data.crud.domain.Post; +import io.hypersistence.utils.spring.repository.HibernateRepository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends HibernateRepository, JpaRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/service/PostService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/service/PostService.java new file mode 100644 index 000000000..20edbb23e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/crud/service/PostService.java @@ -0,0 +1,99 @@ +package com.vladmihalcea.hpjp.spring.data.crud.service; + +import com.vladmihalcea.hpjp.spring.data.crud.domain.Post; +import com.vladmihalcea.hpjp.spring.data.crud.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.crud.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.crud.repository.PostRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityNotFoundException; +import jakarta.persistence.PersistenceContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class PostService { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private TransactionTemplate transactionTemplate; + + private final ExecutorService executorService = Executors.newSingleThreadExecutor(r -> { + Thread bob = new Thread(r); + bob.setName("Bob"); + return bob; + }); + + @Transactional + public PostComment addNewPostComment(String review, Long postId) { + /*PostComment comment = new PostComment() + .setReview(review) + .setPost(postRepository.findById(postId).orElseThrow( + ()-> new EntityNotFoundException( + String.format("Post with id [%d] was not found!", postId) + ) + ));*/ + + PostComment comment = new PostComment() + .setReview(review) + .setPost(postRepository.getReferenceById(postId)); + + postCommentRepository.save(comment); + + return comment; + } + + @Transactional + public PostComment addNewPostCommentRaceCondition(String review, Long postId) { + Post post = postRepository.findById(postId).orElseThrow( + ()-> new EntityNotFoundException( + String.format("Post with id [%d] was not found!", postId) + ) + ); + + try { + Integer updateCount = executorService.submit(() -> transactionTemplate.execute(status -> + entityManager.createQuery(""" + delete from Post + where id = :id + """) + .setParameter("id", postId) + .executeUpdate()) + ).get(); + + assertEquals(1, updateCount.intValue()); + } catch (Exception e) { + throw new IllegalStateException(e); + } + + PostComment comment = new PostComment() + .setReview(review) + .setPost(post); + + postCommentRepository.save(comment); + + return comment; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/SpringDataJPACustomRepositoryTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/SpringDataJPACustomRepositoryTest.java new file mode 100644 index 000000000..4ce5a663d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/SpringDataJPACustomRepositoryTest.java @@ -0,0 +1,272 @@ +package com.vladmihalcea.hpjp.spring.data.custom; + +import com.vladmihalcea.hpjp.hibernate.forum.Post; +import com.vladmihalcea.hpjp.hibernate.forum.PostComment; +import com.vladmihalcea.hpjp.hibernate.forum.Tag; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.PostDTO; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.custom.config.SpringDataJPACustomRepositoryConfiguration; +import com.vladmihalcea.hpjp.spring.data.custom.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.data.custom.service.ForumService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPACustomRepositoryConfiguration.class) +public class SpringDataJPACustomRepositoryTest extends AbstractSpringTest { + + @Autowired + private ForumService forumService; + + @Autowired + private PostRepository postRepository; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class, + Tag.class + }; + } + + @Test + public void testResultTransfromer() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("A must-read for every Java developer!") + ) + ); + + entityManager.persist( + new Post() + .setId(2L) + .setTitle("Hypersistence Optimizer") + .addComment( + new PostComment() + .setId(3L) + .setReview("It's like pair programming with Vlad!") + ) + ); + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + + List postDTOs = forumService.findPostDTOWithComments(); + + assertEquals(2, postDTOs.size()); + assertEquals(2, postDTOs.get(0).getComments().size()); + assertEquals(1, postDTOs.get(1).getComments().size()); + + PostDTO post1DTO = postDTOs.get(0); + + assertEquals(1L, post1DTO.getId().longValue()); + assertEquals(2, post1DTO.getComments().size()); + assertEquals(1L, post1DTO.getComments().get(0).getId().longValue()); + assertEquals(2L, post1DTO.getComments().get(1).getId().longValue()); + + PostDTO post2DTO = postDTOs.get(1); + + assertEquals(2L, post2DTO.getId().longValue()); + assertEquals(1, post2DTO.getComments().size()); + assertEquals(3L, post2DTO.getComments().get(0).getId().longValue()); + } + + @Test + public void testSaveAntiPattern() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("A must-read for every Java developer!") + ) + ); + + entityManager.persist( + new Post() + .setId(2L) + .setTitle("Hypersistence Optimizer") + .addComment( + new PostComment() + .setId(3L) + .setReview("It's like pair programming with Vlad!") + ) + ); + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + + forumService.saveAntiPattern(1L, "Hack!"); + } + + @Test + public void testFindAllAntiPattern() { + int POST_SIZE = 50; + + List tags = List.of( + new Tag() + .setId(1L) + .setName("JDBC"), + new Tag() + .setId(2L) + .setName("JPA"), + new Tag() + .setId(3L) + .setName("Hibernate") + ); + + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + + tags.forEach(tag -> entityManager.persist(tag)); + + for (long i = 1; i <= POST_SIZE; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle( + String.format( + "High-Performance Java Persistence, Part %d", + i + ) + ) + .addTag(tags.get((int) i % 3)) + ); + } + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + List matchingTags = List.of("JPA", "Hibernate"); + LOGGER.info("Fetch post titles using a single query"); + List postTitleQueryRecords = postRepository.findPostTitleByTags(matchingTags); + assertEquals( + BigDecimal.valueOf(POST_SIZE) + .multiply(BigDecimal.valueOf(matchingTags.size())) + .divide(BigDecimal.valueOf(tags.size()), RoundingMode.CEILING) + .intValue(), + postTitleQueryRecords.size() + ); + + LOGGER.info("Fetch post titles using a tons of queries"); + + //The Spring Data findAll Anti-Pattern + List postTitlesStreamRecords = postRepository.findAll() + .stream() + .filter( + post -> post.getTags() + .stream() + .map(Tag::getName) + .anyMatch(matchingTags::contains) + ) + .sorted(Comparator.comparing(Post::getId)) + .map(Post::getTitle) + .collect(Collectors.toList()); + + assertEquals(postTitleQueryRecords, postTitlesStreamRecords); + + return null; + }); + } + + @Test + public void testUpdate() { + Post post = forumService.createPost( + 1L, + "High-Performance Java Persistence" + ); + + forumService.updatePostTitle( + 1L, + "High-Performance Java Persistence 2nd edition" + ); + + assertEquals( + "High-Performance Java Persistence 2nd edition", + forumService.findById(1L).getTitle() + ); + } + + @Test + public void testDeleteAll() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .addComment( + new PostComment() + .setId(1L) + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setId(2L) + .setReview("A must-read for every Java developer!") + ) + ); + + entityManager.persist( + new Post() + .setId(2L) + .setTitle("Hypersistence Optimizer") + .addComment( + new PostComment() + .setId(3L) + .setReview("It's like pair programming with Vlad!") + ) + ); + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + + forumService.deleteAll(); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/config/SpringDataJPACustomRepositoryConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/config/SpringDataJPACustomRepositoryConfiguration.java new file mode 100644 index 000000000..3b63e68be --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/config/SpringDataJPACustomRepositoryConfiguration.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.spring.data.custom.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.custom.service", + } +) +@EnableJpaRepositories( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.custom.repository", + "io.hypersistence.utils.spring.repository" + } +) +public class SpringDataJPACustomRepositoryConfiguration extends SpringDataJPABaseConfiguration { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/repository/CustomPostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/repository/CustomPostRepository.java new file mode 100644 index 000000000..4311c70a5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/repository/CustomPostRepository.java @@ -0,0 +1,18 @@ +package com.vladmihalcea.hpjp.spring.data.custom.repository; + +import com.vladmihalcea.hpjp.hibernate.forum.Post; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.PostDTO; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface CustomPostRepository { + + List findPostDTOWithComments(); + + List findPostTitleByTags(List tags); + + void deleteAll(List posts); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/repository/CustomPostRepositoryImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/repository/CustomPostRepositoryImpl.java new file mode 100644 index 000000000..e1fad9e31 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/repository/CustomPostRepositoryImpl.java @@ -0,0 +1,67 @@ +package com.vladmihalcea.hpjp.spring.data.custom.repository; + +import com.vladmihalcea.hpjp.hibernate.forum.Post; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.DistinctListTransformer; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.PostDTO; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.PostDTOResultTransformer; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class CustomPostRepositoryImpl implements CustomPostRepository { + + @PersistenceContext + private EntityManager entityManager; + + @Override + public List findPostDTOWithComments() { + return entityManager.createNativeQuery(""" + SELECT p.id AS p_id, + p.title AS p_title, + pc.id AS pc_id, + pc.review AS pc_review + FROM post p + JOIN post_comment pc ON p.id = pc.post_id + ORDER BY pc.id + """) + .unwrap(org.hibernate.query.Query.class) + .setTupleTransformer(new PostDTOResultTransformer()) + .setResultListTransformer(DistinctListTransformer.INSTANCE) + .getResultList(); + } + + @Override + public List findPostTitleByTags(List tags) { + return entityManager.createNativeQuery(""" + select p.title + from post p + where exists ( + select 1 + from post_tag pt + join tag t on pt.tag_id = t.id and pt.post_id = p.id + where t.name in (:tags) + ) + order by p.id + """) + .setParameter("tags", tags) + .getResultList(); + } + + @Override + public void deleteAll(List posts) { + entityManager.createQuery(""" + delete from PostComment c + where c.post in :posts + """) + .setParameter("posts", posts) + .executeUpdate(); + + for(Post post : posts) { + entityManager.remove(post); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/repository/PostRepository.java new file mode 100644 index 000000000..c04755a44 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/repository/PostRepository.java @@ -0,0 +1,14 @@ +package com.vladmihalcea.hpjp.spring.data.custom.repository; + +import com.vladmihalcea.hpjp.hibernate.forum.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends + //HibernateRepository, + JpaRepository, CustomPostRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/service/ForumService.java new file mode 100644 index 000000000..7854a3f10 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/custom/service/ForumService.java @@ -0,0 +1,89 @@ +package com.vladmihalcea.hpjp.spring.data.custom.service; + +import com.vladmihalcea.hpjp.hibernate.forum.Post; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.PostDTO; +import com.vladmihalcea.hpjp.spring.data.custom.repository.PostRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumService { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + private PostRepository postRepository; + + @PersistenceContext + private EntityManager entityManager; + + public ForumService(@Autowired PostRepository postRepository) { + this.postRepository = postRepository; + } + + public Post findById(Long id) { + return postRepository.findById(id).orElse(null); + } + + public List findPostDTOWithComments() { + return postRepository.findPostDTOWithComments(); + } + + @Transactional + public void saveAntiPattern(Long postId, String postTitle) { + Post post = postRepository.findById(postId).orElseThrow(); + + post.setTitle(postTitle); + + long startNanos = System.nanoTime(); + postRepository.save(post); + LOGGER.info("Save took: [{}] ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); + } + + @Transactional + public Post createPost(Long id, String title) { + return postRepository.save( + new Post() + .setId(id) + .setTitle(title) + ); + } + + @Transactional + public void updatePostTitle(Long id, String title) { + Post post = findById(id); + post.setTitle(title); + } + + @Transactional + public void deleteAll() { + LOGGER.info("Deleting all posts"); + entityManager.createQuery(""" + select p + from Post p + join fetch p.details + join fetch p.comments + """, Post.class) + .getResultList(); + + List posts = entityManager.createQuery(""" + select p + from Post p + join fetch p.tags + """, Post.class) + .getResultList(); + + postRepository.deleteAll(posts); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/SpringDataJPADTO2EntityTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/SpringDataJPADTO2EntityTest.java new file mode 100644 index 000000000..790731704 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/SpringDataJPADTO2EntityTest.java @@ -0,0 +1,91 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.dto2entity.config.SpringDataJPADTO2EntityConfiguration; +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.Post; +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.PostDetails; +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.Tag; +import com.vladmihalcea.hpjp.spring.data.dto2entity.dto.PostCommentDTO; +import com.vladmihalcea.hpjp.spring.data.dto2entity.dto.PostDTO; +import com.vladmihalcea.hpjp.spring.data.dto2entity.dto.TagDTO; +import com.vladmihalcea.hpjp.spring.data.dto2entity.dto.converter.PostDTOConverter; +import com.vladmihalcea.hpjp.spring.data.dto2entity.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.dto2entity.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.data.dto2entity.service.ForumFacadeService; +import com.vladmihalcea.hpjp.spring.data.dto2entity.service.ForumService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPADTO2EntityConfiguration.class) +public class SpringDataJPADTO2EntityTest extends AbstractSpringTest { + + @Autowired + private ForumService forumService; + + @Autowired + private ForumFacadeService forumFacadeService; + + @Autowired + private PostRepository postRepository; + + @Override + protected Class[] entities() { + return new Class[] { + PostComment.class, + PostDetails.class, + Post.class, + Tag.class + }; + } + + @Override + public void afterInit() { + postRepository.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setDetails(new PostDetails().setCreatedBy("Vlad Mihalcea")) + .addComment( + new PostComment() + .setReview("Best book on JPA and Hibernate!") + ) + .addComment( + new PostComment() + .setReview("A must-read for every Java developer!") + ) + .addTag(new Tag().setName("JDBC")) + .addTag(new Tag().setName("JPA")) + .addTag(new Tag().setName("Hibernate")) + .addTag(new Tag().setName("jOOQ")) + ); + } + + @Test + public void testDtoToEntitySync() { + LOGGER.info("Sync DTOs and JPA entity state"); + + Post post = forumService.findWithDetailsAndCommentsAndTagsById(1L); + + PostDTO postDTO = PostDTOConverter.of(post); + + postDTO.setTitle("High-Performance Java Persistence"); + postDTO.getDetails().setCreatedOn(LocalDateTime.now()); + postDTO.getComments().remove(0); + postDTO.getComments().add( + new PostCommentDTO() + .setReview("A great reference book") + ); + postDTO.getTags().remove(postDTO.getTags().stream().filter(t -> t.getName().equals("JPA")).findFirst().orElseThrow()); + postDTO.getTags().add(new TagDTO().setName("Jakarta Persistence")); + + forumFacadeService.updatePost(postDTO); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/config/SpringDataJPADTO2EntityConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/config/SpringDataJPADTO2EntityConfiguration.java new file mode 100644 index 000000000..c77e06fb8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/config/SpringDataJPADTO2EntityConfiguration.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.dto2entity" + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.dto2entity.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPADTO2EntityConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/domain/Post.java new file mode 100644 index 000000000..5881b0a85 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/domain/Post.java @@ -0,0 +1,131 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.domain; + +import com.vladmihalcea.hpjp.hibernate.association.BidirectionalOneToManyMergeTest; +import jakarta.persistence.*; +import org.hibernate.annotations.DynamicUpdate; + +import java.util.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +@DynamicUpdate +public class Post { + + @Id + private Long id; + + private String title; + + private int rating; + + @OneToOne( + mappedBy = "post", + fetch = FetchType.LAZY, + cascade = CascadeType.ALL + ) + private PostDetails details; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private Set tags = new HashSet<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public int getRating() { + return rating; + } + + public Post setRating(int rating) { + this.rating = rating; + return this; + } + + public PostDetails getDetails() { + return details; + } + + public Post setDetails(PostDetails details) { + if (details == null) { + if (this.details != null) { + this.details.setPost(null); + } + } + else { + details.setPost(this); + } + this.details = details; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } + + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public Post addTag(Tag tag) { + tags.add(tag); + tag.getPosts().add(this); + return this; + } + + public void removeTag(Tag tag) { + tags.remove(tag); + tag.getPosts().remove(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Post)) return false; + return id != null && id.equals(((Post) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/domain/PostComment.java new file mode 100644 index 000000000..74ceffbfa --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/domain/PostComment.java @@ -0,0 +1,71 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.DynamicUpdate; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +@DynamicUpdate +public class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + private int rating; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public int getRating() { + return rating; + } + + public void setRating(int rating) { + this.rating = rating; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostComment)) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/domain/PostDetails.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/domain/PostDetails.java new file mode 100644 index 000000000..763938345 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/domain/PostDetails.java @@ -0,0 +1,100 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.DynamicUpdate; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostDetails") +@Table(name = "post_details") +@DynamicUpdate +public class PostDetails { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_on") + private LocalDateTime updatedOn; + + @Column(name = "updated_by") + private String updatedBy; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public PostDetails setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + return this; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public PostDetails setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostDetails)) return false; + return id != null && id.equals(((PostDetails) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/domain/Tag.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/domain/Tag.java new file mode 100644 index 000000000..2cd26b64d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/domain/Tag.java @@ -0,0 +1,73 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.NaturalId; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Tag") +@Table(name = "tag") +@DynamicUpdate +public class Tag { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + private String name; + + private String description; + + @ManyToMany(mappedBy = "tags") + private List posts = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } + + public List getPosts() { + return posts; + } + + public String getDescription() { + return description; + } + + public Tag setDescription(String description) { + this.description = description; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Tag tag)) return false; + return Objects.equals(getName(), tag.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getName()); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/PostCommentDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/PostCommentDTO.java new file mode 100644 index 000000000..e6fb3b3bc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/PostCommentDTO.java @@ -0,0 +1,40 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.dto; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentDTO { + private Long id; + + private String review; + + public Long getId() { + return id; + } + + public PostCommentDTO setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostCommentDTO setReview(String review) { + this.review = review; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostCommentDTO)) return false; + return id != null && id.equals(((PostCommentDTO) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/PostDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/PostDTO.java new file mode 100644 index 000000000..415ad640e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/PostDTO.java @@ -0,0 +1,76 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.dto; + +import java.util.List; +import java.util.Set; + +/** + * @author Vlad Mihalcea + */ +public class PostDTO { + private Long id; + + private String title; + + private PostDetailsDTO details; + + private List comments; + + private Set tags; + + public Long getId() { + return id; + } + + public PostDTO setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public PostDTO setTitle(String title) { + this.title = title; + return this; + } + + public PostDetailsDTO getDetails() { + return details; + } + + public PostDTO setDetails(PostDetailsDTO details) { + this.details = details; + return this; + } + + public List getComments() { + return comments; + } + + public PostDTO setComments(List comments) { + this.comments = comments; + return this; + } + + public Set getTags() { + return tags; + } + + public PostDTO setTags(Set tags) { + this.tags = tags; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostDTO)) return false; + return id != null && id.equals(((PostDTO) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/PostDetailsDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/PostDetailsDTO.java new file mode 100644 index 000000000..e69e9d409 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/PostDetailsDTO.java @@ -0,0 +1,53 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.dto; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +public class PostDetailsDTO { + private Long id; + + private LocalDateTime createdOn; + + private String createdBy; + + public Long getId() { + return id; + } + + public PostDetailsDTO setId(Long id) { + this.id = id; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public PostDetailsDTO setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetailsDTO setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostDetailsDTO)) return false; + return id != null && id.equals(((PostDetailsDTO) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/TagDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/TagDTO.java new file mode 100644 index 000000000..0a67bf2c1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/TagDTO.java @@ -0,0 +1,43 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.dto; + +import java.util.Objects; + +/** + * @author Vlad Mihalcea + */ +public class TagDTO { + + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public TagDTO setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public TagDTO setName(String name) { + this.name = name; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TagDTO tagDTO)) return false; + return Objects.equals(getName(), tagDTO.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getName()); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/converter/PostDTOConverter.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/converter/PostDTOConverter.java new file mode 100644 index 000000000..fc89c5060 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/dto/converter/PostDTOConverter.java @@ -0,0 +1,76 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.dto.converter; + +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.Post; +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.PostDetails; +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.Tag; +import com.vladmihalcea.hpjp.spring.data.dto2entity.dto.PostCommentDTO; +import com.vladmihalcea.hpjp.spring.data.dto2entity.dto.PostDTO; +import com.vladmihalcea.hpjp.spring.data.dto2entity.dto.PostDetailsDTO; +import com.vladmihalcea.hpjp.spring.data.dto2entity.dto.TagDTO; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.stream.Collectors; + +/** + * @author Vlad Mihalcea + */ +public class PostDTOConverter { + + public static PostDTO of(Post post) { + return new PostDTO() + .setId(post.getId()) + .setTitle(post.getTitle()) + .setDetails( + new PostDetailsDTO() + .setId(post.getDetails().getId()) + .setCreatedOn(post.getDetails().getCreatedOn()) + .setCreatedBy(post.getDetails().getCreatedBy()) + ) + .setComments( + post.getComments().stream().map( + pc -> new PostCommentDTO() + .setId(pc.getId()) + .setReview(pc.getReview()) + ).collect(Collectors.toCollection((ArrayList::new))) + ) + .setTags( + post.getTags().stream().map( + t -> new TagDTO() + .setId(t.getId()) + .setName(t.getName()) + ).collect(Collectors.toCollection((HashSet::new))) + ); + } + + public static Post of(PostDTO postDTO) { + Post post = new Post() + .setId(postDTO.getId()) + .setTitle(postDTO.getTitle()) + .setDetails( + new PostDetails() + .setId(postDTO.getDetails().getId()) + .setCreatedOn(postDTO.getDetails().getCreatedOn()) + .setCreatedBy(postDTO.getDetails().getCreatedBy()) + ); + + for(PostCommentDTO commentDTO : postDTO.getComments()) { + post.addComment( + new PostComment() + .setId(commentDTO.getId()) + .setReview(commentDTO.getReview()) + ); + } + + for(TagDTO tagDTO : postDTO.getTags()) { + post.addTag( + new Tag() + .setId(tagDTO.getId()) + .setName(tagDTO.getName()) + ); + } + + return post; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/repository/PostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/repository/PostCommentRepository.java new file mode 100644 index 000000000..0b6655b17 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/repository/PostCommentRepository.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.repository; + +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.PostComment; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostCommentRepository extends BaseJpaRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/repository/PostDetailsRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/repository/PostDetailsRepository.java new file mode 100644 index 000000000..088c09ebc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/repository/PostDetailsRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.repository; + +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.PostDetails; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostDetailsRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/repository/PostRepository.java new file mode 100644 index 000000000..6d0c2f8f7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/repository/PostRepository.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.repository; + +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { + + @Query(""" + select p + from Post p + join fetch p.details + join fetch p.comments + where p.id = :id + """) + Post findByIdWithDetailsAndComments(@Param("id") Long id); + + @Query(""" + select p + from Post p + join fetch p.tags + where p.id = :id + """) + Post findByIdWithTags(@Param("id") Long id); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/repository/TagRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/repository/TagRepository.java new file mode 100644 index 000000000..0f81ee992 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/repository/TagRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.repository; + +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.Tag; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface TagRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/service/ForumFacadeService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/service/ForumFacadeService.java new file mode 100644 index 000000000..b857901d1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/service/ForumFacadeService.java @@ -0,0 +1,27 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.service; + +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.Post; +import com.vladmihalcea.hpjp.spring.data.dto2entity.dto.PostDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * @author Vlad Mihalcea + */ +@Service +public class ForumFacadeService { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Autowired + private ForumService forumService; + + public void updatePost(PostDTO postDTO) { + LOGGER.info("Convert DTO to JPA entity"); + Post post = forumService.convertDTOToPost(postDTO); + LOGGER.info("Merge updated JPA entity"); + forumService.mergePost(post); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/service/ForumService.java new file mode 100644 index 000000000..ea5c9f6b4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/dto2entity/service/ForumService.java @@ -0,0 +1,125 @@ +package com.vladmihalcea.hpjp.spring.data.dto2entity.service; + +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.Post; +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.dto2entity.domain.Tag; +import com.vladmihalcea.hpjp.spring.data.dto2entity.dto.PostDTO; +import com.vladmihalcea.hpjp.spring.data.dto2entity.dto.converter.PostDTOConverter; +import com.vladmihalcea.hpjp.spring.data.dto2entity.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.dto2entity.repository.PostRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumService { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + public Post findWithDetailsAndCommentsAndTagsById(Long id) { + Post post = postRepository.findByIdWithDetailsAndComments(id); + LOGGER.debug("Post [{}] fetched with details and comments", post.getId()); + return postRepository.findByIdWithTags(id); + } + + @Transactional(readOnly = true) + public Post convertDTOToPost(PostDTO postDTO) { + Post postFromDatabase = postRepository.findByIdWithDetailsAndComments(postDTO.getId()); + Post postFromDTO = PostDTOConverter.of(postDTO); + + postFromDatabase.setTitle(postDTO.getTitle()); + + if(postFromDatabase.getDetails() != null) { + postFromDatabase.getDetails().setCreatedBy(postFromDTO.getDetails().getCreatedBy()); + postFromDatabase.getDetails().setCreatedOn(postFromDTO.getDetails().getCreatedOn()); + } else { + postFromDatabase.setDetails(postFromDTO.getDetails()); + } + + mergeComments(postFromDatabase, postFromDTO.getComments()); + mergeTags(postFromDatabase, postFromDTO.getTags()); + + return postFromDatabase; + } + + @Transactional + public void mergePost(Post post) { + postRepository.merge(post); + } + + private void mergeComments(Post postFromDatabase, List commentsFromDTO) { + List commentsFromDatabase = postFromDatabase.getComments(); + + List removedComments = new ArrayList<>(commentsFromDatabase); + removedComments.removeAll(commentsFromDTO); + + for(PostComment removedComment : removedComments) { + postFromDatabase.removeComment(removedComment); + } + + List newComments = new ArrayList<>(commentsFromDTO); + newComments.removeAll(commentsFromDatabase); + commentsFromDTO.removeAll(newComments); + + Map postCommentByIdMap = commentsFromDatabase + .stream() + .collect(Collectors.toMap(PostComment::getId, Function.identity())); + + for(PostComment updatingComment : commentsFromDTO) { + PostComment existingComment = postCommentByIdMap.get(updatingComment.getId()); + if(existingComment != null) { + existingComment.setReview(updatingComment.getReview()); + } + } + + for(PostComment newComment : newComments) { + postFromDatabase.addComment(newComment); + } + } + + private void mergeTags(Post postFromDatabase, Set tagsFromDTO) { + Set tagsFromDatabase = postFromDatabase.getTags(); + + Set removedTags = new HashSet<>(tagsFromDatabase); + removedTags.removeAll(tagsFromDTO); + + for(Tag removedTag : removedTags) { + postFromDatabase.removeTag(removedTag); + } + + Set newTags = new HashSet<>(tagsFromDTO); + newTags.removeAll(tagsFromDatabase); + tagsFromDTO.removeAll(newTags); + + Map tagByIdMap = tagsFromDatabase + .stream() + .collect(Collectors.toMap(Tag::getId, Function.identity())); + + for(Tag updatingTag : tagsFromDTO) { + Tag existingTag = tagByIdMap.get(updatingTag.getId()); + if(existingTag != null) { + existingTag.setName(updatingTag.getName()); + } + } + + for(Tag newTag : newTags) { + postFromDatabase.addTag(newTag); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/SpringDataJPALockTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/SpringDataJPALockTest.java new file mode 100644 index 000000000..cbff7f523 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/SpringDataJPALockTest.java @@ -0,0 +1,90 @@ +package com.vladmihalcea.hpjp.spring.data.lock; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.lock.config.SpringDataJPALockConfiguration; +import com.vladmihalcea.hpjp.spring.data.lock.domain.Post; +import com.vladmihalcea.hpjp.spring.data.lock.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.lock.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.lock.repository.PostRepository; +import jakarta.persistence.LockModeType; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.support.TransactionCallback; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPALockConfiguration.class) +public class SpringDataJPALockTest extends AbstractSpringTest { + + public static final int POST_COMMENT_COUNT = 5; + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class, + }; + } + + @Test + public void test() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Post firstPost = postRepository.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setSlug("high-performance-java-persistence") + ); + + postRepository.persist( + new Post() + .setId(2L) + .setTitle("Hypersistence Optimizer") + .setSlug("hypersistence-optimizer") + ); + + long commentId = 0; + + for (long i = 0; i < POST_COMMENT_COUNT; i++) { + entityManager.persist( + new PostComment() + .setId(++commentId) + .setReview( + String.format("The %d chapter is amazing!", commentId) + ) + .setPost(firstPost) + ); + } + + return null; + }); + + Post post = postRepository.findBySlug("high-performance-java-persistence"); + assertNotNull(post); + + transactionTemplate.execute(transactionStatus -> { + Post postWithSharedLock = postRepository.lockById(1L, LockModeType.PESSIMISTIC_READ); + + Post postWithExclusiveLock = postRepository.lockById(2L, LockModeType.PESSIMISTIC_WRITE); + + List commentWithLock = postCommentRepository.lockAllByPostId(1L); + assertEquals(POST_COMMENT_COUNT, commentWithLock.size()); + + return null; + }); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/config/SpringDataJPALockConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/config/SpringDataJPALockConfiguration.java new file mode 100644 index 000000000..a631a067a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/config/SpringDataJPALockConfiguration.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.spring.data.lock.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.lock.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.lock", + } +) +@EnableJpaRepositories( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.lock.repository" + }, + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPALockConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/domain/Post.java new file mode 100644 index 000000000..76ecc53a4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/domain/Post.java @@ -0,0 +1,50 @@ +package com.vladmihalcea.hpjp.spring.data.lock.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table( + name = "post", + uniqueConstraints = @UniqueConstraint( + name = "UK_POST_SLUG", + columnNames = "slug" + ) +) +public class Post { + + @Id + private Long id; + + private String title; + + private String slug; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Post setSlug(String slug) { + this.slug = slug; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/domain/PostComment.java new file mode 100644 index 000000000..dfb5fe0d6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/domain/PostComment.java @@ -0,0 +1,46 @@ +package com.vladmihalcea.hpjp.spring.data.lock.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/repository/PostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/repository/PostCommentRepository.java new file mode 100644 index 000000000..a20eae35b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/repository/PostCommentRepository.java @@ -0,0 +1,26 @@ +package com.vladmihalcea.hpjp.spring.data.lock.repository; + +import com.vladmihalcea.hpjp.spring.data.lock.domain.PostComment; +import jakarta.persistence.LockModeType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostCommentRepository extends JpaRepository { + + @Query(""" + select pc + from PostComment pc + where pc.post.id = :postId + """) + @Lock(LockModeType.PESSIMISTIC_READ) + List lockAllByPostId(@Param("postId") Long postId); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/repository/PostRepository.java new file mode 100644 index 000000000..fbcc165fb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/lock/repository/PostRepository.java @@ -0,0 +1,21 @@ +package com.vladmihalcea.hpjp.spring.data.lock.repository; + +import com.vladmihalcea.hpjp.spring.data.lock.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { + + @Query(""" + select p + from Post p + where p.slug = :slug + """) + Post findBySlug(@Param("slug") String slug); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/SpringDataJPAMasqueradeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/SpringDataJPAMasqueradeTest.java new file mode 100644 index 000000000..95244ae90 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/SpringDataJPAMasqueradeTest.java @@ -0,0 +1,155 @@ +package com.vladmihalcea.hpjp.spring.data.masquerade; + +import com.blazebit.persistence.PagedList; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.masquerade.config.SpringDataJPAMasqueradeConfiguration; +import com.vladmihalcea.hpjp.spring.data.masquerade.domain.Post; +import com.vladmihalcea.hpjp.spring.data.masquerade.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.masquerade.dto.PostCommentDTO; +import com.vladmihalcea.hpjp.spring.data.masquerade.dto.PostDTO; +import com.vladmihalcea.hpjp.spring.data.masquerade.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.data.masquerade.service.ForumService; +import com.vladmihalcea.hpjp.util.CryptoUtils; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAMasqueradeConfiguration.class) +public class SpringDataJPAMasqueradeTest extends AbstractSpringTest { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + public static final int POST_COUNT = 50; + + public static final int PAGE_SIZE = 25; + + @Autowired + private PostRepository postRepository; + + @Autowired + private ForumService forumService; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class, + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + LocalDateTime timestamp = LocalDateTime.of( + 2021, 12, 30, 12, 0, 0, 0 + ); + + LongStream.rangeClosed(1, POST_COUNT).forEach(postId -> { + Post post = new Post() + .setId(postId) + .setTitle( + String.format("High-Performance Java Persistence - Chapter %d", + postId) + ) + .setCreatedOn( + timestamp.plusMinutes(postId) + ); + + postRepository.persist(post); + }); + + Post post = postRepository.getReferenceById(1L); + LongStream.rangeClosed(1, 10).forEach(postCommentId -> { + entityManager.persist( + new PostComment() + .setId(postCommentId) + .setPost(post) + .setReview( + String.format("Comment nr %d", postCommentId) + ) + ); + }); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void test() { + PagedList topPage = forumService.firstLatestPosts(PAGE_SIZE); + + assertEquals(POST_COUNT, topPage.getTotalSize()); + assertEquals(POST_COUNT / PAGE_SIZE, topPage.getTotalPages()); + assertEquals(1, topPage.getPage()); + List topIds = topPage.stream() + .map(PostDTO::getId) + .toList(); + assertEquals( + "3qEiB21WnB/yQ4muQe6cpw==", + topIds.get(0) + ); + assertEquals( + Long.valueOf(50), + CryptoUtils.decrypt(topIds.get(0), Long.class) + ); + + assertEquals( + "9jfsI1A92KIzd34ZfRxgtQ==", + topIds.get(1) + ); + assertEquals( + Long.valueOf(49), + CryptoUtils.decrypt(topIds.get(1), Long.class) + ); + + LOGGER.info("Top ids: {}", topIds); + + PagedList nextPage = forumService.findNextLatestPosts(topPage); + + assertEquals(2, nextPage.getPage()); + + List nextIds = nextPage.stream() + .map(PostDTO::getId) + .toList(); + assertEquals(Long.valueOf(25), CryptoUtils.decrypt(nextIds.get(0), Long.class)); + assertEquals(Long.valueOf(24), CryptoUtils.decrypt(nextIds.get(1), Long.class)); + + LOGGER.info("Next ids: {}", nextIds); + + PostDTO firstPost = nextPage.get(nextPage.getSize() - 1); + List comments = forumService.findCommentsByPost( + firstPost.getId() + ); + + assertEquals( + 10, + comments.size() + ); + assertEquals( + "ltAKs4jLw8N7q7SHeUR2Kw==", + comments.get(0).getId() + ); + assertEquals( + Long.valueOf(1), + CryptoUtils.decrypt(comments.get(0).getId(), Long.class) + ); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/config/SpringDataJPAMasqueradeConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/config/SpringDataJPAMasqueradeConfiguration.java new file mode 100644 index 000000000..11305e72f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/config/SpringDataJPAMasqueradeConfiguration.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.spring.data.masquerade.config; + +import com.blazebit.persistence.Criteria; +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.spi.CriteriaBuilderConfiguration; +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.masquerade.domain.Post; +import com.vladmihalcea.hpjp.spring.data.masquerade.dto.PostCommentDTO; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import jakarta.persistence.EntityManagerFactory; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.masquerade", + } +) +@EnableJpaRepositories( + value = "com.vladmihalcea.hpjp.spring.data.masquerade.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPAMasqueradeConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator( + List.of( + PostCommentDTO.class + ) + ) + ) + ); + } + + @Bean + public CriteriaBuilderFactory criteriaBuilderFactory(EntityManagerFactory entityManagerFactory) { + CriteriaBuilderConfiguration config = Criteria.getDefault(); + return config.createCriteriaBuilderFactory(entityManagerFactory); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/domain/Post.java new file mode 100644 index 000000000..a3ac96e08 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/domain/Post.java @@ -0,0 +1,51 @@ +package com.vladmihalcea.hpjp.spring.data.masquerade.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on", nullable = false) + private LocalDateTime createdOn; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/domain/PostComment.java new file mode 100644 index 000000000..62b26b5a5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/domain/PostComment.java @@ -0,0 +1,46 @@ +package com.vladmihalcea.hpjp.spring.data.masquerade.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/dto/PostCommentDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/dto/PostCommentDTO.java new file mode 100644 index 000000000..3df8aa668 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/dto/PostCommentDTO.java @@ -0,0 +1,26 @@ +package com.vladmihalcea.hpjp.spring.data.masquerade.dto; + +import com.vladmihalcea.hpjp.util.CryptoUtils; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentDTO { + + private final String id; + + private final String review; + + public PostCommentDTO(Long id, String review) { + this.id = CryptoUtils.encrypt(id); + this.review = review; + } + + public String getId() { + return id; + } + + public String getReview() { + return review; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/dto/PostDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/dto/PostDTO.java new file mode 100644 index 000000000..fb2f81e6c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/dto/PostDTO.java @@ -0,0 +1,26 @@ +package com.vladmihalcea.hpjp.spring.data.masquerade.dto; + +import com.vladmihalcea.hpjp.util.CryptoUtils; + +/** + * @author Vlad Mihalcea + */ +public class PostDTO { + + private final String id; + + private final String title; + + public PostDTO(Long id, String title) { + this.id = CryptoUtils.encrypt(id); + this.title = title; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/repository/CustomPostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/repository/CustomPostRepository.java new file mode 100644 index 000000000..5ef43a198 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/repository/CustomPostRepository.java @@ -0,0 +1,15 @@ +package com.vladmihalcea.hpjp.spring.data.masquerade.repository; + +import com.blazebit.persistence.PagedList; +import com.vladmihalcea.hpjp.spring.data.masquerade.dto.PostDTO; +import org.springframework.data.domain.Sort; + +/** + * @author Vlad Mihalcea + */ +public interface CustomPostRepository { + + PagedList findTopN(Sort sortBy, int pageSize); + + PagedList findNextN(Sort sortBy, PagedList previousPage); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/repository/CustomPostRepositoryImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/repository/CustomPostRepositoryImpl.java new file mode 100644 index 000000000..5b06ccbf1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/repository/CustomPostRepositoryImpl.java @@ -0,0 +1,58 @@ +package com.vladmihalcea.hpjp.spring.data.masquerade.repository; + +import com.blazebit.persistence.CriteriaBuilder; +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.PagedList; +import com.vladmihalcea.hpjp.spring.data.masquerade.domain.Post; +import com.vladmihalcea.hpjp.spring.data.masquerade.dto.PostDTO; +import jakarta.persistence.EntityManager; +import org.springframework.data.domain.Sort; + +/** + * @author Vlad Mihalcea + */ +public class CustomPostRepositoryImpl implements CustomPostRepository { + + private final EntityManager entityManager; + + private final CriteriaBuilderFactory criteriaBuilderFactory; + + public CustomPostRepositoryImpl( + EntityManager entityManager, + CriteriaBuilderFactory criteriaBuilderFactory) { + this.entityManager = entityManager; + this.criteriaBuilderFactory = criteriaBuilderFactory; + } + + @Override + public PagedList findTopN(Sort sortBy, int pageSize) { + return sortedCriteriaBuilder(sortBy) + .page(0, pageSize) + .withKeysetExtraction(true) + .getResultList(); + } + + @Override + public PagedList findNextN(Sort sortBy, PagedList previousPage) { + return sortedCriteriaBuilder(sortBy) + .page( + previousPage.getKeysetPage(), + previousPage.getPage() * previousPage.getMaxResults(), + previousPage.getMaxResults() + ) + .getResultList(); + } + + private CriteriaBuilder sortedCriteriaBuilder(Sort sortBy) { + CriteriaBuilder criteriaBuilder = criteriaBuilderFactory + .create(entityManager, Post.class) + .from(Post.class, "p"); + sortBy.forEach(order -> { + criteriaBuilder.orderBy(order.getProperty(), order.isAscending()); + }); + return criteriaBuilder.selectNew(PostDTO.class) + .with("p.id") + .with("p.title") + .end(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/repository/PostRepository.java new file mode 100644 index 000000000..addc2675f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/repository/PostRepository.java @@ -0,0 +1,28 @@ +package com.vladmihalcea.hpjp.spring.data.masquerade.repository; + +import com.vladmihalcea.hpjp.spring.data.masquerade.domain.Post; +import com.vladmihalcea.hpjp.spring.data.masquerade.dto.PostCommentDTO; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository, CustomPostRepository { + + @Query(""" + select new PostCommentDTO( + pc.id, + pc.review + ) + from PostComment pc + where pc.post.id = :postId + order by pc.id + """) + List findCommentsByPost(@Param("postId") Long postId); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/service/ForumService.java new file mode 100644 index 000000000..8aa367385 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/masquerade/service/ForumService.java @@ -0,0 +1,45 @@ +package com.vladmihalcea.hpjp.spring.data.masquerade.service; + +import com.blazebit.persistence.PagedList; +import com.vladmihalcea.hpjp.hibernate.fetching.pagination.Post_; +import com.vladmihalcea.hpjp.spring.data.masquerade.dto.PostCommentDTO; +import com.vladmihalcea.hpjp.spring.data.masquerade.dto.PostDTO; +import com.vladmihalcea.hpjp.spring.data.masquerade.repository.PostRepository; +import com.vladmihalcea.hpjp.util.CryptoUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumService { + + @Autowired + private PostRepository postRepository; + + public PagedList firstLatestPosts(int pageSize) { + return postRepository.findTopN( + Sort.by(Post_.CREATED_ON).descending().and(Sort.by(Post_.ID).descending()), + pageSize + ); + } + + public PagedList findNextLatestPosts(PagedList previousPage) { + return postRepository.findNextN( + Sort.by(Post_.CREATED_ON).descending().and(Sort.by(Post_.ID).descending()), + previousPage + ); + } + + public List findCommentsByPost(String postId) { + return postRepository.findCommentsByPost( + CryptoUtils.decrypt(postId, Long.class) + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/SpringDataJPAMergeTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/SpringDataJPAMergeTest.java new file mode 100644 index 000000000..1ee4d7302 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/SpringDataJPAMergeTest.java @@ -0,0 +1,69 @@ +package com.vladmihalcea.hpjp.spring.data.merge; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.merge.config.SpringDataJPAMergeConfiguration; +import com.vladmihalcea.hpjp.spring.data.merge.domain.Post; +import com.vladmihalcea.hpjp.spring.data.merge.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.merge.service.ForumService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.support.TransactionCallback; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAMergeConfiguration.class) +public class SpringDataJPAMergeTest extends AbstractSpringTest { + + @Autowired + private ForumService forumService; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class, + }; + } + + @Override + public void afterInit() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + for (long i = 1; i <= 3; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle( + String.format("High-Performance Java Persistence, Part no. %d", i) + ) + .addComment( + new PostComment() + .setReview( + String.format("Part no. %d review", i) + ) + ) + ); + } + return null; + }); + } + + @Test + public void testSaveAll() { + List posts = forumService.findAllByTitleLike("High-Performance Java Persistence%"); + + for (Post post : posts) { + post.setTitle("Vlad Mihalcea's " + post.getTitle()); + for (PostComment comment : post.getComments()) { + comment.setReview(comment.getReview() + " - ⭐⭐⭐⭐⭐"); + } + } + + LOGGER.info("Save posts"); + forumService.saveAll(posts); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/config/SpringDataJPAMergeConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/config/SpringDataJPAMergeConfiguration.java new file mode 100644 index 000000000..dcb9117e7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/config/SpringDataJPAMergeConfiguration.java @@ -0,0 +1,38 @@ +package com.vladmihalcea.hpjp.spring.data.merge.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.merge.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.merge" + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.merge.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPAMergeConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + properties.put("hibernate.order_updates", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/domain/Post.java new file mode 100644 index 000000000..89951d0c0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/domain/Post.java @@ -0,0 +1,54 @@ +package com.vladmihalcea.hpjp.spring.data.merge.domain; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @OneToMany( + mappedBy = "post", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/domain/PostComment.java new file mode 100644 index 000000000..d3a6182c6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/domain/PostComment.java @@ -0,0 +1,47 @@ +package com.vladmihalcea.hpjp.spring.data.merge.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/repository/BetterPostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/repository/BetterPostRepository.java new file mode 100644 index 000000000..d3449c456 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/repository/BetterPostRepository.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.spring.data.merge.repository; + +import com.vladmihalcea.hpjp.spring.data.merge.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface BetterPostRepository extends BaseJpaRepository { + + @Query(""" + select p + from Post p + left join fetch p.comments + where p.title like :titlePrefix + """) + List findAllWithCommentsByTitleLike(@Param("titlePrefix") String titlePrefix); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/repository/PostRepository.java new file mode 100644 index 000000000..50e9cb1e2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/repository/PostRepository.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.spring.data.merge.repository; + +import com.vladmihalcea.hpjp.spring.data.merge.domain.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends JpaRepository { + + @Query(""" + select p + from Post p + left join fetch p.comments + where p.title like :titlePrefix + """) + List findAllWithCommentsByTitleLike(@Param("titlePrefix") String titlePrefix); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/service/ForumService.java new file mode 100644 index 000000000..a1d95b9d7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/merge/service/ForumService.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.data.merge.service; + +import com.vladmihalcea.hpjp.spring.data.merge.domain.Post; +import com.vladmihalcea.hpjp.spring.data.merge.repository.BetterPostRepository; +import com.vladmihalcea.hpjp.spring.data.merge.repository.PostRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumService { + + private final PostRepository postRepository; + + @Autowired + private BetterPostRepository betterPostRepository; + + public ForumService(PostRepository postRepository) { + this.postRepository = postRepository; + } + + public List findAllByTitleLike(String titlePrefix) { + return postRepository.findAllWithCommentsByTitleLike(titlePrefix); + } + + @Transactional + public void saveAll(List posts) { + postRepository.saveAll(posts); + //betterPostRepository.updateAll(posts); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/SpringDataJPAProjectionTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/SpringDataJPAProjectionTest.java new file mode 100644 index 000000000..b12a0a995 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/SpringDataJPAProjectionTest.java @@ -0,0 +1,162 @@ +package com.vladmihalcea.hpjp.spring.data.projection; + +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.PostDTO; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.projection.config.SpringDataJPAProjectionConfiguration; +import com.vladmihalcea.hpjp.spring.data.projection.domain.Post; +import com.vladmihalcea.hpjp.spring.data.projection.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.projection.dto.PostCommentDTO; +import com.vladmihalcea.hpjp.spring.data.projection.dto.PostCommentRecord; +import com.vladmihalcea.hpjp.spring.data.projection.dto.PostCommentSummary; +import com.vladmihalcea.hpjp.spring.data.projection.repository.PostRepository; +import jakarta.persistence.Tuple; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.LongStream; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAProjectionConfiguration.class) +public class SpringDataJPAProjectionTest extends AbstractSpringTest { + + public static final int POST_COUNT = 50; + public static final int POST_COMMENT_COUNT = 5; + + @Autowired + private PostRepository postRepository; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class, + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + final AtomicLong commentId = new AtomicLong(0); + + LongStream.rangeClosed(1, POST_COUNT).forEach(i -> { + Post post = new Post() + .setId(i) + .setTitle(String.format("High-Performance Java Persistence, Chapter nr. %d", i)); + + LongStream.rangeClosed(1, POST_COMMENT_COUNT).forEach(j -> { + post.addComment( + new PostComment() + .setId(commentId.incrementAndGet()) + .setReview( + String.format("Good review nr. %d", commentId.get()) + ) + ); + + }); + entityManager.persist(post); + }); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void test() { + String titleToken = "High-Performance Java Persistence%"; + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + { + List commentTuples = postRepository + .findAllCommentTuplesByPostTitle(titleToken); + + assertFalse(commentTuples.isEmpty()); + + Tuple commentTuple = commentTuples.get(0); + long id = commentTuple.get("id", Number.class).longValue(); + String title = commentTuple.get("title", String.class); + + assertEquals(1L, id); + assertTrue(title.contains("Chapter nr. 1")); + } + + { + List commentSummaries = postRepository + .findAllCommentSummariesByPostTitle(titleToken); + + assertFalse(commentSummaries.isEmpty()); + + PostCommentSummary commentSummary = commentSummaries.get(0); + Long id = commentSummary.getId(); + String title = commentSummary.getTitle(); + + assertEquals(1L, id.longValue()); + assertTrue(title.contains("Chapter nr. 1")); + } + + { + List commentDTOs = postRepository.findCommentDTOByPostTitle(titleToken); + + assertFalse(commentDTOs.isEmpty()); + + PostCommentDTO commentDTO = commentDTOs.get(0); + Long id = commentDTO.getId(); + String title = commentDTO.getTitle(); + assertEquals(1L, id.longValue()); + assertTrue(title.contains("Chapter nr. 1")); + assertEquals( + commentDTO, + new PostCommentDTO( + commentDTO.getId(), + commentDTO.getTitle(), + commentDTO.getReview() + ) + ); + } + + { + List commentRecords = postRepository.findCommentRecordByPostTitle(titleToken); + + assertFalse(commentRecords.isEmpty()); + + PostCommentRecord commentRecord = commentRecords.get(0); + Long id = commentRecord.id(); + String title = commentRecord.title(); + assertEquals(1L, id.longValue()); + assertTrue(title.contains("Chapter nr. 1")); + assertEquals( + commentRecord, + new PostCommentRecord( + commentRecord.id(), + commentRecord.title(), + commentRecord.review() + ) + ); + } + + List postDTOs = postRepository.findPostDTOByPostTitle(titleToken); + + assertEquals(POST_COUNT, postDTOs.size()); + + PostDTO postDTO = postDTOs.get(0); + assertEquals(Long.valueOf(1), postDTO.getId()); + assertTrue(postDTO.getTitle().contains("Chapter nr. 1")); + assertEquals(POST_COMMENT_COUNT, postDTO.getComments().size()); + + return null; + }); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/config/SpringDataJPAProjectionConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/config/SpringDataJPAProjectionConfiguration.java new file mode 100644 index 000000000..85871495d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/config/SpringDataJPAProjectionConfiguration.java @@ -0,0 +1,50 @@ +package com.vladmihalcea.hpjp.spring.data.projection.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.projection.domain.Post; +import com.vladmihalcea.hpjp.spring.data.projection.dto.PostCommentDTO; +import com.vladmihalcea.hpjp.spring.data.projection.dto.PostCommentRecord; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.projection", + } +) +@EnableJpaRepositories("com.vladmihalcea.hpjp.spring.data.projection.repository") +public class SpringDataJPAProjectionConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator( + List.of( + PostCommentDTO.class, + PostCommentRecord.class + ) + ) + ) + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/domain/Post.java new file mode 100644 index 000000000..0e134850c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/domain/Post.java @@ -0,0 +1,50 @@ +package com.vladmihalcea.hpjp.spring.data.projection.domain; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/domain/PostComment.java new file mode 100644 index 000000000..776cf94a4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/domain/PostComment.java @@ -0,0 +1,46 @@ +package com.vladmihalcea.hpjp.spring.data.projection.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/dto/PostCommentDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/dto/PostCommentDTO.java new file mode 100644 index 000000000..3809fab42 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/dto/PostCommentDTO.java @@ -0,0 +1,52 @@ +package com.vladmihalcea.hpjp.spring.data.projection.dto; + +import java.util.Objects; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentDTO { + + private final Long id; + + private final String title; + + private final String review; + + public PostCommentDTO(Long id, String title, String review) { + this.id = id; + this.title = title; + this.review = review; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getReview() { + return review; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostCommentDTO)) return false; + PostCommentDTO that = (PostCommentDTO) o; + return Objects.equals(getId(), that.getId()) && + Objects.equals(getTitle(), that.getTitle()) && + Objects.equals(getReview(), that.getReview()); + } + + @Override + public int hashCode() { + return Objects.hash( + getId(), + getTitle(), + getReview() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/dto/PostCommentRecord.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/dto/PostCommentRecord.java new file mode 100644 index 000000000..4b7d55fbf --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/dto/PostCommentRecord.java @@ -0,0 +1,11 @@ +package com.vladmihalcea.hpjp.spring.data.projection.dto; + +/** + * @author Vlad Mihalcea + */ +public record PostCommentRecord( + Long id, + String title, + String review +) { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/dto/PostCommentSummary.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/dto/PostCommentSummary.java new file mode 100644 index 000000000..5536750d5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/dto/PostCommentSummary.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.data.projection.dto; + +/** + * @author Vlad Mihalcea + */ +public interface PostCommentSummary { + + Long getId(); + + String getTitle(); + + String getReview(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/repository/CustomPostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/repository/CustomPostRepository.java new file mode 100644 index 000000000..84913418f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/repository/CustomPostRepository.java @@ -0,0 +1,14 @@ +package com.vladmihalcea.hpjp.spring.data.projection.repository; + +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.PostDTO; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface CustomPostRepository { + + List findPostDTOByPostTitle(@Param("postTitle") String postTitle); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/repository/CustomPostRepositoryImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/repository/CustomPostRepositoryImpl.java new file mode 100644 index 000000000..a10412356 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/repository/CustomPostRepositoryImpl.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.spring.data.projection.repository; + +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.DistinctListTransformer; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.PostDTO; +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.PostDTOResultTransformer; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.hibernate.query.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class CustomPostRepositoryImpl implements CustomPostRepository { + + @PersistenceContext + private EntityManager entityManager; + + @Override + public List findPostDTOByPostTitle(@Param("postTitle") String postTitle) { + return entityManager.createNativeQuery(""" + SELECT p.id AS p_id, + p.title AS p_title, + pc.id AS pc_id, + pc.review AS pc_review + FROM post p + JOIN post_comment pc ON p.id = pc.post_id + WHERE p.title LIKE :postTitle + ORDER BY pc.id + """) + .setParameter("postTitle", postTitle) + .unwrap(Query.class) + .setTupleTransformer(new PostDTOResultTransformer()) + .setResultListTransformer(DistinctListTransformer.INSTANCE) + .getResultList(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/repository/PostRepository.java new file mode 100644 index 000000000..0219be721 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/projection/repository/PostRepository.java @@ -0,0 +1,70 @@ +package com.vladmihalcea.hpjp.spring.data.projection.repository; + +import com.vladmihalcea.hpjp.spring.data.projection.domain.Post; +import com.vladmihalcea.hpjp.spring.data.projection.dto.PostCommentDTO; +import com.vladmihalcea.hpjp.spring.data.projection.dto.PostCommentRecord; +import com.vladmihalcea.hpjp.spring.data.projection.dto.PostCommentSummary; +import jakarta.persistence.Tuple; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends JpaRepository, CustomPostRepository { + + @Query(""" + select + p.id as id, + p.title as title, + c.review as review + from PostComment c + join c.post p + where p.title like :postTitle + order by c.id + """) + List findAllCommentTuplesByPostTitle(@Param("postTitle") String postTitle); + + @Query(""" + select + p.id as id, + p.title as title, + c.review as review + from PostComment c + join c.post p + where p.title like :postTitle + order by c.id + """) + List findAllCommentSummariesByPostTitle(@Param("postTitle") String postTitle); + + @Query(""" + select new PostCommentDTO( + p.id as id, + p.title as title, + c.review as review + ) + from PostComment c + join c.post p + where p.title like :postTitle + order by c.id + """) + List findCommentDTOByPostTitle(@Param("postTitle") String postTitle); + + @Query(""" + select new PostCommentRecord( + p.id as id, + p.title as title, + c.review as review + ) + from PostComment c + join c.post p + where p.title like :postTitle + order by c.id + """) + List findCommentRecordByPostTitle(@Param("postTitle") String postTitle); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/SpringDataJPAQueryByExampleTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/SpringDataJPAQueryByExampleTest.java new file mode 100644 index 000000000..d34a8af94 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/SpringDataJPAQueryByExampleTest.java @@ -0,0 +1,236 @@ +package com.vladmihalcea.hpjp.spring.data.query.example; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.query.example.config.SpringDataJPAQueryByExampleConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.example.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.example.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.query.example.domain.PostComment_; +import com.vladmihalcea.hpjp.spring.data.query.example.domain.Tag; +import com.vladmihalcea.hpjp.spring.data.query.example.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.query.example.repository.PostRepository; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.*; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAQueryByExampleConfiguration.class) +public class SpringDataJPAQueryByExampleTest extends AbstractSpringTest { + + public static final int POST_COUNT = 2; + public static final int POST_COMMENT_COUNT = 10; + public static final int TAG_COUNT = 10; + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class, + Tag.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + List tags = new ArrayList<>(); + + for (long i = 1; i <= TAG_COUNT; i++) { + Tag tag = new Tag() + .setId(i) + .setName(String.format("Tag nr. %d", i)); + + entityManager.persist(tag); + tags.add(tag); + } + + LocalDateTime timestamp = LocalDateTime.of( + 2023, 3, 15, 12, 0, 0, 0 + ); + + long commentId = 0; + + for (long postId = 1; postId <= POST_COUNT; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post nr. %d", postId)); + + for (long i = 1; i <= POST_COMMENT_COUNT; i++) { + PostComment comment = new PostComment() + .setId(++commentId) + .setReview(i % 7 == 0 ? "Spam comment" : String.format("Awesome post %d", i)) + .setStatus(PostComment.Status.PENDING) + .setCreatedOn(timestamp.plusMinutes(postId)) + .setVotes((int) (i % 7)); + + post.addComment(comment); + } + + for (int i = 0; i < TAG_COUNT; i++) { + post.getTags().add(tags.get(i)); + } + + entityManager.persist(post); + } + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void testFindByReview() { + Example postExample = Example.of( + new PostComment() + .setReview("Spam comment"), + ExampleMatcher.matching() + .withIgnorePaths(PostComment_.VOTES) + .withStringMatcher(ExampleMatcher.StringMatcher.EXACT) + ); + + List comments = (List) postCommentRepository.findAll(postExample); + assertFalse(comments.isEmpty()); + } + + @Test + public void testFindByPost() { + PostComment postComment = new PostComment() + .setPost( + new Post() + .setId(1L) + ); + + List comments = (List) postCommentRepository.findAll( + Example.of( + postComment, + ExampleMatcher.matching() + .withIgnorePaths(PostComment_.VOTES) + ) + ); + assertEquals(POST_COMMENT_COUNT, comments.size()); + } + + @Test + public void testFindByPostOrderByCreatedOn() { + PostComment postComment = new PostComment() + .setPost( + new Post() + .setId(1L) + ); + + List comments = (List) postCommentRepository.findAll( + Example.of( + postComment, + ExampleMatcher.matching() + .withIgnorePaths(PostComment_.VOTES) + ), + Sort.by(Sort.Order.asc(PostComment_.CREATED_ON)) + ); + + assertEquals(POST_COMMENT_COUNT, comments.size()); + } + + @Test + public void testFindByPostAndStatusOrderByCreatedOn() { + List comments = (List) postCommentRepository.findAll( + Example.of( + new PostComment() + .setPost(new Post().setId(1L)) + .setStatus(PostComment.Status.PENDING), + ExampleMatcher.matching() + .withIgnorePaths(PostComment_.VOTES) + ), + Sort.by(Sort.Order.asc(PostComment_.CREATED_ON)) + ); + + assertEquals(POST_COMMENT_COUNT, comments.size()); + } + + @Test + public void testFindByPostAndStatusAndReviewLikeOrderByCreatedOn() { + PostComment postComment = new PostComment() + .setPost(new Post().setId(1L)) + .setStatus(PostComment.Status.PENDING) + .setReview("Spam"); + + List comments = (List) postCommentRepository.findAll( + Example.of( + postComment, + ExampleMatcher.matching() + .withIgnorePaths(PostComment_.VOTES) + .withMatcher( + PostComment_.REVIEW, + ExampleMatcher.GenericPropertyMatcher::contains + ) + ), + Sort.by(Sort.Order.asc(PostComment_.CREATED_ON)) + ); + + assertFalse(comments.isEmpty()); + } + + @Test + public void testFindByPostAndStatusAndReviewLikeAndVotesGreaterThanEqualOrderByCreatedOn() { + String reviewPattern = "Awesome"; + int votes = 0; + + PostComment postComment = new PostComment() + .setPost(new Post().setId(1L)) + .setStatus(PostComment.Status.PENDING) + .setReview(reviewPattern) + .setVotes(votes); + + List comments = (List) postCommentRepository.findAll( + Example.of( + postComment, + ExampleMatcher.matching() + .withMatcher(PostComment_.REVIEW, ExampleMatcher.GenericPropertyMatcher::contains) + ), + Sort.by(Sort.Order.asc(PostComment_.CREATED_ON)) + ); + } + + @Test + public void testFindBy() { + String reviewPattern = "Awesome"; + int pageSize = 10; + + PostComment postComment = new PostComment() + .setPost(new Post().setId(1L)) + .setStatus(PostComment.Status.PENDING) + .setReview(reviewPattern); + + Page comments = postCommentRepository.findBy( + Example.of( + postComment, + ExampleMatcher.matching() + .withIgnorePaths(PostComment_.VOTES) + .withMatcher(PostComment_.REVIEW, ExampleMatcher.GenericPropertyMatcher::contains) + ), + q -> q + .sortBy(Sort.by(PostComment_.CREATED_ON).ascending()) + .page(Pageable.ofSize(pageSize)) + ); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/config/SpringDataJPAQueryByExampleConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/config/SpringDataJPAQueryByExampleConfiguration.java new file mode 100644 index 000000000..d3f18e7c9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/config/SpringDataJPAQueryByExampleConfiguration.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.data.query.example.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.example.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.query.example", + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.query.example.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPAQueryByExampleConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/domain/Post.java new file mode 100644 index 000000000..0fdc57136 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/domain/Post.java @@ -0,0 +1,65 @@ +package com.vladmihalcea.hpjp.spring.data.query.example.domain; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/domain/PostComment.java new file mode 100644 index 000000000..7c84e6e6d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/domain/PostComment.java @@ -0,0 +1,101 @@ +package com.vladmihalcea.hpjp.spring.data.query.example.domain; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + private PostComment parent; + + private String review; + + @Enumerated(EnumType.ORDINAL) + private Status status; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + private int votes; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public PostComment getParent() { + return parent; + } + + public PostComment setParent(PostComment parent) { + this.parent = parent; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Status getStatus() { + return status; + } + + public PostComment setStatus(Status status) { + this.status = status; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public int getVotes() { + return votes; + } + + public PostComment setVotes(int votes) { + this.votes = votes; + return this; + } + + public enum Status { + PENDING, + APPROVED, + SPAM; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/domain/PostCommentDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/domain/PostCommentDTO.java new file mode 100644 index 000000000..222f5fde7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/domain/PostCommentDTO.java @@ -0,0 +1,90 @@ +package com.vladmihalcea.hpjp.spring.data.query.example.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentDTO { + + public static final String ID = "id"; + public static final String POST_ID = "postId"; + public static final String PARENT_ID = "parentId"; + public static final String REVIEW = "review"; + public static final String CREATED_ON = "createdOn"; + public static final String VOTES = "votes"; + + private Long id; + + private Long postId; + + private Long parentId; + + private String review; + + private Date createdOn; + + private int votes; + + @JsonIgnore + private PostCommentDTO parent; + + private List replies = new ArrayList<>(); + + public PostCommentDTO(Object[] tuples, Map aliasToIndexMap) { + this.id = (Long) tuples[aliasToIndexMap.get(ID)]; + this.postId = (Long) tuples[aliasToIndexMap.get(POST_ID)]; + this.parentId = (Long) tuples[aliasToIndexMap.get(PARENT_ID)]; + this.review = (String) tuples[aliasToIndexMap.get(REVIEW)]; + this.createdOn = Timestamp.valueOf((LocalDateTime) tuples[aliasToIndexMap.get(CREATED_ON)]); + this.votes = (int) tuples[aliasToIndexMap.get(VOTES)]; + } + + public Long getId() { + return id; + } + + public Long getPostId() { + return postId; + } + + public Long getParentId() { + return parentId; + } + + public String getReview() { + return review; + } + + public Date getCreatedOn() { + return createdOn; + } + + public int getVotes() { + return votes; + } + + public List getReplies() { + return replies; + } + + public void addReply(PostCommentDTO reply) { + replies.add(reply); + reply.parent = this; + } + + public PostCommentDTO getParent() { + return parent; + } + + public PostCommentDTO root() { + return (parent != null) ? parent.root() : this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/domain/Tag.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/domain/Tag.java new file mode 100644 index 000000000..7d60b8d66 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/domain/Tag.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.spring.data.query.example.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Tag") +@Table(name = "tag") +public class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/repository/PostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/repository/PostCommentRepository.java new file mode 100644 index 000000000..2cd2901dd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/repository/PostCommentRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.query.example.repository; + +import com.vladmihalcea.hpjp.spring.data.query.example.domain.PostComment; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostCommentRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/repository/PostRepository.java new file mode 100644 index 000000000..eeae7d0ab --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/example/repository/PostRepository.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.data.query.example.repository; + +import com.vladmihalcea.hpjp.spring.data.query.example.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/exists/SpringDataJPAExistsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/exists/SpringDataJPAExistsTest.java new file mode 100644 index 000000000..40ea30284 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/exists/SpringDataJPAExistsTest.java @@ -0,0 +1,115 @@ +package com.vladmihalcea.hpjp.spring.data.query.exists; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.query.exists.config.SpringDataJPAExistsConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.exists.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.exists.domain.Post_; +import com.vladmihalcea.hpjp.spring.data.query.exists.repository.PostRepository; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import static org.junit.Assert.assertTrue; +import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAExistsConfiguration.class) +public class SpringDataJPAExistsTest extends AbstractSpringTest { + + @Autowired + private PostRepository postRepository; + + /*@Autowired + private HypersistenceOptimizer hypersistenceOptimizer;*/ + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + entityManager.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + .setSlug("high-performance-java-persistence") + ); + + + entityManager.persist( + new Post() + .setId(2L) + .setTitle("Hypersistence Optimizer") + .setSlug("hypersistence-optimizer") + ); + + for (long i = 3; i <= 1000; i++) { + entityManager.persist( + new Post() + .setId(i) + .setTitle(String.format("Post %d", i)) + .setSlug(String.format("post-%d", i)) + ); + } + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void test() { + String slug = "high-performance-java-persistence"; + + //Query by id - Bad Idea + assertTrue( + postRepository.findBySlug(slug).isPresent() + ); + + //Query by example - Bad Idea + assertTrue( + postRepository.exists( + Example.of( + new Post().setSlug(slug), + ExampleMatcher.matching() + .withIgnorePaths(Post_.ID) + .withMatcher(Post_.SLUG, exact()) + ) + ) + ); + + //hypersistenceOptimizer.getEvents().clear(); + assertTrue( + postRepository.existsById(1L) + ); + //assertTrue(hypersistenceOptimizer.getEvents().isEmpty()); + //Query using exists - Okayish Idea + assertTrue( + postRepository.existsBySlug(slug) + ); + //assertTrue(hypersistenceOptimizer.getEvents().isEmpty()); + + //Query using exists - Okayish Idea + assertTrue( + postRepository.existsBySlugWithCount(slug) + ); + + assertTrue( + postRepository.existsBySlugWithCase(slug) + ); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/exists/config/SpringDataJPAExistsConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/exists/config/SpringDataJPAExistsConfiguration.java new file mode 100644 index 000000000..2fd3e29ae --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/exists/config/SpringDataJPAExistsConfiguration.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.hpjp.spring.data.query.exists.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.exists.domain.Post; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.query.exists", + } +) +@EnableJpaRepositories("com.vladmihalcea.hpjp.spring.data.query.exists.repository") +public class SpringDataJPAExistsConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/exists/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/exists/domain/Post.java new file mode 100644 index 000000000..2c43f7ce3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/exists/domain/Post.java @@ -0,0 +1,52 @@ +package com.vladmihalcea.hpjp.spring.data.query.exists.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import org.hibernate.annotations.NaturalId; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table( + name = "post", + uniqueConstraints = @UniqueConstraint( + name = "UK_POST_SLUG", + columnNames = "slug" + ) +) +public class Post { + + @Id + private Long id; + + private String title; + + @NaturalId + private String slug; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public Post setSlug(String slug) { + this.slug = slug; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/exists/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/exists/repository/PostRepository.java new file mode 100644 index 000000000..364f2214b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/exists/repository/PostRepository.java @@ -0,0 +1,45 @@ +package com.vladmihalcea.hpjp.spring.data.query.exists.repository; + +import com.vladmihalcea.hpjp.spring.data.query.exists.domain.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends JpaRepository { + + Optional findBySlug(String slug); + + boolean existsById(Long id); + + boolean existsBySlug(String slug); + + @Query(value = """ + SELECT + CASE WHEN EXISTS ( + SELECT 1 + FROM post + WHERE slug = :slug + ) + THEN 'true' + ELSE 'false' + END + """, + nativeQuery = true + ) + boolean existsBySlugWithCase(@Param("slug") String slug); + + @Query(value = """ + select count(p.id) = 1 + from Post p + where p.slug = :slug + """ + ) + boolean existsBySlugWithCount(@Param("slug") String slug); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/SpringDataJPAJoinFetchPaginationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/SpringDataJPAJoinFetchPaginationTest.java new file mode 100644 index 000000000..eb87a7db4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/SpringDataJPAJoinFetchPaginationTest.java @@ -0,0 +1,214 @@ +package com.vladmihalcea.hpjp.spring.data.query.fetch; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.query.fetch.config.SpringDataJPAJoinFetchPaginationConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.fetch.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.fetch.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.query.fetch.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.data.query.fetch.service.ForumService; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAJoinFetchPaginationConfiguration.class) +public class SpringDataJPAJoinFetchPaginationTest extends AbstractSpringTest { + + public static final int POST_COUNT = 1_000; + public static final int COMMENT_COUNT = 10; + + @Autowired + private PostRepository postRepository; + + @Autowired + private ForumService forumService; + + @Autowired + private DataSource dataSource; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + LocalDateTime timestamp = LocalDate.now().atStartOfDay().plusHours(12); + + LongStream.rangeClosed(1, POST_COUNT).forEach(postId -> { + Post post = new Post() + .setId(postId) + .setTitle( + String.format("High-Performance Java Persistence - Chapter %d", + postId) + ) + .setCreatedOn(timestamp.plusMinutes(postId)); + + LongStream.rangeClosed(1, COMMENT_COUNT) + .forEach(commentOffset -> { + long commentId = ((postId - 1) * COMMENT_COUNT) + commentOffset; + + post.addComment( + new PostComment() + .setId(commentId) + .setReview( + String.format("Comment nr. %d - A must-read!", commentId) + ) + .setCreatedOn(timestamp.plusMinutes(commentId)) + ); + + }); + + postRepository.persist(post); + }); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + + executeStatement(dataSource, "CREATE INDEX IF NOT EXISTS idx_post_created_on ON post (created_on)"); + executeStatement(dataSource, "CREATE INDEX IF NOT EXISTS idx_post_comment_post_id ON post_comment (post_id)"); + executeStatement(dataSource, "ANALYZE VERBOSE"); + } + + @Test + public void testFindAllByTitleTopN() { + int maxCount = 25; + + Page posts = postRepository.findAllByTitleLike( + "High-Performance Java Persistence %", + PageRequest.of(0, maxCount, Sort.by("createdOn")) + ); + + assertEquals(maxCount, posts.getSize()); + } + + @Test + public void testFindAllByTitleNextN() { + int maxCount = 25; + + Page posts = postRepository.findAllByTitleLike( + "High-Performance Java Persistence %", + PageRequest.of(1, maxCount, Sort.by("createdOn")) + ); + + assertEquals(maxCount, posts.getSize()); + } + + @Test + public void testFindAllByTitleTopNWithNativeQuery() { + int maxCount = 25; + + Page posts = postRepository.findAllByTitleLikeNativeQuery( + "High-Performance Java Persistence %", + PageRequest.of(0, maxCount) + ); + + assertEquals(maxCount, posts.getSize()); + } + + @Test + public void testFindAllByTitleNextNWithNativeQuery() { + int maxCount = 25; + + Page posts = postRepository.findAllByTitleLikeNativeQuery( + "High-Performance Java Persistence %", + PageRequest.of(1, maxCount) + ); + + assertEquals(maxCount, posts.getSize()); + } + + @Test + public void testFindAllWithCommentsByTitleAntiPattern() { + + int maxCount = 25; + + try { + Page posts = postRepository.findAllByTitleWithCommentsAntiPattern( + "High-Performance Java Persistence %", + PageRequest.of(0, maxCount, Sort.by("createdOn", "id")) + ); + + assertEquals(maxCount, posts.getSize()); + } catch (Exception e) { + LOGGER.error("In-memory pagination", e); + + assertTrue(ExceptionUtil.rootCause(e).getMessage().startsWith("firstResult/maxResults specified with collection fetch")); + } + } + + @Test + public void testFindTopNWithCommentsByTitle() { + + int maxCount = 25; + + List posts = postRepository.findFirstByTitleWithCommentsByTitle( + "High-Performance Java Persistence %", + maxCount + ); + + assertEquals(maxCount, posts.size()); + } + + @Test + public void testFindWithCommentsByTitleWithTwoQueries() { + + int maxCount = 25; + + List posts = forumService.findAllPostsByTitleWithComments( + "High-Performance Java Persistence %", + PageRequest.of(0, maxCount, Sort.by("createdOn")) + ); + + assertEquals(maxCount, posts.size()); + } + + protected void executeStatement(DataSource dataSource, String sql) { + Boolean initialAutoCommit = null; + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + try { + initialAutoCommit = connection.getAutoCommit(); + if (!initialAutoCommit) { + connection.setAutoCommit(true); + } + statement.executeUpdate(sql); + } finally { + if(initialAutoCommit != null) { + connection.setAutoCommit(initialAutoCommit); + } + } + } catch (SQLException e) { + LOGGER.error("Statement failed", e); + } + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/SpringDataJPAOSIVTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/SpringDataJPAOSIVTest.java new file mode 100644 index 000000000..a2f6e71b0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/SpringDataJPAOSIVTest.java @@ -0,0 +1,130 @@ +package com.vladmihalcea.hpjp.spring.data.query.fetch; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.query.fetch.config.SpringDataJPAJoinFetchPaginationConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.fetch.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.fetch.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.query.fetch.domain.Post_; +import com.vladmihalcea.hpjp.spring.data.query.fetch.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.data.query.fetch.service.ForumService; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceUnit; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.data.domain.*; +import org.springframework.orm.jpa.EntityManagerHolder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.sql.DataSource; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.stream.LongStream; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAJoinFetchPaginationConfiguration.class) +public class SpringDataJPAOSIVTest extends AbstractSpringTest { + + public static final int POST_COUNT = 100; + public static final int COMMENT_COUNT = 10; + + @Autowired + private PostRepository postRepository; + + @Autowired + private ForumService forumService; + + @Autowired + private DataSource dataSource; + + @Autowired + private CacheManager cacheManager; + + @PersistenceUnit + private EntityManagerFactory entityManagerFactory; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + LocalDateTime timestamp = LocalDate.now().atStartOfDay().plusHours(12); + + LongStream.rangeClosed(1, POST_COUNT).forEach(postId -> { + Post post = new Post() + .setId(postId) + .setTitle( + String.format("High-Performance Java Persistence - Chapter %d", + postId) + ) + .setCreatedOn(timestamp.plusMinutes(postId)); + + LongStream.rangeClosed(1, COMMENT_COUNT) + .forEach(commentOffset -> { + long commentId = ((postId - 1) * COMMENT_COUNT) + commentOffset; + + post.addComment( + new PostComment() + .setId(commentId) + .setReview( + String.format("Comment nr. %d - A must-read!", commentId) + ) + .setCreatedOn(timestamp.plusMinutes(commentId)) + ); + + }); + + postRepository.persist(post); + }); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void testOSIV() { + try(EntityManager entityManager = entityManagerFactory.createEntityManager()) { + EntityManagerHolder entityManagerHolder = new EntityManagerHolder(entityManager); + TransactionSynchronizationManager.bindResource( + entityManagerFactory, + entityManagerHolder + ); + + Page posts = transactionTemplate.execute( + status -> postRepository.findAll( + Example.of( + new Post() + .setTitle("High-Performance Java Persistence"), + ExampleMatcher.matching() + .withStringMatcher(ExampleMatcher.StringMatcher.STARTING) + .withIgnorePaths(Post_.CREATED_ON) + ), + PageRequest.of(0, 10, Sort.by(Post_.CREATED_ON)) + ) + ); + + for(Post post : posts) { + LOGGER.info("Post has {} comments", post.getComments().size()); + } + } finally { + TransactionSynchronizationManager.unbindResource(entityManagerFactory); + } + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/SpringDataJPAStreamTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/SpringDataJPAStreamTest.java new file mode 100644 index 000000000..e7b3af5d9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/SpringDataJPAStreamTest.java @@ -0,0 +1,114 @@ +package com.vladmihalcea.hpjp.spring.data.query.fetch; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.query.fetch.config.SpringDataJPAJoinFetchPaginationConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.fetch.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.fetch.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.query.fetch.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.data.query.fetch.service.ForumService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.LongStream; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAJoinFetchPaginationConfiguration.class) +public class SpringDataJPAStreamTest extends AbstractSpringTest { + + public static final int POST_COUNT = 10; + public static final int COMMENT_COUNT = 10; + + @Autowired + private PostRepository postRepository; + + @Autowired + private ForumService forumService; + + @Autowired + private CacheManager cacheManager; + + @Autowired + private ExecutorService executorService; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + LocalDateTime timestamp = LocalDate.now().atStartOfDay().plusHours(12); + + LongStream.rangeClosed(1, POST_COUNT).forEach(postId -> { + Post post = new Post() + .setId(postId) + .setTitle( + String.format("High-Performance Java Persistence - Chapter %d", + postId) + ) + .setCreatedOn(timestamp.plusMinutes(postId)); + + LongStream.rangeClosed(1, COMMENT_COUNT) + .forEach(commentOffset -> { + long commentId = ((postId - 1) * COMMENT_COUNT) + commentOffset; + + post.addComment( + new PostComment() + .setId(commentId) + .setReview( + String.format("Comment nr. %d - A must-read!", commentId) + ) + .setCreatedOn(timestamp.plusMinutes(commentId)) + ); + + }); + + postRepository.persist(post); + }); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void testFindAllPostsPublishedToday() { + List posts = forumService.findAllPostsPublishedToday(); + + assertEquals(POST_COUNT, posts.size()); + } + + @Test + public void testUpdateCache() throws InterruptedException { + Cache postCache = cacheManager.getCache(Post.class.getSimpleName()); + + assertNull(postCache.get(1L)); + forumService.updatePostCache(); + + executorService.shutdown(); + executorService.awaitTermination(1, TimeUnit.SECONDS); + assertNotNull(postCache.get(1L)); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/config/SpringDataJPAJoinFetchPaginationConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/config/SpringDataJPAJoinFetchPaginationConfiguration.java new file mode 100644 index 000000000..7d36b1092 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/config/SpringDataJPAJoinFetchPaginationConfiguration.java @@ -0,0 +1,66 @@ +package com.vladmihalcea.hpjp.spring.data.query.fetch.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.fetch.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.metamodel.EntityType; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.query.fetch", + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.query.fetch.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +@EnableCaching +public class SpringDataJPAJoinFetchPaginationConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + //properties.put("hibernate.query.fail_on_pagination_over_collection_fetch", "true"); + } + + @Bean + public CacheManager cacheManager(EntityManagerFactory entityManagerFactory) { + String[] entityNames = entityManagerFactory + .getMetamodel() + .getEntities() + .stream() + .map(EntityType::getName) + .toArray(String[]::new); + + return new ConcurrentMapCacheManager(entityNames); + } + + @Bean + public ExecutorService executorService() { + return Executors.newFixedThreadPool( + Runtime.getRuntime().availableProcessors() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/domain/Post.java new file mode 100644 index 000000000..7ae608d3f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/domain/Post.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.spring.data.query.fetch.domain; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on", nullable = false) + private LocalDateTime createdOn = LocalDateTime.now(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/domain/PostComment.java new file mode 100644 index 000000000..1e718d741 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/domain/PostComment.java @@ -0,0 +1,60 @@ +package com.vladmihalcea.hpjp.spring.data.query.fetch.domain; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/repository/PostRepository.java new file mode 100644 index 000000000..86396f759 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/repository/PostRepository.java @@ -0,0 +1,111 @@ +package com.vladmihalcea.hpjp.spring.data.query.fetch.repository; + +import com.vladmihalcea.hpjp.spring.data.query.fetch.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import jakarta.persistence.QueryHint; +import org.hibernate.jpa.AvailableHints; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Stream; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { + + Page findAllByTitleLike(@Param("titlePattern") String titlePattern, Pageable pageRequest); + + @Query(""" + select p + from Post p + where p.title like :titlePattern + """ + ) + Page findAllByTitleLikeQuery(@Param("titlePattern") String titlePattern, Pageable pageRequest); + + @Query(value = """ + SELECT p.id, p.title, p.created_on + FROM post p + WHERE p.title ilike :titlePattern + ORDER BY p.created_on + """, + nativeQuery = true + ) + Page findAllByTitleLikeNativeQuery(@Param("titlePattern") String titlePattern, Pageable pageRequest); + + @Query( + value = """ + select p + from Post p + left join fetch p.comments + where p.title like :titlePattern + """, + countQuery = """ + select count(p) + from Post p + where p.title like :titlePattern + """ + ) + Page findAllByTitleWithCommentsAntiPattern( + @Param("titlePattern") String titlePattern, + Pageable pageRequest + ); + + @Query(""" + select p + from Post p + left join fetch p.comments pc + where p.id in ( + select pr.id + from ( + select + p1.id as id, + dense_rank() over (order by p1.createdOn, p1.id) as ranking + from Post p1 + where p1.title like :titlePattern + ) pr + where pr.ranking <= :maxCount + ) + """ + ) + List findFirstByTitleWithCommentsByTitle( + @Param("titlePattern") String titlePattern, + @Param("maxCount") int maxCount + ); + + @Query(""" + select p.id + from Post p + where p.title like :titlePattern + """ + ) + List findAllPostIdsByTitle(@Param("titlePattern") String titlePattern, Pageable pageRequest); + + @Query(""" + select p + from Post p + left join fetch p.comments + where p.id in :postIds + """ + ) + List findAllByIdWithComments(@Param("postIds") List postIds); + + @Query(""" + select p + from Post p + where date(p.createdOn) >= :sinceDate + """ + ) + @QueryHints( + @QueryHint(name = AvailableHints.HINT_FETCH_SIZE, value = "25") + ) + Stream streamByCreatedOnSince(@Param("sinceDate") LocalDate sinceDate); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/service/ForumService.java new file mode 100644 index 000000000..d068ae9bf --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/fetch/service/ForumService.java @@ -0,0 +1,64 @@ +package com.vladmihalcea.hpjp.spring.data.query.fetch.service; + +import com.vladmihalcea.hpjp.spring.data.query.fetch.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.fetch.repository.PostRepository; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.stream.Stream; + +/** + * @author Vlad Mihalcea + */ +@Service +public class ForumService { + + private final PostRepository postRepository; + + private final CacheManager cacheManager; + + private final Cache postCache; + + private final ExecutorService executorService; + + public ForumService( + PostRepository postRepository, + CacheManager cacheManager, + ExecutorService executorService) { + this.postRepository = postRepository; + this.cacheManager = cacheManager; + this.postCache = cacheManager.getCache(Post.class.getSimpleName()); + this.executorService = executorService; + } + + @Transactional(readOnly = true) + public List findAllPostsByTitleWithComments(String titlePattern, PageRequest pageRequest) { + return postRepository.findAllByIdWithComments( + postRepository.findAllPostIdsByTitle( + titlePattern, + pageRequest + ) + ); + } + + @Transactional(readOnly = true) + public List findAllPostsPublishedToday() { + try(Stream stream = postRepository.streamByCreatedOnSince(LocalDate.now())) { + return stream.toList(); + } + } + + @Transactional(readOnly = true) + public void updatePostCache() { + LocalDate yesterday = LocalDate.now().minusDays(1); + try(Stream postStream = postRepository.streamByCreatedOnSince(yesterday)) { + postStream.forEach(post -> executorService.submit(() -> postCache.put(post.getId(), post))); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/SpringDataJPAQueryHintTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/SpringDataJPAQueryHintTest.java new file mode 100644 index 000000000..5d4f4bdca --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/SpringDataJPAQueryHintTest.java @@ -0,0 +1,101 @@ +package com.vladmihalcea.hpjp.spring.data.query.hint; + +import com.vladmihalcea.hpjp.hibernate.logging.validator.sql.SQLStatementCountValidator; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.query.hint.config.SpringDataJPAQueryHintConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.hint.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.hint.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.query.hint.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.data.query.hint.service.ForumService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import javax.sql.DataSource; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAQueryHintConfiguration.class) +public class SpringDataJPAQueryHintTest extends AbstractSpringTest { + + @Autowired + private PostRepository postRepository; + + @Autowired + private ForumService forumService; + + @Autowired + private DataSource dataSource; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + + int COMMENT_COUNT = 10; + + LocalDateTime timestamp = LocalDateTime.of( + 2023, 3, 22, 12, 0, 0, 0 + ); + + LongStream.rangeClosed(1, 10).forEach(postId -> { + Post post = new Post() + .setId(postId) + .setTitle( + String.format("High-Performance Java Persistence - Chapter %d", + postId) + ) + .setCreatedOn(timestamp.plusMinutes(postId)); + + LongStream.rangeClosed(1, COMMENT_COUNT) + .forEach(commentOffset -> { + long commentId = ((postId - 1) * COMMENT_COUNT) + commentOffset; + + post.addComment( + new PostComment() + .setId(commentId) + .setReview( + String.format("Comment nr. %d - A must-read!", commentId) + ) + .setCreatedOn(timestamp.plusMinutes(commentId)) + ); + + }); + + postRepository.persist(post); + }); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void testFindTopNWithCommentsByTitle() { + SQLStatementCountValidator.reset(); + List posts = forumService.findAllByIdWithComments(List.of(1L, 2L, 3L)); + + assertEquals(3, posts.size()); + SQLStatementCountValidator.assertSelectCount(1); + SQLStatementCountValidator.assertUpdateCount(0); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/config/SpringDataJPAQueryHintConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/config/SpringDataJPAQueryHintConfiguration.java new file mode 100644 index 000000000..158f3994d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/config/SpringDataJPAQueryHintConfiguration.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.data.query.hint.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.hint.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.query.hint", + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.query.hint.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPAQueryHintConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/domain/Post.java new file mode 100644 index 000000000..1e369ec9d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/domain/Post.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.spring.data.query.hint.domain; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @Column(name = "created_on", nullable = false) + private LocalDateTime createdOn = LocalDateTime.now(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/domain/PostComment.java new file mode 100644 index 000000000..e0abc2a14 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/domain/PostComment.java @@ -0,0 +1,60 @@ +package com.vladmihalcea.hpjp.spring.data.query.hint.domain; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/repository/PostRepository.java new file mode 100644 index 000000000..b3b597e5b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/repository/PostRepository.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hpjp.spring.data.query.hint.repository; + +import com.vladmihalcea.hpjp.spring.data.query.hint.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import jakarta.persistence.QueryHint; +import org.hibernate.jpa.AvailableHints; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { + + @Query(""" + select p + from Post p + left join fetch p.comments + where p.id in :postIds + """ + ) + @QueryHints( + @QueryHint(name = AvailableHints.HINT_READ_ONLY, value = "true") + ) + List findAllByIdWithComments(@Param("postIds") List postIds); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/service/ForumService.java new file mode 100644 index 000000000..772cec2a0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/hint/service/ForumService.java @@ -0,0 +1,45 @@ +package com.vladmihalcea.hpjp.spring.data.query.hint.service; + +import com.vladmihalcea.hpjp.spring.data.query.hint.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.hint.repository.PostRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +@Service +public class ForumService { + + @Autowired + private PostRepository postRepository; + + @PersistenceContext + private EntityManager entityManager; + + @Transactional + public List findAllByIdWithComments(List ids) { + List posts = postRepository.findAllByIdWithComments(ids); + SharedSessionContractImplementor session = entityManager.unwrap(SharedSessionContractImplementor.class); + org.hibernate.engine.spi.PersistenceContext persistenceContext = session.getPersistenceContext(); + + for(Post post : posts) { + assertTrue(entityManager.contains(post)); + + EntityEntry entityEntry = persistenceContext.getEntry(post); + assertNull(entityEntry.getLoadedState()); + post.setTitle("Changed!"); + } + return posts; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/SpringDataJPAQueryMethodTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/SpringDataJPAQueryMethodTest.java new file mode 100644 index 000000000..c51ffe48a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/SpringDataJPAQueryMethodTest.java @@ -0,0 +1,184 @@ +package com.vladmihalcea.hpjp.spring.data.query.method; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.query.method.config.SpringDataJPAQueryMethodConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.method.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.method.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.query.method.domain.PostCommentDTO; +import com.vladmihalcea.hpjp.spring.data.query.method.domain.Tag; +import com.vladmihalcea.hpjp.spring.data.query.method.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.query.method.repository.PostRepository; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAQueryMethodConfiguration.class) +public class SpringDataJPAQueryMethodTest extends AbstractSpringTest { + + public static final int POST_COUNT = 2; + public static final int POST_COMMENT_COUNT = 10; + public static final int TAG_COUNT = 10; + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class, + Tag.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + List tags = new ArrayList<>(); + + for (long i = 1; i <= TAG_COUNT; i++) { + Tag tag = new Tag() + .setId(i) + .setName(String.format("Tag nr. %d", i)); + + entityManager.persist(tag); + tags.add(tag); + } + + LocalDateTime timestamp = LocalDateTime.of( + 2023, 3, 15, 12, 0, 0, 0 + ); + + long commentId = 0; + + for (long postId = 1; postId <= POST_COUNT; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post nr. %d", postId)); + + PostComment parent = null; + + for (long i = 1; i <= POST_COMMENT_COUNT; i++) { + PostComment comment = new PostComment() + .setId(++commentId) + .setParent(parent) + .setReview(i % 7 == 0 ? "Spam comment" : String.format("Comment %d", i)) + .setStatus(PostComment.Status.PENDING) + .setCreatedOn(timestamp.plusMinutes(postId)) + .setVotes((int) (i % 7)); + + if(i == 2 || i == 4 || i == 8) { + parent = comment; + } + post.addComment(comment); + } + + for (int i = 0; i < TAG_COUNT; i++) { + post.getTags().add(tags.get(i)); + } + + entityManager.persist(post); + } + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void testFindByPost() { + Post post = postRepository.getReferenceById(1L); + + List comments = postCommentRepository.findAllByPost(post); + assertEquals(POST_COMMENT_COUNT, comments.size()); + } + + @Test + public void testFindByPostOrderByCreatedOn() { + Post post = postRepository.getReferenceById(1L); + + List comments = postCommentRepository.findAllByPostOrderByCreatedOn(post); + assertEquals(POST_COMMENT_COUNT, comments.size()); + } + + @Test + public void testFindByPostAndStatusOrderByCreatedOn() { + Post post = postRepository.getReferenceById(1L); + + List comments = postCommentRepository.findAllByPostAndStatusOrderByCreatedOn( + post, + PostComment.Status.PENDING + ); + assertEquals(POST_COMMENT_COUNT, comments.size()); + } + + @Test + public void testFindByPostAndStatusAndReviewLikeOrderByCreatedOn() { + Post post = postRepository.getReferenceById(1L); + String reviewPattern = "Spam"; + + List comments = postCommentRepository.findAllByPostAndStatusAndReviewLikeOrderByCreatedOn( + post, + PostComment.Status.PENDING, + reviewPattern + ); + assertTrue(comments.isEmpty()); + } + + @Test + public void testFindByPostAndStatusAndReviewLikeAndVotesGreaterThanEqualOrderByCreatedOn() { + Post post = postRepository.getReferenceById(1L); + String reviewPattern = "Spam%"; + int minVotes = 1; + + int expectedCommentCount = 0; + { + List comments = postCommentRepository.findAllByPostAndStatusAndReviewLikeAndVotesGreaterThanEqualOrderByCreatedOn( + post, + PostComment.Status.PENDING, + reviewPattern, + minVotes + ); + + expectedCommentCount = comments.size(); + } + + List comments = postCommentRepository.findAllByPostStatusReviewAndMinVotes( + post, + PostComment.Status.PENDING, + reviewPattern, + minVotes + ); + + assertEquals(expectedCommentCount, comments.size()); + } + + @Test + public void testFindHierarchy() throws JsonProcessingException { + Post post = postRepository.getReferenceById(1L); + + List commentRoots = postCommentRepository.findCommentHierarchy(post); + String json = new ObjectMapper().writeValueAsString(commentRoots); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/config/SpringDataJPAQueryMethodConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/config/SpringDataJPAQueryMethodConfiguration.java new file mode 100644 index 000000000..79a7b0b3e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/config/SpringDataJPAQueryMethodConfiguration.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.data.query.method.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.method.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.query.method", + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.query.method.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPAQueryMethodConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/domain/Post.java new file mode 100644 index 000000000..077074ce8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/domain/Post.java @@ -0,0 +1,65 @@ +package com.vladmihalcea.hpjp.spring.data.query.method.domain; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/domain/PostComment.java new file mode 100644 index 000000000..b91f0e433 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/domain/PostComment.java @@ -0,0 +1,101 @@ +package com.vladmihalcea.hpjp.spring.data.query.method.domain; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + private PostComment parent; + + private String review; + + @Enumerated(EnumType.ORDINAL) + private Status status; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + private int votes; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public PostComment getParent() { + return parent; + } + + public PostComment setParent(PostComment parent) { + this.parent = parent; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Status getStatus() { + return status; + } + + public PostComment setStatus(Status status) { + this.status = status; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public int getVotes() { + return votes; + } + + public PostComment setVotes(int votes) { + this.votes = votes; + return this; + } + + public enum Status { + PENDING, + APPROVED, + SPAM; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/domain/PostCommentDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/domain/PostCommentDTO.java new file mode 100644 index 000000000..fb385a12e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/domain/PostCommentDTO.java @@ -0,0 +1,90 @@ +package com.vladmihalcea.hpjp.spring.data.query.method.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentDTO { + + public static final String ID = "id"; + public static final String POST_ID = "postId"; + public static final String PARENT_ID = "parentId"; + public static final String REVIEW = "review"; + public static final String CREATED_ON = "createdOn"; + public static final String VOTES = "votes"; + + private Long id; + + private Long postId; + + private Long parentId; + + private String review; + + private Date createdOn; + + private int votes; + + @JsonIgnore + private PostCommentDTO parent; + + private List replies = new ArrayList<>(); + + public PostCommentDTO(Object[] tuples, Map aliasToIndexMap) { + this.id = (Long) tuples[aliasToIndexMap.get(ID)]; + this.postId = (Long) tuples[aliasToIndexMap.get(POST_ID)]; + this.parentId = (Long) tuples[aliasToIndexMap.get(PARENT_ID)]; + this.review = (String) tuples[aliasToIndexMap.get(REVIEW)]; + this.createdOn = Timestamp.valueOf((LocalDateTime) tuples[aliasToIndexMap.get(CREATED_ON)]); + this.votes = (int) tuples[aliasToIndexMap.get(VOTES)]; + } + + public Long getId() { + return id; + } + + public Long getPostId() { + return postId; + } + + public Long getParentId() { + return parentId; + } + + public String getReview() { + return review; + } + + public Date getCreatedOn() { + return createdOn; + } + + public int getVotes() { + return votes; + } + + public List getReplies() { + return replies; + } + + public void addReply(PostCommentDTO reply) { + replies.add(reply); + reply.parent = this; + } + + public PostCommentDTO getParent() { + return parent; + } + + public PostCommentDTO root() { + return (parent != null) ? parent.root() : this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/domain/Tag.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/domain/Tag.java new file mode 100644 index 000000000..42d17407d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/domain/Tag.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.spring.data.query.method.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Tag") +@Table(name = "tag") +public class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/repository/CustomPostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/repository/CustomPostCommentRepository.java new file mode 100644 index 000000000..92c47f3d8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/repository/CustomPostCommentRepository.java @@ -0,0 +1,14 @@ +package com.vladmihalcea.hpjp.spring.data.query.method.repository; + +import com.vladmihalcea.hpjp.spring.data.query.method.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.method.domain.PostCommentDTO; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface CustomPostCommentRepository { + + List findCommentHierarchy(Post post); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/repository/CustomPostCommentRepositoryImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/repository/CustomPostCommentRepositoryImpl.java new file mode 100644 index 000000000..f74314e4b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/repository/CustomPostCommentRepositoryImpl.java @@ -0,0 +1,70 @@ +package com.vladmihalcea.hpjp.spring.data.query.method.repository; + +import com.vladmihalcea.hpjp.spring.data.query.method.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.method.domain.PostCommentDTO; +import io.hypersistence.utils.hibernate.query.DistinctListTransformer; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.hibernate.query.TupleTransformer; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class CustomPostCommentRepositoryImpl implements CustomPostCommentRepository { + + @PersistenceContext + private EntityManager entityManager; + + @Override + public List findCommentHierarchy(Post post) { + return entityManager.createQuery(""" + select + pc.id as id, + pc.post.id as postId, + pc.parent.id as parentId, + pc.review as review, + pc.createdOn as createdOn, + pc.votes as votes + from PostComment pc + where + pc.post = :post + order by createdOn + """) + .setParameter("post", post) + .unwrap(org.hibernate.query.Query.class) + .setTupleTransformer(new PostCommentTupleTransformer()) + .setResultListTransformer(DistinctListTransformer.INSTANCE) + .getResultList(); + } + + public static class PostCommentTupleTransformer implements TupleTransformer { + + private Map commentDTOMap = new LinkedHashMap<>(); + + @Override + public PostCommentDTO transformTuple(Object[] tuple, String[] aliases) { + Map aliasToIndexMap = aliasToIndexMap(aliases); + PostCommentDTO commentDTO = new PostCommentDTO(tuple, aliasToIndexMap); + commentDTOMap.put(commentDTO.getId(), commentDTO); + + PostCommentDTO parent = commentDTOMap.get(commentDTO.getParentId()); + if (parent != null) { + parent.addReply(commentDTO); + } + + return commentDTO.root(); + } + + private Map aliasToIndexMap(String[] aliases) { + Map aliasToIndexMap = new LinkedHashMap<>(); + for (int i = 0; i < aliases.length; i++) { + aliasToIndexMap.put(aliases[i], i); + } + return aliasToIndexMap; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/repository/PostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/repository/PostCommentRepository.java new file mode 100644 index 000000000..d58369df3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/repository/PostCommentRepository.java @@ -0,0 +1,57 @@ +package com.vladmihalcea.hpjp.spring.data.query.method.repository; + +import com.vladmihalcea.hpjp.spring.data.query.method.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.method.domain.PostComment; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostCommentRepository + extends BaseJpaRepository, CustomPostCommentRepository { + + List findAllByPost(Post post); + + List findAllByPostOrderByCreatedOn(Post post); + + List findAllByPostAndStatusOrderByCreatedOn( + Post post, + PostComment.Status status + ); + + List findAllByPostAndStatusAndReviewLikeOrderByCreatedOn( + Post post, + PostComment.Status status, + String reviewPattern + ); + + List findAllByPostAndStatusAndReviewLikeAndVotesGreaterThanEqualOrderByCreatedOn( + Post post, + PostComment.Status status, + String reviewPattern, + int votes + ); + + @Query(""" + select pc + from PostComment pc + where + pc.post = :post and + pc.status = :status and + pc.review like :reviewPattern and + pc.votes >= :votes + order by createdOn + """) + List findAllByPostStatusReviewAndMinVotes( + @Param("post") Post post, + @Param("status") PostComment.Status status, + @Param("reviewPattern") String reviewPattern, + @Param("votes") int votes + ); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/repository/PostRepository.java new file mode 100644 index 000000000..8fdb450df --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/method/repository/PostRepository.java @@ -0,0 +1,44 @@ +package com.vladmihalcea.hpjp.spring.data.query.method.repository; + +import com.vladmihalcea.hpjp.spring.data.query.method.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { + + //This query will throw a MultipleBagFetchException when Spring bootstraps + /* + @Query(""" + select p + from Post p + left join fetch p.comments + left join fetch p.tags + where p.id between :minId and :maxId + """) + List findAllWithCommentsAndTags(@Param("minId") long minId, @Param("maxId") long maxId); + */ + + @Query(""" + select p + from Post p + left join fetch p.comments + where p.id between :minId and :maxId + """) + List findAllWithComments(@Param("minId") long minId, @Param("maxId") long maxId); + + @Query(""" + select p + from Post p + left join fetch p.tags + where p.id between :minId and :maxId + """) + List findAllWithTags(@Param("minId") long minId, @Param("maxId") long maxId); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/SpringDataJPAMultipleBagFetchTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/SpringDataJPAMultipleBagFetchTest.java new file mode 100644 index 000000000..9e9b36707 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/SpringDataJPAMultipleBagFetchTest.java @@ -0,0 +1,106 @@ +package com.vladmihalcea.hpjp.spring.data.query.multibag; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.query.multibag.config.SpringDataJPAMultipleBagFetchConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.multibag.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.multibag.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.query.multibag.domain.Tag; +import com.vladmihalcea.hpjp.spring.data.query.multibag.service.BrokenForumService; +import com.vladmihalcea.hpjp.spring.data.query.multibag.service.ForumService; +import org.hibernate.LazyInitializationException; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAMultipleBagFetchConfiguration.class) +public class SpringDataJPAMultipleBagFetchTest extends AbstractSpringTest { + + public static final long POST_COUNT = 50; + public static final long POST_COMMENT_COUNT = 20; + public static final long TAG_COUNT = 10; + + @Autowired + private BrokenForumService brokenForumService; + + @Autowired + private ForumService forumService; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class, + Tag.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + List tags = new ArrayList<>(); + + for (long i = 1; i <= TAG_COUNT; i++) { + Tag tag = new Tag() + .setId(i) + .setName(String.format("Tag nr. %d", i)); + + entityManager.persist(tag); + tags.add(tag); + } + + long commentId = 0; + + for (long postId = 1; postId <= POST_COUNT; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post nr. %d", postId)); + + + for (long i = 0; i < POST_COMMENT_COUNT; i++) { + post.addComment( + new PostComment() + .setId(++commentId) + .setReview("Excellent!") + ); + } + + for (int i = 0; i < TAG_COUNT; i++) { + post.getTags().add(tags.get(i)); + } + + entityManager.persist(post); + } + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void testLazyInitializationException() { + List comments = forumService.findAllCommentsByReview("Excellent!"); + + try { + for(PostComment comment : comments) { + LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); + } + } catch (LazyInitializationException expected) { + assertTrue(expected.getMessage().toLowerCase(Locale.ROOT).contains("could not initialize proxy")); + } + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/config/SpringDataJPAMultipleBagFetchConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/config/SpringDataJPAMultipleBagFetchConfiguration.java new file mode 100644 index 000000000..611c77923 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/config/SpringDataJPAMultipleBagFetchConfiguration.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.data.query.multibag.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.multibag.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.query.multibag", + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.query.multibag.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPAMultipleBagFetchConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/domain/Post.java new file mode 100644 index 000000000..d146d3083 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/domain/Post.java @@ -0,0 +1,65 @@ +package com.vladmihalcea.hpjp.spring.data.query.multibag.domain; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/domain/PostComment.java new file mode 100644 index 000000000..7e74818f6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/domain/PostComment.java @@ -0,0 +1,46 @@ +package com.vladmihalcea.hpjp.spring.data.query.multibag.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/domain/Tag.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/domain/Tag.java new file mode 100644 index 000000000..4c7061ca3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/domain/Tag.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.spring.data.query.multibag.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Tag") +@Table(name = "tag") +public class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/repository/CustomPostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/repository/CustomPostRepository.java new file mode 100644 index 000000000..599af2b93 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/repository/CustomPostRepository.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.data.query.multibag.repository; + +import com.vladmihalcea.hpjp.spring.data.query.multibag.domain.Post; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface CustomPostRepository { + + List findAllWithCommentsAndTags(long minId, long maxId); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/repository/CustomPostRepositoryImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/repository/CustomPostRepositoryImpl.java new file mode 100644 index 000000000..5895537e3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/repository/CustomPostRepositoryImpl.java @@ -0,0 +1,30 @@ +package com.vladmihalcea.hpjp.spring.data.query.multibag.repository; + +import com.vladmihalcea.hpjp.spring.data.query.multibag.domain.Post; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class CustomPostRepositoryImpl implements CustomPostRepository { + + @PersistenceContext + private EntityManager entityManager; + + @Override + public List findAllWithCommentsAndTags(long minId, long maxId) { + return entityManager.createQuery(""" + select p + from Post p + left join fetch p.comments + left join fetch p.tags + where p.id between :minId and :maxId + """, Post.class) + .setParameter("minId", minId) + .setParameter("maxId", maxId) + .getResultList(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/repository/PostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/repository/PostCommentRepository.java new file mode 100644 index 000000000..ce1b06753 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/repository/PostCommentRepository.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.spring.data.query.multibag.repository; + +import com.vladmihalcea.hpjp.spring.data.query.multibag.domain.PostComment; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostCommentRepository extends BaseJpaRepository { + + List findAllByReview(String review); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/repository/PostRepository.java new file mode 100644 index 000000000..ce9eaf154 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/repository/PostRepository.java @@ -0,0 +1,44 @@ +package com.vladmihalcea.hpjp.spring.data.query.multibag.repository; + +import com.vladmihalcea.hpjp.spring.data.query.multibag.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository, CustomPostRepository { + + //This query will throw a MultipleBagFetchException when Spring bootstraps + /* + @Query(""" + select p + from Post p + left join fetch p.comments + left join fetch p.tags + where p.id between :minId and :maxId + """) + List findAllWithCommentsAndTags(@Param("minId") long minId, @Param("maxId") long maxId); + */ + + @Query(""" + select p + from Post p + left join fetch p.comments + where p.id between :minId and :maxId + """) + List findAllWithComments(@Param("minId") Long minId, @Param("maxId") Long maxId); + + @Query(""" + select p + from Post p + left join fetch p.tags + where p.id between :minId and :maxId + """) + List findAllWithTags(@Param("minId") Long minId, @Param("maxId") Long maxId); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/service/BrokenForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/service/BrokenForumService.java new file mode 100644 index 000000000..07eb79388 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/service/BrokenForumService.java @@ -0,0 +1,28 @@ +package com.vladmihalcea.hpjp.spring.data.query.multibag.service; + +import com.vladmihalcea.hpjp.spring.data.query.multibag.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.multibag.repository.PostRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class BrokenForumService { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Autowired + private PostRepository postRepository; + + public List findAllWithCommentsAndTags(long minId, long maxId) { + return postRepository.findAllWithCommentsAndTags(minId, maxId); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/service/ForumService.java new file mode 100644 index 000000000..dc3c143fb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/multibag/service/ForumService.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.data.query.multibag.service; + +import com.vladmihalcea.hpjp.spring.data.query.multibag.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.multibag.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.query.multibag.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.query.multibag.repository.PostRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumService { + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + public List findAllCommentsByReview(String review) { + return postCommentRepository.findAllByReview(review); + } + + public List findAllWithCommentsAndTags(Long minId, Long maxId) { + List posts = postRepository.findAllWithComments(minId, maxId); + + return !posts.isEmpty() ? + postRepository.findAllWithTags(minId, maxId) : + posts; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/SpringDataJPASpecificationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/SpringDataJPASpecificationTest.java new file mode 100644 index 000000000..ec9e054a4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/SpringDataJPASpecificationTest.java @@ -0,0 +1,173 @@ +package com.vladmihalcea.hpjp.spring.data.query.specification; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.query.specification.config.SpringDataJPASpecificationConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.specification.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.specification.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.query.specification.domain.Tag; +import com.vladmihalcea.hpjp.spring.data.query.specification.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.query.specification.repository.PostRepository; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static com.vladmihalcea.hpjp.spring.data.query.specification.repository.PostCommentRepository.Specs.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPASpecificationConfiguration.class) +public class SpringDataJPASpecificationTest extends AbstractSpringTest { + + public static final int POST_COUNT = 2; + public static final int POST_COMMENT_COUNT = 10; + public static final int TAG_COUNT = 10; + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class, + Tag.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + List tags = new ArrayList<>(); + + for (long i = 1; i <= TAG_COUNT; i++) { + Tag tag = new Tag() + .setId(i) + .setName(String.format("Tag nr. %d", i)); + + entityManager.persist(tag); + tags.add(tag); + } + + LocalDateTime timestamp = LocalDateTime.of( + 2023, 3, 15, 12, 0, 0, 0 + ); + + long commentId = 0; + + for (long postId = 1; postId <= POST_COUNT; postId++) { + Post post = new Post() + .setId(postId) + .setTitle(String.format("Post nr. %d", postId)); + + + for (long i = 1; i <= POST_COMMENT_COUNT; i++) { + PostComment comment = new PostComment() + .setId(++commentId) + .setReview(i % 7 == 0 ? "Spam comment" : String.format("Awesome post %d", i)) + .setStatus(PostComment.Status.PENDING) + .setCreatedOn(timestamp.plusMinutes(postId)) + .setVotes((int) (i % 7)); + + post.addComment(comment); + } + + for (int i = 0; i < TAG_COUNT; i++) { + post.getTags().add(tags.get(i)); + } + + entityManager.persist(post); + } + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void testFindByPost() { + Post post = postRepository.getReferenceById(1L); + + List comments = postCommentRepository.findAll( + byPost(post) + ); + assertEquals(POST_COMMENT_COUNT, comments.size()); + } + + @Test + public void testFindByPostOrderByCreatedOn() { + Post post = postRepository.getReferenceById(1L); + + List comments = postCommentRepository.findAll( + orderByCreatedOn( + byPost(post) + ) + ); + + assertEquals(POST_COMMENT_COUNT, comments.size()); + } + + @Test + public void testFindByPostAndStatusOrderByCreatedOn() { + Post post = postRepository.getReferenceById(1L); + + List comments = postCommentRepository.findAll( + orderByCreatedOn( + byPost(post) + .and(byStatus(PostComment.Status.PENDING)) + ) + ); + + assertEquals(POST_COMMENT_COUNT, comments.size()); + } + + @Test + public void testFindByPostAndStatusAndReviewLikeOrderByCreatedOn() { + Post post = postRepository.getReferenceById(1L); + String reviewPattern = "Spam%"; + + List comments = postCommentRepository.findAll( + orderByCreatedOn( + byPost(post) + .and(byStatus(PostComment.Status.PENDING)) + .and(byReviewLike(reviewPattern)) + ) + ); + + assertFalse(comments.isEmpty()); + } + + @Test + public void testFindByPostAndStatusAndReviewLikeAndVotesGreaterThanEqualOrderByCreatedOn() { + Post post = postRepository.getReferenceById(1L); + String reviewPattern = "Awesome%"; + int minVotes = 1; + + List comments = postCommentRepository.findAll( + orderByCreatedOn( + byPost(post) + .and(byStatus(PostComment.Status.PENDING)) + .and(byReviewLike(reviewPattern)) + .and(byVotesGreaterThanEqual(minVotes)) + ) + ); + + assertFalse(comments.isEmpty()); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/config/SpringDataJPASpecificationConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/config/SpringDataJPASpecificationConfiguration.java new file mode 100644 index 000000000..7c0b700a6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/config/SpringDataJPASpecificationConfiguration.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.data.query.specification.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.specification.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.query.specification", + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.query.specification.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPASpecificationConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/domain/Post.java new file mode 100644 index 000000000..08e79a137 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/domain/Post.java @@ -0,0 +1,71 @@ +package com.vladmihalcea.hpjp.spring.data.query.specification.domain; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/domain/PostComment.java new file mode 100644 index 000000000..f76d09de9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/domain/PostComment.java @@ -0,0 +1,101 @@ +package com.vladmihalcea.hpjp.spring.data.query.specification.domain; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + private PostComment parent; + + private String review; + + @Enumerated(EnumType.ORDINAL) + private Status status; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + private int votes; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public PostComment getParent() { + return parent; + } + + public PostComment setParent(PostComment parent) { + this.parent = parent; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Status getStatus() { + return status; + } + + public PostComment setStatus(Status status) { + this.status = status; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public int getVotes() { + return votes; + } + + public PostComment setVotes(int votes) { + this.votes = votes; + return this; + } + + public enum Status { + PENDING, + APPROVED, + SPAM; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/domain/PostCommentDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/domain/PostCommentDTO.java new file mode 100644 index 000000000..3d226fe9a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/domain/PostCommentDTO.java @@ -0,0 +1,90 @@ +package com.vladmihalcea.hpjp.spring.data.query.specification.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentDTO { + + public static final String ID = "id"; + public static final String POST_ID = "postId"; + public static final String PARENT_ID = "parentId"; + public static final String REVIEW = "review"; + public static final String CREATED_ON = "createdOn"; + public static final String VOTES = "votes"; + + private Long id; + + private Long postId; + + private Long parentId; + + private String review; + + private Date createdOn; + + private int votes; + + @JsonIgnore + private PostCommentDTO parent; + + private List replies = new ArrayList<>(); + + public PostCommentDTO(Object[] tuples, Map aliasToIndexMap) { + this.id = (Long) tuples[aliasToIndexMap.get(ID)]; + this.postId = (Long) tuples[aliasToIndexMap.get(POST_ID)]; + this.parentId = (Long) tuples[aliasToIndexMap.get(PARENT_ID)]; + this.review = (String) tuples[aliasToIndexMap.get(REVIEW)]; + this.createdOn = Timestamp.valueOf((LocalDateTime) tuples[aliasToIndexMap.get(CREATED_ON)]); + this.votes = (int) tuples[aliasToIndexMap.get(VOTES)]; + } + + public Long getId() { + return id; + } + + public Long getPostId() { + return postId; + } + + public Long getParentId() { + return parentId; + } + + public String getReview() { + return review; + } + + public Date getCreatedOn() { + return createdOn; + } + + public int getVotes() { + return votes; + } + + public List getReplies() { + return replies; + } + + public void addReply(PostCommentDTO reply) { + replies.add(reply); + reply.parent = this; + } + + public PostCommentDTO getParent() { + return parent; + } + + public PostCommentDTO root() { + return (parent != null) ? parent.root() : this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/domain/Tag.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/domain/Tag.java new file mode 100644 index 000000000..509d39cd1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/domain/Tag.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.spring.data.query.specification.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Tag") +@Table(name = "tag") +public class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/repository/PostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/repository/PostCommentRepository.java new file mode 100644 index 000000000..804d18c09 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/repository/PostCommentRepository.java @@ -0,0 +1,48 @@ +package com.vladmihalcea.hpjp.spring.data.query.specification.repository; + +import com.vladmihalcea.hpjp.spring.data.query.specification.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.specification.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.query.specification.domain.PostComment_; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostCommentRepository + extends BaseJpaRepository, + JpaSpecificationExecutor { + + interface Specs { + + static Specification byPost(Post post) { + return (root, query, builder) -> + builder.equal(root.get(PostComment_.post), post); + } + + static Specification byStatus(PostComment.Status status) { + return (root, query, builder) -> + builder.equal(root.get(PostComment_.status), status); + } + + static Specification byReviewLike(String reviewPattern) { + return (root, query, builder) -> + builder.like(root.get(PostComment_.review), reviewPattern); + } + + static Specification byVotesGreaterThanEqual(int votes) { + return (root, query, builder) -> + builder.greaterThanOrEqualTo(root.get(PostComment_.votes), votes); + } + + static Specification orderByCreatedOn(Specification spec) { + return (root, query, builder) -> { + query.orderBy(builder.asc(root.get(PostComment_.createdOn))); + return spec.toPredicate(root, query, builder); + }; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/repository/PostRepository.java new file mode 100644 index 000000000..9474dc479 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/specification/repository/PostRepository.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.data.query.specification.repository; + +import com.vladmihalcea.hpjp.spring.data.query.specification.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/SpringDataJPAWindowTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/SpringDataJPAWindowTest.java new file mode 100644 index 000000000..1ea0561e8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/SpringDataJPAWindowTest.java @@ -0,0 +1,109 @@ +package com.vladmihalcea.hpjp.spring.data.query.window; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.query.window.config.SpringDataJPAWindowConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.window.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.window.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.query.window.domain.PostComment_; +import com.vladmihalcea.hpjp.spring.data.query.window.repository.PostCommentRepository; +import com.vladmihalcea.hpjp.spring.data.query.window.repository.PostRepository; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Sort; +import org.springframework.data.support.WindowIterator; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.time.LocalDateTime; + + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAWindowConfiguration.class) +public class SpringDataJPAWindowTest extends AbstractSpringTest { + + public static final int POST_COMMENT_COUNT = 30; + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + LocalDateTime timestamp = LocalDateTime.of( + 2024, 9, 26, 4, 0, 0, 0 + ); + + long commentId = 1; + + Post post = new Post() + .setId(1L) + .setTitle("Post nr. 1"); + + for (long i = 1; i <= POST_COMMENT_COUNT; i++) { + post.addComment( + new PostComment() + .setId(commentId++) + .setReview(String.format("Awesome post %d", i)) + .setStatus(PostComment.Status.PENDING) + .setCreatedOn(timestamp.plusHours(commentId)) + .setVotes((int) (i % 7)) + ); + } + + entityManager.persist(post); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void testWindowIterator() { + Post post = postRepository.getReferenceById(1L); + + int pageSize = 10; + + WindowIterator commentWindowIterator = WindowIterator.of( + position -> postCommentRepository.findByPost( + post, + PageRequest.of( + 0, + pageSize, + Sort.by( + Sort.Order.desc(PostComment_.CREATED_ON), + Sort.Order.desc(PostComment_.ID) + ) + ), + position + ) + ).startingAt(ScrollPosition.keyset()); + + commentWindowIterator.forEachRemaining( + comment -> LOGGER.info( + "Post comment {} created at {}", + comment.getReview(), + comment.getCreatedOn() + ) + ); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/config/SpringDataJPAWindowConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/config/SpringDataJPAWindowConfiguration.java new file mode 100644 index 000000000..4018fb7aa --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/config/SpringDataJPAWindowConfiguration.java @@ -0,0 +1,35 @@ +package com.vladmihalcea.hpjp.spring.data.query.window.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.query.window.domain.Post; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.query.window", + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.query.window.repository" +) +public class SpringDataJPAWindowConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/domain/Post.java new file mode 100644 index 000000000..a2bd9fd07 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/domain/Post.java @@ -0,0 +1,56 @@ +package com.vladmihalcea.hpjp.spring.data.query.window.domain; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/domain/PostComment.java new file mode 100644 index 000000000..fa460a1a8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/domain/PostComment.java @@ -0,0 +1,101 @@ +package com.vladmihalcea.hpjp.spring.data.query.window.domain; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post_comment") +public class PostComment { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + private PostComment parent; + + private String review; + + @Enumerated(EnumType.ORDINAL) + private Status status; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + private int votes; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public PostComment getParent() { + return parent; + } + + public PostComment setParent(PostComment parent) { + this.parent = parent; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Status getStatus() { + return status; + } + + public PostComment setStatus(Status status) { + this.status = status; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public int getVotes() { + return votes; + } + + public PostComment setVotes(int votes) { + this.votes = votes; + return this; + } + + public enum Status { + PENDING, + APPROVED, + SPAM; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/repository/PostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/repository/PostCommentRepository.java new file mode 100644 index 000000000..993af0199 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/repository/PostCommentRepository.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.spring.data.query.window.repository; + +import com.vladmihalcea.hpjp.spring.data.query.window.domain.Post; +import com.vladmihalcea.hpjp.spring.data.query.window.domain.PostComment; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostCommentRepository extends JpaRepository { + + Window findByPost( + Post post, + Pageable pageable, + ScrollPosition position + ); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/repository/PostRepository.java new file mode 100644 index 000000000..ec7b60dc0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/query/window/repository/PostRepository.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.data.query.window.repository; + +import com.vladmihalcea.hpjp.spring.data.query.window.domain.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends JpaRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/SpringDataJPARecordTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/SpringDataJPARecordTest.java new file mode 100644 index 000000000..863f34a86 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/SpringDataJPARecordTest.java @@ -0,0 +1,80 @@ +package com.vladmihalcea.hpjp.spring.data.record; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.record.config.SpringDataJPARecordConfiguration; +import com.vladmihalcea.hpjp.spring.data.record.domain.Post; +import com.vladmihalcea.hpjp.spring.data.record.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.record.domain.PostCommentRecord; +import com.vladmihalcea.hpjp.spring.data.record.domain.PostRecord; +import com.vladmihalcea.hpjp.spring.data.record.service.ForumService; +import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; + +import java.util.stream.LongStream; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPARecordConfiguration.class) +public class SpringDataJPARecordTest extends AbstractSpringTest { + + public static final int POST_COMMENT_COUNT = 5; + + @Autowired + private ForumService forumService; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class + }; + } + + @Override + public void afterInit() { + PostRecord postRecord = new PostRecord( + 1L, + "High-Performance Java Persistence", + LongStream.rangeClosed(1, POST_COMMENT_COUNT).mapToObj(i -> + new PostCommentRecord( + null, + String.format("Good review nr. %d", i) + ) + ).toList() + ); + + forumService.insertPostRecord(postRecord); + } + + @Test + public void test() { + PostRecord postRecord = forumService.findPostRecordById(1L); + + LOGGER.info("PostRecord to JSON: {}", JacksonUtil.toString(postRecord)); + + String upatedPostRecordJSONSTring = """ + { + "id": 1, + "title": "High-Performance Java Persistence, 2nd edition", + "comments": [ + { + "id": 1, + "review": "Best book on JPA and Hibernate!" + }, + { + "id": 2, + "review": "A must-read for every Java developer!" + } + ] + } + """; + + forumService.mergePostRecord( + JacksonUtil.fromString(upatedPostRecordJSONSTring, PostRecord.class) + ); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/config/SpringDataJPARecordConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/config/SpringDataJPARecordConfiguration.java new file mode 100644 index 000000000..adcb0c167 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/config/SpringDataJPARecordConfiguration.java @@ -0,0 +1,44 @@ +package com.vladmihalcea.hpjp.spring.data.record.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.record.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.hibernate.cfg.AvailableSettings; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.record", + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.record.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPARecordConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put( + AvailableSettings.STATEMENT_BATCH_SIZE, + 50 + ); + properties.put( + AvailableSettings.ORDER_INSERTS, + Boolean.TRUE + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/domain/Post.java new file mode 100644 index 000000000..9c8d0aa7c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/domain/Post.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.spring.data.record.domain; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + return this; + } + + public PostRecord toRecord() { + return new PostRecord( + id, + title, + comments.stream().map(comment -> + new PostCommentRecord( + comment.getId(), + comment.getReview() + ) + ).toList() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/domain/PostComment.java new file mode 100644 index 000000000..539ad35f1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/domain/PostComment.java @@ -0,0 +1,47 @@ +package com.vladmihalcea.hpjp.spring.data.record.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +public class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/domain/PostCommentRecord.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/domain/PostCommentRecord.java new file mode 100644 index 000000000..276a33eba --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/domain/PostCommentRecord.java @@ -0,0 +1,15 @@ +package com.vladmihalcea.hpjp.spring.data.record.domain; + +/** + * @author Vlad Mihalcea + */ +public record PostCommentRecord( + Long id, + String review +) { + public PostComment toPostComment() { + return new PostComment() + .setId(id) + .setReview(review); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/domain/PostRecord.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/domain/PostRecord.java new file mode 100644 index 000000000..e7c21d76c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/domain/PostRecord.java @@ -0,0 +1,20 @@ +package com.vladmihalcea.hpjp.spring.data.record.domain; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public record PostRecord( + Long id, + String title, + List comments +) { + public Post toPost() { + Post post = new Post() + .setId(id) + .setTitle(title); + comments.forEach(comment -> post.addComment(comment.toPostComment())); + return post; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/repository/PostRepository.java new file mode 100644 index 000000000..9e6970d16 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/repository/PostRepository.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.spring.data.record.repository; + +import com.vladmihalcea.hpjp.spring.data.record.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { + + @Query(""" + select p + from Post p + join fetch p.comments + where p.id = :postId + """) + Optional findWithCommentsById(@Param("postId") Long postId); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/service/ForumService.java new file mode 100644 index 000000000..5e6d6cdba --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/record/service/ForumService.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.spring.data.record.service; + +import com.vladmihalcea.hpjp.spring.data.record.domain.Post; +import com.vladmihalcea.hpjp.spring.data.record.domain.PostRecord; +import com.vladmihalcea.hpjp.spring.data.record.repository.PostRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumService { + + @Autowired + private PostRepository postRepository; + + public PostRecord findPostRecordById(Long postId) { + return postRepository + .findWithCommentsById(postId) + .map(Post::toRecord) + .orElse(null); + } + + @Transactional + public PostRecord insertPostRecord(PostRecord postRecord) { + return postRepository.persist(postRecord.toPost()).toRecord(); + } + + @Transactional + public PostRecord mergePostRecord(PostRecord postRecord) { + return postRepository.merge(postRecord.toPost()).toRecord(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/SpringDataJPARecursiveTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/SpringDataJPARecursiveTest.java new file mode 100644 index 000000000..cb5aa04f4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/SpringDataJPARecursiveTest.java @@ -0,0 +1,175 @@ +package com.vladmihalcea.hpjp.spring.data.recursive; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.recursive.config.SpringDataJPARecursiveConfiguration; +import com.vladmihalcea.hpjp.spring.data.recursive.domain.Post; +import com.vladmihalcea.hpjp.spring.data.recursive.domain.PostComment; +import com.vladmihalcea.hpjp.spring.data.recursive.domain.PostCommentDTO; +import com.vladmihalcea.hpjp.spring.data.recursive.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.data.recursive.service.ForumService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPARecursiveConfiguration.class) +public class SpringDataJPARecursiveTest extends AbstractSpringTest { + + public static final int POST_COUNT = 50; + public static final int PAGE_SIZE = 25; + public static final int TOP_N_HIERARCHY = 3; + + @Autowired + private PostRepository postRepository; + + @Autowired + private ForumService forumService; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Post post = new Post() + .setId(1L) + .setTitle("Post 1"); + + PostComment comment1 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 10, 13, 12, 23, 5))) + .setScore(1) + .setReview("Comment 1"); + + PostComment comment1_1 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 10, 14, 13, 23, 10))) + .setScore(2) + .setReview("Comment 1.1") + .setParent(comment1); + + PostComment comment1_2 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 10, 14, 15, 45, 15))) + .setScore(2) + .setParent(comment1) + .setReview("Comment 1.2"); + + PostComment comment1_2_1 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 10, 15, 10, 15, 20))) + .setScore(1) + .setReview("Comment 1.2.1") + .setParent(comment1_2); + + PostComment comment2 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 10, 13, 15, 23, 25))) + .setScore(1) + .setReview("Comment 2"); + + PostComment comment2_1 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 10, 14, 11, 23, 30))) + .setScore(1) + .setReview("Comment 2.1") + .setParent(comment2); + + PostComment comment2_2 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 10, 14, 14, 45, 35))) + .setScore(1) + .setReview("Comment 2.2") + .setParent(comment2); + + PostComment comment3 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 10, 15, 10, 15, 40))) + .setScore(1) + .setReview("Comment 3"); + + PostComment comment3_1 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 10, 16, 11, 15, 45))) + .setScore(10) + .setReview("Comment 3.1") + .setParent(comment3); + + PostComment comment3_2 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 10, 17, 18, 30, 50))) + .setScore(-2) + .setReview("Comment 3.2") + .setParent(comment3); + + PostComment comment4 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 10, 19, 21, 43, 55))) + .setReview("Comment 4") + .setScore(-5); + + PostComment comment5 = new PostComment() + .setPost(post) + .setCreatedOn(Timestamp.valueOf(LocalDateTime.of(2023, 10, 22, 23, 45, 0))) + .setReview("Comment 5"); + + entityManager.persist(post); + entityManager.persist(comment1); + entityManager.persist(comment1_1); + entityManager.persist(comment1_2); + entityManager.persist(comment1_2_1); + entityManager.persist(comment2); + entityManager.persist(comment2_1); + entityManager.persist(comment2_2); + entityManager.persist(comment3); + entityManager.persist(comment3_1); + entityManager.persist(comment3_2); + entityManager.persist(comment4); + entityManager.persist(comment5); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void testAggregateUsingJava() { + List postCommentRoots = forumService.findTopCommentHierarchiesByPostUsingJava(1L, TOP_N_HIERARCHY); + + assertEquals(3, postCommentRoots.size()); + + assertEquals(9, postCommentRoots.get(0).getTotalScore()); + assertEquals(6, postCommentRoots.get(1).getTotalScore()); + assertEquals(3, postCommentRoots.get(2).getTotalScore()); + } + + @Test + public void testAggregateUsingSQL() { + List postCommentRoots = forumService.findTopCommentHierarchiesByPostUsingSQL(1L, TOP_N_HIERARCHY); + + assertEquals(3, postCommentRoots.size()); + + assertEquals(9, postCommentRoots.get(0).getTotalScore()); + assertEquals(6, postCommentRoots.get(1).getTotalScore()); + assertEquals(3, postCommentRoots.get(2).getTotalScore()); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/config/SpringDataJPARecursiveConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/config/SpringDataJPARecursiveConfiguration.java new file mode 100644 index 000000000..97f179d3e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/config/SpringDataJPARecursiveConfiguration.java @@ -0,0 +1,52 @@ +package com.vladmihalcea.hpjp.spring.data.recursive.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.recursive.domain.Post; +import com.vladmihalcea.hpjp.spring.data.recursive.domain.PostCommentDTO; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.recursive", + } +) +@EnableJpaRepositories( + value = "com.vladmihalcea.hpjp.spring.data.recursive.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPARecursiveConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator( + List.of( + PostCommentDTO.class + ) + ) + ) + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/domain/Post.java new file mode 100644 index 000000000..d35b798ef --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/domain/Post.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.spring.data.recursive.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/domain/PostComment.java new file mode 100644 index 000000000..e780449af --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/domain/PostComment.java @@ -0,0 +1,100 @@ +package com.vladmihalcea.hpjp.spring.data.recursive.domain; + +import jakarta.persistence.*; + +import java.util.Date; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "PostComment") +@Table(name = "post_comment") +@SqlResultSetMapping( + name = "PostCommentDTO", + classes = @ConstructorResult( + targetClass = PostCommentDTO.class, + columns = { + @ColumnResult(name = "id"), + @ColumnResult(name = "parent_id"), + @ColumnResult(name = "review"), + @ColumnResult(name = "created_on"), + @ColumnResult(name = "score") + } + ) +) +public class PostComment { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne + @JoinColumn(name = "parent_id") + private PostComment parent; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn; + + private String review; + + private int score; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public PostComment getParent() { + return parent; + } + + public PostComment setParent(PostComment parent) { + this.parent = parent; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Date getCreatedOn() { + return createdOn; + } + + public PostComment setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + return this; + } + + public int getScore() { + return score; + } + + public PostComment setScore(int score) { + this.score = score; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/domain/PostCommentDTO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/domain/PostCommentDTO.java new file mode 100644 index 000000000..44acdcc81 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/domain/PostCommentDTO.java @@ -0,0 +1,76 @@ +package com.vladmihalcea.hpjp.spring.data.recursive.domain; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentDTO { + + private Long id; + private Long parentId; + private String review; + private Date createdOn; + private long score; + + private PostCommentDTO parent; + + private List children = new ArrayList<>(); + + public PostCommentDTO(Number id, Number parentId, String review, Date createdOn, Number score) { + this.id = id.longValue(); + this.parentId = parentId != null ? parentId.longValue() : null; + this.review = review; + this.createdOn = createdOn; + this.score = score.longValue(); + } + + public Long getId() { + return id; + } + + public Long getParentId() { + return parentId; + } + + public String getReview() { + return review; + } + + public Date getCreatedOn() { + return createdOn; + } + + public long getScore() { + return score; + } + + public long getTotalScore() { + long total = getScore(); + for (PostCommentDTO child : children) { + total += child.getTotalScore(); + } + return total; + } + + public List getChildren() { + List copy = new ArrayList<>(children); + copy.sort(Comparator.comparing(PostCommentDTO::getCreatedOn)); + return copy; + } + + public void addChild(PostCommentDTO child) { + children.add(child); + child.parent = this; + } + + public PostCommentDTO getRoot() { + if(parent != null) { + return parent.getRoot(); + } + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/repository/CustomPostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/repository/CustomPostRepository.java new file mode 100644 index 000000000..1784c02ea --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/repository/CustomPostRepository.java @@ -0,0 +1,14 @@ +package com.vladmihalcea.hpjp.spring.data.recursive.repository; + +import com.vladmihalcea.hpjp.spring.data.recursive.domain.PostCommentDTO; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface CustomPostRepository { + + List findTopCommentDTOsByPost(@Param("postId") Long postId, int ranking); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/repository/CustomPostRepositoryImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/repository/CustomPostRepositoryImpl.java new file mode 100644 index 000000000..fb9fc992c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/repository/CustomPostRepositoryImpl.java @@ -0,0 +1,85 @@ +package com.vladmihalcea.hpjp.spring.data.recursive.repository; + +import com.vladmihalcea.hpjp.hibernate.query.dto.projection.transformer.DistinctListTransformer; +import com.vladmihalcea.hpjp.spring.data.recursive.domain.PostCommentDTO; +import jakarta.persistence.EntityManager; +import org.hibernate.query.NativeQuery; +import org.hibernate.query.TupleTransformer; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class CustomPostRepositoryImpl implements CustomPostRepository { + + private final EntityManager entityManager; + + public CustomPostRepositoryImpl( + EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + public List findTopCommentDTOsByPost(Long postId, int ranking) { + return entityManager.createNativeQuery(""" + SELECT id, parent_id, review, created_on, score, total_score + FROM ( + SELECT + id, parent_id, review, created_on, score, total_score, + DENSE_RANK() OVER (ORDER BY total_score DESC) AS ranking + FROM ( + SELECT + id, parent_id, review, created_on, score, + SUM(score) OVER (PARTITION BY root_id) AS total_score + FROM ( + WITH RECURSIVE post_comment_score( + id, root_id, post_id, parent_id, review, created_on, score) + AS ( + SELECT + id, id, post_id, parent_id, review, created_on, score + FROM post_comment + WHERE post_id = :postId AND parent_id IS NULL + UNION ALL + SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, + pc.review, pc.created_on, pc.score + FROM post_comment pc + INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id + ) + SELECT id, parent_id, root_id, review, created_on, score + FROM post_comment_score + ) total_score_comment + ) total_score_ranking + ) total_score_filtering + WHERE ranking <= :ranking + ORDER BY total_score DESC, id ASC + """, PostCommentDTO.class.getSimpleName()) + .unwrap(NativeQuery.class) + .setParameter("postId", 1L) + .setParameter("ranking", ranking) + .setTupleTransformer(new PostCommentScoreTupleTransformer()) + .setResultListTransformer(DistinctListTransformer.INSTANCE) + .getResultList(); + } + + public static class PostCommentScoreTupleTransformer implements TupleTransformer { + + private Map postCommentScoreMap = new HashMap<>(); + + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + PostCommentDTO commentScore = (PostCommentDTO) tuple[0]; + Long parentId = commentScore.getParentId(); + if (parentId != null) { + PostCommentDTO parent = postCommentScoreMap.get(parentId); + if (parent != null) { + parent.addChild(commentScore); + } + } + postCommentScoreMap.putIfAbsent(commentScore.getId(), commentScore); + return commentScore.getRoot(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/repository/PostRepository.java new file mode 100644 index 000000000..b640a8e02 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/repository/PostRepository.java @@ -0,0 +1,32 @@ +package com.vladmihalcea.hpjp.spring.data.recursive.repository; + +import com.vladmihalcea.hpjp.spring.data.recursive.domain.Post; +import com.vladmihalcea.hpjp.spring.data.recursive.domain.PostCommentDTO; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository, CustomPostRepository { + + @Query(value = """ + select new PostCommentDTO( + id, + parent.id, + review, + createdOn, + score + ) + from PostComment pc + where post.id = :postId + order by id + """ + ) + List findAllCommentDTOsByPost(@Param("postId") Long postId); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/service/ForumService.java new file mode 100644 index 000000000..ba9e21d45 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/recursive/service/ForumService.java @@ -0,0 +1,53 @@ +package com.vladmihalcea.hpjp.spring.data.recursive.service; + +import com.vladmihalcea.hpjp.spring.data.recursive.domain.PostCommentDTO; +import com.vladmihalcea.hpjp.spring.data.recursive.repository.PostRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumService { + + @Autowired + private PostRepository postRepository; + + public List findTopCommentHierarchiesByPostUsingJava(Long postId, int ranking) { + List postComments = postRepository.findAllCommentDTOsByPost(postId); + + Map postCommentMap = postComments + .stream() + .collect(Collectors.toMap(PostCommentDTO::getId, Function.identity())); + + List postCommentRoots = postComments + .stream() + .filter(pcs -> { + boolean isRoot = pcs.getParentId() == null; + if(!isRoot) { + postCommentMap.get(pcs.getParentId()).addChild(pcs); + } + return isRoot; + }) + .sorted( + Comparator.comparing(PostCommentDTO::getTotalScore).reversed() + ) + .limit(ranking) + .toList(); + + return postCommentRoots; + } + + public List findTopCommentHierarchiesByPostUsingSQL(Long postId, int ranking) { + return postRepository.findTopCommentDTOsByPost(postId, ranking); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/SpringDataJPAUnidirectionalBulkTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/SpringDataJPAUnidirectionalBulkTest.java new file mode 100644 index 000000000..c8e72c40e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/SpringDataJPAUnidirectionalBulkTest.java @@ -0,0 +1,155 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.unidirectional.config.SpringDataJPAUnidirectionalConfiguration; +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.*; +import com.vladmihalcea.hpjp.spring.data.unidirectional.repository.*; +import com.vladmihalcea.hpjp.spring.data.unidirectional.service.ForumService; +import com.vladmihalcea.hpjp.util.exception.ExceptionUtil; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.support.TransactionCallback; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAUnidirectionalConfiguration.class) +public class SpringDataJPAUnidirectionalBulkTest extends AbstractSpringTest { + + @Autowired + private PostRepository postRepository; + + @Autowired + private DefaultPostRepository defaultPostRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserVoteRepository userVoteRepository; + + @Autowired + private PostDetailsRepository postDetailsRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @Autowired + private TagRepository tagRepository; + + @Autowired + private PostTagRepository postTagRepository; + + @Autowired + private ForumService forumService; + + @Override + protected Class[] entities() { + return new Class[]{ + UserVote.class, + PostComment.class, + PostDetails.class, + PostTag.class, + Post.class, + Tag.class, + User.class + }; + } + + @Override + public void afterInit() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + User alice = new User() + .setId(1L) + .setFirstName("Alice") + .setLastName("Smith"); + + User bob = new User() + .setId(2L) + .setFirstName("Bob") + .setLastName("Johnson"); + + userRepository.persist(alice); + userRepository.persist(bob); + + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + postRepository.persist(post); + + postDetailsRepository.persist( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + .setPost(post) + ); + + PostComment comment1 = new PostComment() + .setReview("Best book on JPA and Hibernate!") + .setPost(post); + + PostComment comment2 = new PostComment() + .setReview("A must-read for every Java developer!") + .setPost(post); + + postCommentRepository.persist(comment1); + postCommentRepository.persist(comment2); + + userVoteRepository.persist( + new UserVote() + .setUser(alice) + .setComment(comment1) + .setScore(Math.random() > 0.5 ? 1 : -1) + ); + + userVoteRepository.persist( + new UserVote() + .setUser(bob) + .setComment(comment2) + .setScore(Math.random() > 0.5 ? 1 : -1) + ); + + Tag jdbc = new Tag().setName("JDBC"); + Tag hibernate = new Tag().setName("Hibernate"); + Tag jOOQ = new Tag().setName("jOOQ"); + + tagRepository.persist(jdbc); + tagRepository.persist(hibernate); + tagRepository.persist(jOOQ); + + postTagRepository.persist(new PostTag(post, jdbc)); + postTagRepository.persist(new PostTag(post, hibernate)); + postTagRepository.persist(new PostTag(post, jOOQ)); + + return null; + }); + } + + @Test + public void testDefaultDeleteById() { + try { + final JpaRepository postRepository = defaultPostRepository; + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + postRepository.deleteById(1L); + + return null; + }); + + fail("Should have thrown ConstraintViolationException"); + } catch (Exception e) { + LOGGER.info("Expected", e); + assertTrue(ExceptionUtil.isCausedBy(e, ConstraintViolationException.class)); + } + } + + @Test + public void testDeleteWithBulk() { + forumService.deletePostById(1L); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/SpringDataJPAUnidirectionalEventListenerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/SpringDataJPAUnidirectionalEventListenerTest.java new file mode 100644 index 000000000..8334705e1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/SpringDataJPAUnidirectionalEventListenerTest.java @@ -0,0 +1,130 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.unidirectional.config.SpringDataJPAUnidirectionalEventListenerConfiguration; +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.*; +import com.vladmihalcea.hpjp.spring.data.unidirectional.repository.*; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.support.TransactionCallback; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAUnidirectionalEventListenerConfiguration.class) +public class SpringDataJPAUnidirectionalEventListenerTest extends AbstractSpringTest { + + @Autowired + private PostRepository postRepository; + + @Autowired + private DefaultPostRepository defaultPostRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserVoteRepository userVoteRepository; + + @Autowired + private PostDetailsRepository postDetailsRepository; + + @Autowired + private PostCommentRepository postCommentRepository; + + @Autowired + private TagRepository tagRepository; + + @Autowired + private PostTagRepository postTagRepository; + + @Override + protected Class[] entities() { + return new Class[]{ + UserVote.class, + PostComment.class, + PostDetails.class, + PostTag.class, + Post.class, + Tag.class, + User.class + }; + } + + @Override + public void afterInit() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + User alice = new User() + .setId(1L) + .setFirstName("Alice") + .setLastName("Smith"); + + User bob = new User() + .setId(2L) + .setFirstName("Bob") + .setLastName("Johnson"); + + userRepository.persist(alice); + userRepository.persist(bob); + + Post post = new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence"); + postRepository.persist(post); + + postDetailsRepository.persist( + new PostDetails() + .setCreatedBy("Vlad Mihalcea") + .setPost(post) + ); + + PostComment comment1 = new PostComment() + .setReview("Best book on JPA and Hibernate!") + .setPost(post); + + PostComment comment2 = new PostComment() + .setReview("A must-read for every Java developer!") + .setPost(post); + + postCommentRepository.persist(comment1); + postCommentRepository.persist(comment2); + + userVoteRepository.persist( + new UserVote() + .setUser(alice) + .setComment(comment1) + .setScore(Math.random() > 0.5 ? 1 : -1) + ); + + userVoteRepository.persist( + new UserVote() + .setUser(bob) + .setComment(comment2) + .setScore(Math.random() > 0.5 ? 1 : -1) + ); + + Tag jdbc = new Tag().setName("JDBC"); + Tag hibernate = new Tag().setName("Hibernate"); + Tag jOOQ = new Tag().setName("jOOQ"); + + tagRepository.persist(jdbc); + tagRepository.persist(hibernate); + tagRepository.persist(jOOQ); + + postTagRepository.persist(new PostTag(post, jdbc)); + postTagRepository.persist(new PostTag(post, hibernate)); + postTagRepository.persist(new PostTag(post, jOOQ)); + + return null; + }); + } + + @Test + public void testDeleteWithEventListener() { + final JpaRepository postRepository = defaultPostRepository; + postRepository.deleteById(1L); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/config/SpringDataJPAUnidirectionalConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/config/SpringDataJPAUnidirectionalConfiguration.java new file mode 100644 index 000000000..6aaf0c853 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/config/SpringDataJPAUnidirectionalConfiguration.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.unidirectional" + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.unidirectional.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPAUnidirectionalConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/config/SpringDataJPAUnidirectionalEventListenerConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/config/SpringDataJPAUnidirectionalEventListenerConfiguration.java new file mode 100644 index 000000000..509a14377 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/config/SpringDataJPAUnidirectionalEventListenerConfiguration.java @@ -0,0 +1,38 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.config; + +import com.vladmihalcea.hpjp.spring.data.unidirectional.event.CascadeDeleteEventListenerIntegrator; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.List; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.unidirectional" + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.unidirectional.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPAUnidirectionalEventListenerConfiguration extends SpringDataJPAUnidirectionalConfiguration { + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put( + EntityManagerFactoryBuilderImpl.INTEGRATOR_PROVIDER, + (IntegratorProvider) () -> List.of( + CascadeDeleteEventListenerIntegrator.INSTANCE + ) + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/Post.java new file mode 100644 index 000000000..f8dce1c2b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/Post.java @@ -0,0 +1,38 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "posts") +public class Post extends VersionedEntity { + + @Id + private Long id; + + @Column(length = 250) + private String title; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/PostComment.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/PostComment.java new file mode 100644 index 000000000..8f93310ea --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/PostComment.java @@ -0,0 +1,53 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post_comments") +public class PostComment extends VersionedEntity { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn( + foreignKey = @ForeignKey( + name = "FK_post_comment_post_id" + ) + ) + private Post post; + + @Column(length = 250) + private String review; + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/PostDetails.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/PostDetails.java new file mode 100644 index 000000000..13ce70a11 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/PostDetails.java @@ -0,0 +1,72 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.domain; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post_details") +public class PostDetails extends VersionedEntity { + + @Id + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn( + name = "id", + foreignKey = @ForeignKey( + name = "FK_post_details_id" + ) + ) + private Post post; + + @Column(name = "created_on") + private LocalDateTime createdOn; + + @Column(name = "created_by") + private String createdBy; + + public Long getId() { + return id; + } + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public Post getPost() { + return post; + } + + public PostDetails setPost(Post post) { + this.post = post; + this.id = post.getId(); + if (getVersion() == null) { + setVersion(post.getVersion()); + } + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public PostDetails setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/PostTag.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/PostTag.java new file mode 100644 index 000000000..04215b899 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/PostTag.java @@ -0,0 +1,83 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.domain; + +import jakarta.persistence.*; + +import java.util.Date; +import java.util.Objects; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post_tags") +public class PostTag extends VersionedEntity { + + @EmbeddedId + private PostTagId id; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("postId") + @JoinColumn( + foreignKey = @ForeignKey( + name = "FK_post_tag_post_id" + ) + ) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("tagId") + @JoinColumn( + foreignKey = @ForeignKey( + name = "FK_post_tag_tag_id" + ) + ) + private Tag tag; + + @Column(name = "created_on") + private Date createdOn = new Date(); + + public PostTag() {} + + public PostTag(Post post, Tag tag) { + this.post = post; + this.tag = tag; + this.id = new PostTagId(post.getId(), tag.getId()); + } + + public PostTagId getId() { + return id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Tag getTag() { + return tag; + } + + public void setTag(Tag tag) { + this.tag = tag; + } + + public Date getCreatedOn() { + return createdOn; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTag that = (PostTag) o; + return Objects.equals(this.id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(this.id); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/PostTagId.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/PostTagId.java new file mode 100644 index 000000000..ee62a6731 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/PostTagId.java @@ -0,0 +1,49 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +import java.io.Serializable; +import java.util.Objects; + +/** + * @author Vlad Mihalcea + */ +@Embeddable +public class PostTagId implements Serializable { + + @Column(name = "post_id") + private Long postId; + + @Column(name = "tag_id") + private Long tagId; + + public PostTagId() {} + + public PostTagId(Long postId, Long tagId) { + this.postId = postId; + this.tagId = tagId; + } + + public Long getPostId() { + return postId; + } + + public Long getTagId() { + return tagId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostTagId that = (PostTagId) o; + return Objects.equals(this.postId, that.getPostId()) && + Objects.equals(this.tagId, that.getTagId()); + } + + @Override + public int hashCode() { + return Objects.hash(this.postId, this.tagId); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/Tag.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/Tag.java new file mode 100644 index 000000000..526343184 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/Tag.java @@ -0,0 +1,38 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.NaturalId; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "tags") +public class Tag extends VersionedEntity { + + @Id + @GeneratedValue + private Long id; + + @NaturalId + @Column(length = 40) + private String name; + + public Long getId() { + return id; + } + + public Tag setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Tag setName(String name) { + this.name = name; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/User.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/User.java new file mode 100644 index 000000000..64488a5c0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/User.java @@ -0,0 +1,50 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "users") +public class User extends VersionedEntity { + + @Id + private Long id; + + @Column(name = "first_name", length = 50) + private String firstName; + + @Column(name = "last_name", length = 50) + private String lastName; + + public Long getId() { + return id; + } + + public User setId(Long id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public User setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public User setLastName(String lastName) { + this.lastName = lastName; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/UserVote.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/UserVote.java new file mode 100644 index 000000000..02dd5220e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/UserVote.java @@ -0,0 +1,69 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "user_votes") +public class UserVote extends VersionedEntity { + + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn( + foreignKey = @ForeignKey( + name = "FK_user_vote_user_id" + ) + ) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn( + foreignKey = @ForeignKey( + name = "FK_user_vote_comment_id" + ) + ) + private PostComment comment; + + private int score; + + public Long getId() { + return id; + } + + public UserVote setId(Long id) { + this.id = id; + return this; + } + + public User getUser() { + return user; + } + + public UserVote setUser(User user) { + this.user = user; + return this; + } + + public PostComment getComment() { + return comment; + } + + public UserVote setComment(PostComment comment) { + this.comment = comment; + return this; + } + + public int getScore() { + return score; + } + + public UserVote setScore(int score) { + this.score = score; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/VersionedEntity.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/VersionedEntity.java new file mode 100644 index 000000000..7cd1d850a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/domain/VersionedEntity.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.domain; + +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Version; + +/** + * @author Vlad Mihalcea + */ +@MappedSuperclass +public class VersionedEntity { + + @Version + private Short version; + + public Short getVersion() { + return version; + } + + public void setVersion(Short version) { + this.version = version; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/event/CascadeDeleteEventListener.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/event/CascadeDeleteEventListener.java new file mode 100644 index 000000000..8157ea97e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/event/CascadeDeleteEventListener.java @@ -0,0 +1,66 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.event; + +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.*; +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.event.spi.DeleteContext; +import org.hibernate.event.spi.DeleteEvent; +import org.hibernate.event.spi.DeleteEventListener; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class CascadeDeleteEventListener implements DeleteEventListener { + + public static final CascadeDeleteEventListener INSTANCE = new CascadeDeleteEventListener(); + + @Override + public void onDelete(DeleteEvent event) throws HibernateException { + final Object entity = event.getObject(); + Session session = event.getSession(); + + if (entity instanceof Post post) { + session.remove( + session.find(PostDetails.class, post.getId()) + ); + + session.createQuery(""" + select uv + from UserVote uv + where uv.comment.id in ( + select id + from PostComment + where post.id = :postId + ) + """, UserVote.class) + .setParameter("postId", post.getId()) + .getResultList() + .forEach(session::remove); + + session.createQuery(""" + select pc + from PostComment pc + where pc.post.id = :postId + """, PostComment.class) + .setParameter("postId", post.getId()) + .getResultList() + .forEach(session::remove); + + session.createQuery(""" + select pt + from PostTag pt + where pt.post.id = :postId + """, PostTag.class) + .setParameter("postId", post.getId()) + .getResultList() + .forEach(session::remove); + } + } + + @Override + public void onDelete(DeleteEvent event, DeleteContext transientEntities) throws HibernateException { + onDelete(event); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/event/CascadeDeleteEventListenerIntegrator.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/event/CascadeDeleteEventListenerIntegrator.java new file mode 100644 index 000000000..11e31b6d1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/event/CascadeDeleteEventListenerIntegrator.java @@ -0,0 +1,34 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.event; + +import org.hibernate.boot.Metadata; +import org.hibernate.boot.spi.BootstrapContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; + +public class CascadeDeleteEventListenerIntegrator implements Integrator { + + public static final CascadeDeleteEventListenerIntegrator INSTANCE = + new CascadeDeleteEventListenerIntegrator(); + + @Override + public void integrate(Metadata metadata, BootstrapContext bootstrapContext, SessionFactoryImplementor sessionFactory) { + final EventListenerRegistry eventListenerRegistry = sessionFactory + .getServiceRegistry() + .getService(EventListenerRegistry.class); + + eventListenerRegistry.prependListeners( + EventType.DELETE, + CascadeDeleteEventListener.INSTANCE + ); + } + + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/CustomPostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/CustomPostRepository.java new file mode 100644 index 000000000..9d0408246 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/CustomPostRepository.java @@ -0,0 +1,9 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.repository; + +/** + * @author Vlad Mihalcea + */ +public interface CustomPostRepository { + + void deleteById(ID postId); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/CustomPostRepositoryImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/CustomPostRepositoryImpl.java new file mode 100644 index 000000000..4dfdbecad --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/CustomPostRepositoryImpl.java @@ -0,0 +1,49 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.repository; + +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.Post; +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.PostDetails; +import jakarta.persistence.EntityManager; + +/** + * @author Vlad Mihalcea + */ +public class CustomPostRepositoryImpl implements CustomPostRepository { + + private final PostDetailsRepository postDetailsRepository; + + private final UserVoteRepository userVoteRepository; + + private final PostCommentRepository postCommentRepository; + + private final PostTagRepository postTagRepository; + + private final EntityManager entityManager; + + public CustomPostRepositoryImpl( + PostDetailsRepository postDetailsRepository, + UserVoteRepository userVoteRepository, + PostCommentRepository postCommentRepository, + PostTagRepository postTagRepository, + EntityManager entityManager) { + this.postDetailsRepository = postDetailsRepository; + this.userVoteRepository = userVoteRepository; + this.postCommentRepository = postCommentRepository; + this.postTagRepository = postTagRepository; + this.entityManager = entityManager; + } + + @Override + public void deleteById(Long postId) { + postDetailsRepository.deleteByPostId(postId); + userVoteRepository.deleteAllByPostId(postId); + postCommentRepository.deleteAllByPostId(postId); + postTagRepository.deleteAllByPostId(postId); + + entityManager.createQuery(""" + delete from Post + where id = :postId + """) + .setParameter("postId", postId) + .executeUpdate(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/DefaultPostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/DefaultPostRepository.java new file mode 100644 index 000000000..1d6f0c194 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/DefaultPostRepository.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.repository; + +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface DefaultPostRepository extends JpaRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/PostCommentRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/PostCommentRepository.java new file mode 100644 index 000000000..15d108506 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/PostCommentRepository.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.repository; + +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.PostComment; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostCommentRepository extends BaseJpaRepository { + + @Query(""" + delete from PostComment + where post.id = :postId + """) + @Modifying + void deleteAllByPostId(@Param("postId") Long postId); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/PostDetailsRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/PostDetailsRepository.java new file mode 100644 index 000000000..19e64315d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/PostDetailsRepository.java @@ -0,0 +1,22 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.repository; + +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.PostDetails; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostDetailsRepository extends BaseJpaRepository { + + @Query(""" + delete from PostDetails + where post.id = :postId + """) + @Modifying + void deleteByPostId(@Param("postId") Long postId); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/PostRepository.java new file mode 100644 index 000000000..9913e6336 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/PostRepository.java @@ -0,0 +1,14 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.repository; + +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository, + CustomPostRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/PostTagRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/PostTagRepository.java new file mode 100644 index 000000000..eb7e9c2a2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/PostTagRepository.java @@ -0,0 +1,23 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.repository; + +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.PostTag; +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.PostTagId; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostTagRepository extends BaseJpaRepository { + + @Query(""" + delete from PostTag + where post.id = :postId + """) + @Modifying + void deleteAllByPostId(@Param("postId") Long postId); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/TagRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/TagRepository.java new file mode 100644 index 000000000..aaae6ed30 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/TagRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.repository; + +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.Tag; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface TagRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/UserRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/UserRepository.java new file mode 100644 index 000000000..9134b81ae --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/UserRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.repository; + +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.User; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface UserRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/UserVoteRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/UserVoteRepository.java new file mode 100644 index 000000000..b8c822e76 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/repository/UserVoteRepository.java @@ -0,0 +1,26 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.repository; + +import com.vladmihalcea.hpjp.spring.data.unidirectional.domain.UserVote; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface UserVoteRepository extends BaseJpaRepository { + + @Query(""" + delete from UserVote + where comment.id in ( + select id + from PostComment + where post.id = :postId + ) + """) + @Modifying + void deleteAllByPostId(@Param("postId") Long postId); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/service/ForumService.java new file mode 100644 index 000000000..9e94c585f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/unidirectional/service/ForumService.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.spring.data.unidirectional.service; + +import com.vladmihalcea.hpjp.spring.data.unidirectional.repository.PostRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumService { + + private final PostRepository postRepository; + + public ForumService(PostRepository postRepository) { + this.postRepository = postRepository; + } + + @Transactional + public void deletePostById(Long postId) { + postRepository.deleteById(postId); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/update/SpringDataJPAUpdateTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/update/SpringDataJPAUpdateTest.java new file mode 100644 index 000000000..5dcbe3a19 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/update/SpringDataJPAUpdateTest.java @@ -0,0 +1,58 @@ +package com.vladmihalcea.hpjp.spring.data.update; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.data.update.config.SpringDataJPAUpdateConfiguration; +import com.vladmihalcea.hpjp.spring.data.update.domain.Post; +import com.vladmihalcea.hpjp.spring.data.update.repository.PostRepository; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.support.TransactionCallback; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAUpdateConfiguration.class) +public class SpringDataJPAUpdateTest extends AbstractSpringTest { + + @Autowired + private PostRepository postRepository; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testDefaultUpdate() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + postRepository.persist( + new Post() + .setId(1L) + .setTitle("High-Performance Java Persistence") + ); + + postRepository.persist( + new Post() + .setId(2L) + .setTitle("Java Persistence with Hibernate") + ); + + return null; + }); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Post post1 = postRepository.findById(1L).orElseThrow(); + post1.setTitle("High-Performance Java Persistence 2nd Edition"); + + Post post2 = postRepository.findById(2L).orElseThrow(); + post2.setLikes(12); + + postRepository.flush(); + return null; + }); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/update/config/SpringDataJPAUpdateConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/update/config/SpringDataJPAUpdateConfiguration.java new file mode 100644 index 000000000..92ccc4cae --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/update/config/SpringDataJPAUpdateConfiguration.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.data.update.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.data.update.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.data.update" + } +) +@EnableJpaRepositories( + basePackages = "com.vladmihalcea.hpjp.spring.data.update.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +public class SpringDataJPAUpdateConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Post.class.getPackageName(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put("hibernate.jdbc.batch_size", "100"); + properties.put("hibernate.order_inserts", "true"); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/update/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/update/domain/Post.java new file mode 100644 index 000000000..d1d1ba8d9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/update/domain/Post.java @@ -0,0 +1,84 @@ +package com.vladmihalcea.hpjp.spring.data.update.domain; + +import jakarta.persistence.*; + +import java.sql.Timestamp; +import java.time.format.DateTimeFormatter; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Post") +@Table(name = "post") +public class Post { + + @Id + private Long id; + + private String title; + + private long likes; + + @Column(name = "created_on", nullable = false, updatable = false) + private Timestamp createdOn; + + @Transient + private String creationTimestamp; + + public Post() { + this.createdOn = new Timestamp(System.currentTimeMillis()); + } + + public String getCreationTimestamp() { + if(creationTimestamp == null) { + creationTimestamp = DateTimeFormatter.ISO_DATE_TIME.format( + createdOn.toLocalDateTime() + ); + } + return creationTimestamp; + } + + @Override + public String toString() { + return String.format(""" + Post{ + id=%d + title='%s' + likes=%d + creationTimestamp='%s' + }""" + , id, title, likes, getCreationTimestamp() + ); + } + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public long getLikes() { + return likes; + } + + public Post setLikes(long likes) { + this.likes = likes; + return this; + } + + public Timestamp getCreatedOn() { + return createdOn; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/data/update/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/update/repository/PostRepository.java new file mode 100644 index 000000000..161373c42 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/data/update/repository/PostRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.data.update.repository; + +import com.vladmihalcea.hpjp.spring.data.update.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/SpringTablePartitioningTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/SpringTablePartitioningTest.java new file mode 100644 index 000000000..dd0958119 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/SpringTablePartitioningTest.java @@ -0,0 +1,79 @@ +package com.vladmihalcea.hpjp.spring.partition; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.partition.config.SpringTablePartitioningConfiguration; +import com.vladmihalcea.hpjp.spring.partition.domain.Partition; +import com.vladmihalcea.hpjp.spring.partition.domain.Post; +import com.vladmihalcea.hpjp.spring.partition.domain.User; +import com.vladmihalcea.hpjp.spring.partition.repository.UserRepository; +import com.vladmihalcea.hpjp.spring.partition.service.ForumService; +import com.vladmihalcea.hpjp.spring.partition.util.UserContext; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; + +import java.util.List; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringTablePartitioningConfiguration.class) +public class SpringTablePartitioningTest extends AbstractSpringTest { + + public static final int POST_COUNT = 3; + + @Autowired + private ForumService forumService; + + @Autowired + private UserRepository userRepository; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class, + User.class + }; + } + + @Test + public void test() { + final User vlad = new User() + .setFirstName("Vlad") + .setLastName("Mihalcea") + .setPartition(Partition.EUROPE); + + userRepository.persist(vlad); + + UserContext.logIn(vlad); + + forumService.createPosts(LongStream.rangeClosed(1, POST_COUNT) + .mapToObj(postId -> new Post() + .setTitle( + String.format("High-Performance Java Persistence - Part %d", + postId + ) + ) + .setUser(vlad) + ) + .toList() + ); + + LongStream.rangeClosed(1, POST_COUNT).boxed() + .forEach(id -> { + Post post = forumService.findById(id); + if (post != null) { + LOGGER.info("Post title: {}", post.getTitle()); + } + }); + + List posts = forumService.findByIds( + LongStream.rangeClosed(1, POST_COUNT).boxed().toList() + ); + assertTrue(posts.size() <= POST_COUNT); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/config/SpringTablePartitioningConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/config/SpringTablePartitioningConfiguration.java new file mode 100644 index 000000000..50c1de59b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/config/SpringTablePartitioningConfiguration.java @@ -0,0 +1,218 @@ +package com.vladmihalcea.hpjp.spring.partition.config; + +import com.vladmihalcea.hpjp.spring.partition.domain.PartitionAware; +import com.vladmihalcea.hpjp.spring.partition.domain.User; +import com.vladmihalcea.hpjp.spring.partition.event.PartitionAwareEventListenerIntegrator; +import com.vladmihalcea.hpjp.spring.partition.util.UserContext; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import jakarta.persistence.EntityManagerFactory; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.Session; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +@EnableTransactionManagement +@EnableAspectJAutoProxy +@EnableJpaRepositories( + value = "com.vladmihalcea.hpjp.spring.partition.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +@ComponentScan( + value = { + "com.vladmihalcea.hpjp.spring.partition.service", + "io.hypersistence.utils.spring.aop" + } +) +public class SpringTablePartitioningConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + @Bean + public static PropertySourcesPlaceholderConfigurer properties() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Bean + public Database database() { + return Database.POSTGRESQL; + } + + @Bean + public DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + public DataSource poolingDataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setAutoCommit(false); + hikariConfig.setDataSource(dataSourceProvider().dataSource()); + return new HikariDataSource(hikariConfig); + } + + @Bean + public DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + DataSource dataSource = ProxyDataSourceBuilder + .create(poolingDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + + try(Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + connection.setAutoCommit(true); + createDatabaseSchema(statement); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + return dataSource; + } + + private void createDatabaseSchema(Statement statement) throws SQLException { + executeStatement(statement, "DROP TABLE IF EXISTS posts cascade"); + executeStatement(statement, "DROP TABLE IF EXISTS users cascade"); + executeStatement(statement, "DROP SEQUENCE IF EXISTS posts_seq"); + executeStatement(statement, "DROP SEQUENCE IF EXISTS users_seq"); + + executeStatement(statement, "CREATE SEQUENCE posts_seq START WITH 1 INCREMENT BY 50"); + executeStatement(statement, "CREATE SEQUENCE users_seq START WITH 1 INCREMENT BY 50"); + + executeStatement(statement, """ + CREATE TABLE users ( + id bigint NOT NULL, + first_name varchar(255), + last_name varchar(255), + registered_on timestamp(6), + partition_key varchar(255), + PRIMARY KEY (id, partition_key) + ) PARTITION BY LIST (partition_key) + """); + executeStatement(statement, "CREATE TABLE users_asia PARTITION OF users FOR VALUES IN ('Asia')"); + executeStatement(statement, "CREATE TABLE users_africa PARTITION OF users FOR VALUES IN ('Africa')"); + executeStatement(statement, "CREATE TABLE users_north_america PARTITION OF users FOR VALUES IN ('North America')"); + executeStatement(statement, "CREATE TABLE users_south_america PARTITION OF users FOR VALUES IN ('South America')"); + executeStatement(statement, "CREATE TABLE users_europe PARTITION OF users FOR VALUES IN ('Europe')"); + executeStatement(statement, "CREATE TABLE users_australia PARTITION OF users FOR VALUES IN ('Australia')"); + + executeStatement(statement, """ + CREATE TABLE posts ( + id bigint NOT NULL, + title varchar(255), + created_on timestamp(6), + user_id bigint, + partition_key varchar(255), + PRIMARY KEY (id, partition_key) + ) PARTITION BY LIST (partition_key) + """); + executeStatement(statement, "CREATE TABLE posts_asia PARTITION OF posts FOR VALUES IN ('Asia')"); + executeStatement(statement, "CREATE TABLE posts_africa PARTITION OF posts FOR VALUES IN ('Africa')"); + executeStatement(statement, "CREATE TABLE posts_north_america PARTITION OF posts FOR VALUES IN ('North America')"); + executeStatement(statement, "CREATE TABLE posts_south_america PARTITION OF posts FOR VALUES IN ('South America')"); + executeStatement(statement, "CREATE TABLE posts_europe PARTITION OF posts FOR VALUES IN ('Europe')"); + executeStatement(statement, "CREATE TABLE posts_australia PARTITION OF posts FOR VALUES IN ('Australia')"); + + executeStatement(statement, """ + ALTER TABLE IF EXISTS posts + ADD CONSTRAINT fk_posts_user_id FOREIGN KEY (user_id, partition_key) REFERENCES users + """); + } + + private void executeStatement(Statement statement, String sql) throws SQLException { + statement.executeUpdate(sql); + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory( + @Autowired DataSource dataSource) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setPackagesToScan(packagesToScan()); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + entityManagerFactoryBean.setJpaProperties(additionalProperties()); + return entityManagerFactoryBean; + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + transactionManager.setEntityManagerInitializer(entityManager -> { + User user = UserContext.getCurrent(); + if (user != null) { + entityManager.unwrap(Session.class) + .enableFilter(PartitionAware.PARTITION_KEY) + .setParameter(PartitionAware.PARTITION_KEY, user.getPartitionKey()); + } + }); + return transactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { + return new TransactionTemplate(transactionManager(entityManagerFactory)); + } + + @Bean + public Integer partitionProcessingSize() { + return 100; + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.setProperty("hibernate.hbm2ddl.auto", "none"); + properties.setProperty("hibernate.jdbc.batch_size", partitionProcessingSize().toString()); + properties.setProperty("hibernate.order_inserts", "true"); + properties.setProperty("hibernate.order_updates", "true"); + properties.put( + EntityManagerFactoryBuilderImpl.INTEGRATOR_PROVIDER, + (IntegratorProvider) () -> List.of( + PartitionAwareEventListenerIntegrator.INSTANCE + ) + ); + return properties; + } + + protected String[] packagesToScan() { + return new String[]{ + "com.vladmihalcea.hpjp.spring.partition.domain" + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/domain/Partition.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/domain/Partition.java new file mode 100644 index 000000000..4b53e3df3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/domain/Partition.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.spring.partition.domain; + +/** + * @author Vlad Mihalcea + */ +public enum Partition { + ASIA("Asia"), + AFRICA("Africa"), + NORTH_AMERICA("North America"), + SOUTH_AMERICA("South America"), + EUROPE("Europe"), + AUSTRALIA("Australia"), + ; + + private final String key; + + Partition(String key) { + this.key = key; + } + + public String getKey() { + return key; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/domain/PartitionAware.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/domain/PartitionAware.java new file mode 100644 index 000000000..afe0ae65d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/domain/PartitionAware.java @@ -0,0 +1,46 @@ +package com.vladmihalcea.hpjp.spring.partition.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import org.hibernate.annotations.Filter; +import org.hibernate.annotations.FilterDef; +import org.hibernate.annotations.ParamDef; +import org.hibernate.annotations.PartitionKey; + +/** + * @author Vlad Mihalcea + */ +@MappedSuperclass +@FilterDef( + name = PartitionAware.PARTITION_KEY, + parameters = @ParamDef( + name = PartitionAware.PARTITION_KEY, + type = String.class + ) +) +@Filter( + name = PartitionAware.PARTITION_KEY, + condition = "partition_key = :partitionKey" +) +public abstract class PartitionAware { + + public static final String PARTITION_KEY = "partitionKey"; + + @Column(name = "partition_key") + @PartitionKey + private String partitionKey; + + public String getPartitionKey() { + return partitionKey; + } + + public T setPartitionKey(String partitionKey) { + this.partitionKey = partitionKey; + return (T) this; + } + + public T setPartition(Partition partition) { + this.partitionKey = partition.getKey(); + return (T) this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/domain/Post.java new file mode 100644 index 000000000..5da9434bf --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/domain/Post.java @@ -0,0 +1,64 @@ +package com.vladmihalcea.hpjp.spring.partition.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "posts") +public class Post extends PartitionAware { + + @Id + @GeneratedValue + private Long id; + + private String title; + + @Column(name = "created_on") + @CreationTimestamp + private LocalDateTime createdOn; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public Post setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public User getUser() { + return user; + } + + public Post setUser(User user) { + this.user = user; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/domain/User.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/domain/User.java new file mode 100644 index 000000000..50be7e5b4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/domain/User.java @@ -0,0 +1,64 @@ +package com.vladmihalcea.hpjp.spring.partition.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "users") +public class User extends PartitionAware { + + @Id + @GeneratedValue + private Long id; + + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + @Column(name = "registered_on") + @CreationTimestamp + private LocalDateTime createdOn = LocalDateTime.now(); + + public Long getId() { + return id; + } + + public User setId(Long id) { + this.id = id; + return this; + } + + public String getFirstName() { + return firstName; + } + + public User setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public User setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public User setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/event/PartitionAwareEventListenerIntegrator.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/event/PartitionAwareEventListenerIntegrator.java new file mode 100644 index 000000000..af55e1604 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/event/PartitionAwareEventListenerIntegrator.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.spring.partition.event; + +import org.hibernate.boot.Metadata; +import org.hibernate.boot.spi.BootstrapContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; + +public class PartitionAwareEventListenerIntegrator implements Integrator { + + public static final PartitionAwareEventListenerIntegrator INSTANCE = + new PartitionAwareEventListenerIntegrator(); + + @Override + public void integrate( + Metadata metadata, + BootstrapContext bootstrapContext, + SessionFactoryImplementor sessionFactory) { + sessionFactory + .getServiceRegistry() + .getService(EventListenerRegistry.class) + .prependListeners( + EventType.PERSIST, + PartitionAwareInsertEventListener.INSTANCE + ); + } + + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/event/PartitionAwareInsertEventListener.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/event/PartitionAwareInsertEventListener.java new file mode 100644 index 000000000..da7ff3733 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/event/PartitionAwareInsertEventListener.java @@ -0,0 +1,40 @@ +package com.vladmihalcea.hpjp.spring.partition.event; + +import com.vladmihalcea.hpjp.spring.partition.domain.PartitionAware; +import org.hibernate.Filter; +import org.hibernate.HibernateException; +import org.hibernate.event.spi.PersistContext; +import org.hibernate.event.spi.PersistEvent; +import org.hibernate.event.spi.PersistEventListener; +import org.hibernate.internal.FilterImpl; + +/** + * @author Vlad Mihalcea + */ +public class PartitionAwareInsertEventListener implements PersistEventListener { + + public static final PartitionAwareInsertEventListener INSTANCE = new PartitionAwareInsertEventListener(); + + @Override + public void onPersist(PersistEvent event) throws HibernateException { + final Object entity = event.getObject(); + + if (entity instanceof PartitionAware partitionAware) { + if (partitionAware.getPartitionKey() == null) { + FilterImpl partitionKeyFilter = (FilterImpl) event + .getSession() + .getEnabledFilter(PartitionAware.PARTITION_KEY); + partitionAware.setPartitionKey( + (String) partitionKeyFilter + .getParameter(PartitionAware.PARTITION_KEY) + ); + } + } + } + + @Override + public void onPersist(PersistEvent event, PersistContext persistContext) + throws HibernateException { + onPersist(event); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/repository/PostRepository.java new file mode 100644 index 000000000..f0c607b2e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/repository/PostRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.partition.repository; + +import com.vladmihalcea.hpjp.spring.partition.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/repository/UserRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/repository/UserRepository.java new file mode 100644 index 000000000..1769d9022 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/repository/UserRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.partition.repository; + +import com.vladmihalcea.hpjp.spring.partition.domain.User; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface UserRepository extends BaseJpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/service/ForumService.java new file mode 100644 index 000000000..e98cb88c5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/service/ForumService.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.partition.service; + +import com.vladmihalcea.hpjp.spring.partition.domain.Post; +import com.vladmihalcea.hpjp.spring.partition.repository.PostRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumService { + + private final PostRepository postRepository; + + public ForumService( + @Autowired PostRepository postRepository) { + this.postRepository = postRepository; + } + + @Transactional + public void createPosts(List posts) { + postRepository.persistAll(posts); + } + + public List findByIds(List ids) { + return postRepository.findAllById(ids); + } + + public Post findById(Long id) { + return postRepository.findById(id).orElse(null); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/util/UserContext.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/util/UserContext.java new file mode 100644 index 000000000..f9be472fb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/partition/util/UserContext.java @@ -0,0 +1,23 @@ +package com.vladmihalcea.hpjp.spring.partition.util; + +import com.vladmihalcea.hpjp.spring.partition.domain.User; + +/** + * @author Vlad Mihalcea + */ +public class UserContext { + + private static final ThreadLocal userHolder = new ThreadLocal<>(); + + public static void logIn(User user) { + userHolder.set(user); + } + + public static void logOut() { + userHolder.remove(); + } + + public static User getCurrent() { + return userHolder.get(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/SpringStatelessSessionBatchingTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/SpringStatelessSessionBatchingTest.java new file mode 100644 index 000000000..ad6bf16b6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/SpringStatelessSessionBatchingTest.java @@ -0,0 +1,59 @@ +package com.vladmihalcea.hpjp.spring.stateless; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.stateless.config.SpringStatelessSessionBatchingConfiguration; +import com.vladmihalcea.hpjp.spring.stateless.domain.Post; +import com.vladmihalcea.hpjp.spring.stateless.service.ForumService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; + +import java.util.List; +import java.util.stream.LongStream; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringStatelessSessionBatchingConfiguration.class) +public class SpringStatelessSessionBatchingTest extends AbstractSpringTest { + + public static final int POST_COUNT = 15; + + @Autowired + private ForumService forumService; + + @Override + protected Class[] entities() { + return new Class[]{ + Post.class + }; + } + + @Test + public void testBatchWrite() { + List posts = LongStream.rangeClosed(1, POST_COUNT) + .mapToObj(postId -> new Post() + .setId(postId) + .setTitle( + String.format("High-Performance Java Persistence - Page %d", + postId + ) + ) + .setCreatedBy("Vlad Mihalcea") + .setUpdatedBy("Vlad Mihalcea") + ) + .toList(); + + forumService.createPosts(posts); + + LongStream.rangeClosed(1, POST_COUNT) + .boxed() + .forEach(id -> { + Post post = forumService.findById(id); + if(post != null) { + LOGGER.info("Post [{}] found", post.getId()); + } + }); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/config/SpringStatelessSessionBatchingConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/config/SpringStatelessSessionBatchingConfiguration.java new file mode 100644 index 000000000..b475b67c6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/config/SpringStatelessSessionBatchingConfiguration.java @@ -0,0 +1,178 @@ +package com.vladmihalcea.hpjp.spring.stateless.config; + +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import jakarta.persistence.EntityManagerFactory; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.SessionFactory; +import org.hibernate.StatelessSessionBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hibernate.tool.schema.Action; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Locale; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +@EnableTransactionManagement +@EnableAspectJAutoProxy +@EnableJpaRepositories( + value = "com.vladmihalcea.hpjp.spring.stateless.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +@ComponentScan( + value = { + "com.vladmihalcea.hpjp.spring.stateless.service", + "io.hypersistence.utils.spring.aop" + } +) +public class SpringStatelessSessionBatchingConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + private int maxConnections = 64; + + @Bean + public static PropertySourcesPlaceholderConfigurer properties() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Bean + public Database database() { + return Database.MYSQL; + } + + @Bean + public DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + public DataSource poolingDataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setMaximumPoolSize(maxConnections); + hikariConfig.setAutoCommit(false); + hikariConfig.setDataSource(dataSourceProvider().dataSource()); + return new HikariDataSource(hikariConfig); + } + + @Bean + public DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + DataSource dataSource = ProxyDataSourceBuilder + .create(poolingDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + try(Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + statement.executeUpdate("drop table if exists post"); + statement.executeUpdate(""" + create table post ( + id bigint not null auto_increment, + created_by varchar(255), + created_on datetime(6), + title varchar(255), + updated_by varchar(255), + updated_on datetime(6), + version smallint, + primary key (id) + ) + """); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + return dataSource; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory( + @Autowired DataSource dataSource) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setPackagesToScan(packagesToScan()); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + entityManagerFactoryBean.setJpaProperties(additionalProperties()); + return entityManagerFactoryBean; + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { + return new TransactionTemplate(transactionManager(entityManagerFactory)); + } + + @Bean + public StatelessSessionBuilder statelessSessionBuilder(EntityManagerFactory entityManagerFactory) { + return entityManagerFactory.unwrap(SessionFactory.class).withStatelessOptions(); + } + + @Bean + public Integer batchProcessingSize() { + return 100; + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.setProperty( + AvailableSettings.HBM2DDL_AUTO, + Action.NONE.name().toLowerCase(Locale.ROOT) + ); + properties.setProperty( + AvailableSettings.STATEMENT_BATCH_SIZE, + String.valueOf(batchProcessingSize()) + ); + properties.setProperty( + AvailableSettings.ORDER_INSERTS, + Boolean.TRUE.toString() + ); + properties.setProperty( + AvailableSettings.ORDER_UPDATES, + Boolean.TRUE.toString() + ); + return properties; + } + + protected String[] packagesToScan() { + return new String[]{ + "com.vladmihalcea.hpjp.spring.stateless.domain" + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/domain/AbstractPost.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/domain/AbstractPost.java new file mode 100644 index 000000000..ea2d594c5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/domain/AbstractPost.java @@ -0,0 +1,85 @@ +package com.vladmihalcea.hpjp.spring.stateless.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Version; + +import java.time.LocalDateTime; + +/** + * @author Vlad Mihalcea + */ +@MappedSuperclass +public abstract class AbstractPost { + + private String title; + + @Column(name = "created_on") + private LocalDateTime createdOn = LocalDateTime.now(); + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_on") + private LocalDateTime updatedOn = LocalDateTime.now(); + + @Column(name = "updated_by") + private String updatedBy; + + @Version + private Short version; + + public String getTitle() { + return title; + } + + public T setTitle(String title) { + this.title = title; + return (T) this; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public T setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return (T) this; + } + + public String getCreatedBy() { + return createdBy; + } + + public T setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return (T) this; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public T setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + return (T) this; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public T setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + return (T) this; + } + + public Short getVersion() { + return version; + } + + public T setVersion(Short version) { + this.version = version; + return (T) this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/domain/BatchInsertPost.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/domain/BatchInsertPost.java new file mode 100644 index 000000000..51b6e2f80 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/domain/BatchInsertPost.java @@ -0,0 +1,52 @@ +package com.vladmihalcea.hpjp.spring.stateless.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.SQLInsert; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post") +@SQLInsert(sql = """ +INSERT INTO post ( + created_by, created_on, title, + updated_by, updated_on, version +) +VALUES ( + ?, ?, ?, + ?, ?, ? +) +""") +public class BatchInsertPost extends AbstractPost { + + @Id + @Column(insertable = false) + @GeneratedValue(generator = "noop_generator") + @GenericGenerator( + name = "noop_generator", + strategy = "com.vladmihalcea.hpjp.spring.stateless.domain.NoOpGenerator" + ) + private Long id; + + public Long getId() { + return id; + } + + public BatchInsertPost setId(Long id) { + this.id = id; + return this; + } + + public static BatchInsertPost valueOf(Post post) { + return new BatchInsertPost() + .setId(post.getId()) + .setTitle(post.getTitle()) + .setCreatedBy(post.getCreatedBy()) + .setCreatedOn(post.getCreatedOn()) + .setUpdatedBy(post.getUpdatedBy()) + .setUpdatedOn(post.getUpdatedOn()) + .setVersion(post.getVersion()); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/domain/NoOpGenerator.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/domain/NoOpGenerator.java new file mode 100644 index 000000000..a739c6b87 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/domain/NoOpGenerator.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.spring.stateless.domain; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.IdentifierGenerator; +import org.hibernate.id.factory.spi.StandardGenerator; + +/** + * @author Vlad Mihalcea + */ +public class NoOpGenerator implements IdentifierGenerator, StandardGenerator { + + @Override + public Object generate(SharedSessionContractImplementor session, Object obj) { + return null; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/domain/Post.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/domain/Post.java new file mode 100644 index 000000000..7540f83a1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/domain/Post.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.spring.stateless.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "post") +public class Post extends AbstractPost { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/repository/CustomPostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/repository/CustomPostRepository.java new file mode 100644 index 000000000..9ae051565 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/repository/CustomPostRepository.java @@ -0,0 +1,10 @@ +package com.vladmihalcea.hpjp.spring.stateless.repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface CustomPostRepository { + List persistAll(Iterable entities); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/repository/CustomPostRepositoryImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/repository/CustomPostRepositoryImpl.java new file mode 100644 index 000000000..cc03c7260 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/repository/CustomPostRepositoryImpl.java @@ -0,0 +1,62 @@ +package com.vladmihalcea.hpjp.spring.stateless.repository; + +import com.vladmihalcea.hpjp.spring.stateless.domain.BatchInsertPost; +import com.vladmihalcea.hpjp.spring.stateless.domain.Post; +import jakarta.persistence.EntityManager; +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.StatelessSession; +import org.hibernate.StatelessSessionBuilder; +import org.hibernate.resource.jdbc.spi.JdbcSessionOwner; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * @author Vlad Mihalcea + */ +public class CustomPostRepositoryImpl implements CustomPostRepository { + + private final EntityManager entityManager; + + private final StatelessSessionBuilder statelessSessionBuilder; + + private final Integer batchProcessingSize; + + public CustomPostRepositoryImpl( + EntityManager entityManager, + StatelessSessionBuilder statelessSessionBuilder, + Integer batchProcessingSize) { + this.entityManager = entityManager; + this.statelessSessionBuilder = statelessSessionBuilder; + this.batchProcessingSize = batchProcessingSize; + } + + @Override + public List persistAll(Iterable entities) { + final StatelessSession statelessSession = statelessSessionBuilder + .connection( + entityManager + .unwrap(Session.class) + .doReturningWork(connection -> connection) + ) + .openStatelessSession(); + try { + statelessSession.setJdbcBatchSize(batchProcessingSize); + statelessSession.beginTransaction(); + + return StreamSupport.stream(entities.spliterator(), false) + .peek(entity -> { + statelessSession.insert(BatchInsertPost.valueOf(entity)); + }) + .collect(Collectors.toList()); + } catch (Exception e) { + throw new HibernateException(e); + } finally { + JdbcSessionOwner jdbcSessionOwner = ((JdbcSessionOwner) statelessSession); + jdbcSessionOwner.flushBeforeTransactionCompletion(); + statelessSession.close(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/repository/PostRepository.java new file mode 100644 index 000000000..01789a294 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/repository/PostRepository.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.stateless.repository; + +import com.vladmihalcea.hpjp.spring.stateless.domain.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository, CustomPostRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/service/ForumService.java new file mode 100644 index 000000000..a186ed954 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/stateless/service/ForumService.java @@ -0,0 +1,76 @@ +package com.vladmihalcea.hpjp.spring.stateless.service; + +import com.vladmihalcea.hpjp.spring.stateless.domain.Post; +import com.vladmihalcea.hpjp.spring.stateless.repository.PostRepository; +import com.vladmihalcea.hpjp.util.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumService { + + private static final Logger LOGGER = LoggerFactory.getLogger(ForumService.class); + + private static final ExecutorService executorService = Executors.newFixedThreadPool( + Runtime.getRuntime().availableProcessors() + ); + + private final PostRepository postRepository; + + private final TransactionTemplate transactionTemplate; + + private final int batchProcessingSize; + + public ForumService( + @Autowired PostRepository postRepository, + @Autowired TransactionTemplate transactionTemplate, + @Autowired int batchProcessingSize) { + this.postRepository = postRepository; + this.transactionTemplate = transactionTemplate; + this.batchProcessingSize = batchProcessingSize; + } + + @Transactional(propagation = Propagation.NEVER) + public void createPosts(List posts) { + CollectionUtils.spitInBatches(posts, batchProcessingSize) + .map(postBatch -> executorService.submit(() -> { + try { + transactionTemplate.execute((status) -> postRepository.persistAll(postBatch)); + } catch (TransactionException e) { + LOGGER.error("Batch transaction failure", e); + } + })) + .forEach(future -> { + try { + future.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + LOGGER.error("Batch execution failure", e); + } + }); + } + + public List findByIds(List ids) { + return postRepository.findAllById(ids); + } + + public Post findById(Long id) { + return postRepository.findById(id).orElse(null); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/ContractTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/ContractTest.java new file mode 100644 index 000000000..714b799cc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/ContractTest.java @@ -0,0 +1,137 @@ +package com.vladmihalcea.hpjp.spring.transaction.contract; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.transaction.contract.config.ContractConfiguration; +import com.vladmihalcea.hpjp.spring.transaction.contract.domain.Annex; +import com.vladmihalcea.hpjp.spring.transaction.contract.domain.AnnexSignature; +import com.vladmihalcea.hpjp.spring.transaction.contract.domain.Contract; +import com.vladmihalcea.hpjp.spring.transaction.contract.domain.ContractSignature; +import com.vladmihalcea.hpjp.spring.transaction.contract.repository.ContractRepository; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = ContractConfiguration.class) +public class ContractTest extends AbstractSpringTest { + + @Autowired + private ContractRepository contractRepository; + + @Override + protected Class[] entities() { + return new Class[]{ + AnnexSignature.class, + Annex.class, + ContractSignature.class, + Contract.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Contract contract = new Contract() + .setId(1L) + .setTitle("Hypersistence Training"); + + ContractSignature contractSignature = new ContractSignature() + .setContract(contract) + .setFirstName("Vlad") + .setLastName("Mihalcea"); + + Annex annex1 = new Annex() + .setId(1L) + .setDetails("High-Performance Java Persistence Training") + .setContract(contract); + + AnnexSignature annexSignature1 = new AnnexSignature() + .setAnnex(annex1) + .setFirstName("Vlad") + .setLastName("Mihalcea"); + + Annex annex2 = new Annex() + .setId(2L) + .setDetails("High-Performance SQL Training") + .setContract(contract); + + AnnexSignature annexSignature2 = new AnnexSignature() + .setAnnex(annex2) + .setFirstName("Vlad") + .setLastName("Mihalcea"); + + entityManager.persist(contract); + entityManager.persist(contractSignature); + entityManager.persist(annex1); + entityManager.persist(annex2); + entityManager.persist(annexSignature1); + entityManager.persist(annexSignature2); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void test() { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + AnnexSignature signature = entityManager.createQuery(""" + select a_s + from AnnexSignature a_s + join fetch a_s.annex a + join fetch a.contract c + where a_s.id = :id + """, AnnexSignature.class) + .setParameter("id", 2L) + .getSingleResult(); + + signature.setFirstName("Vlad-Alexandru"); + + return null; + }); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Annex annex = entityManager.createQuery(""" + select a + from Annex a + join fetch a.contract + where a.id = :id + """, Annex.class) + .setParameter("id", 2L) + .getSingleResult(); + + annex.setDetails("High-Performance SQL Online Training"); + + return null; + }); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + entityManager.persist( + new Annex() + .setId(3L) + .setDetails("Spring 6 Migration Training") + .setContract( + entityManager.getReference( + Contract.class, 1L + ) + ) + ); + + return null; + }); + + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Annex annex = entityManager.getReference(Annex.class, 3L); + entityManager.remove(annex); + + return null; + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/config/ContractConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/config/ContractConfiguration.java new file mode 100644 index 000000000..2cb4926fb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/config/ContractConfiguration.java @@ -0,0 +1,126 @@ +package com.vladmihalcea.hpjp.spring.transaction.contract.config; + +import com.vladmihalcea.hpjp.spring.transaction.contract.event.RootAwareEventListenerIntegrator; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import jakarta.persistence.EntityManagerFactory; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.List; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.transaction.contract", + } +) +@EnableJpaRepositories("com.vladmihalcea.hpjp.spring.transaction.contract.repository") +@EnableTransactionManagement +@EnableAspectJAutoProxy +public class ContractConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + @Bean + public static PropertySourcesPlaceholderConfigurer properties() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Bean + public Database database() { + return Database.POSTGRESQL; + } + + @Bean + public DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + public DataSource poolingDataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setMaximumPoolSize(64); + hikariConfig.setAutoCommit(false); + hikariConfig.setDataSource(dataSourceProvider().dataSource()); + return new HikariDataSource(hikariConfig); + } + + @Bean + public DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + DataSource dataSource = ProxyDataSourceBuilder + .create(poolingDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + return dataSource; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory( + @Autowired DataSource dataSource) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setPackagesToScan(packagesToScan()); + entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); + entityManagerFactoryBean.setJpaProperties(additionalProperties()); + return entityManagerFactoryBean; + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + properties.put( + EntityManagerFactoryBuilderImpl.INTEGRATOR_PROVIDER, + (IntegratorProvider) () -> List.of( + RootAwareEventListenerIntegrator.INSTANCE + ) + ); + return properties; + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { + return new TransactionTemplate(transactionManager(entityManagerFactory)); + } + + protected String[] packagesToScan() { + return new String[]{ + "com.vladmihalcea.hpjp.spring.transaction.contract.domain" + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/Annex.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/Annex.java new file mode 100644 index 000000000..44ab16753 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/Annex.java @@ -0,0 +1,52 @@ +package com.vladmihalcea.hpjp.spring.transaction.contract.domain; + +import jakarta.persistence.*; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "annex") +public class Annex implements RootAware { + + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Contract contract; + + private String details; + + public Long getId() { + return id; + } + + public Annex setId(Long id) { + this.id = id; + return this; + } + + public Contract getContract() { + return contract; + } + + public Annex setContract(Contract post) { + this.contract = post; + return this; + } + + public String getDetails() { + return details; + } + + public Annex setDetails(String review) { + this.details = review; + return this; + } + + @Override + public Contract root() { + return contract; + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/AnnexSignature.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/AnnexSignature.java new file mode 100644 index 000000000..330581ba2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/AnnexSignature.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.hpjp.spring.transaction.contract.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "annex_signature") +public class AnnexSignature extends BaseSignature + implements RootAware { + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @OnDelete(action = OnDeleteAction.CASCADE) + private Annex annex; + + public Annex getAnnex() { + return annex; + } + + public AnnexSignature setAnnex(Annex annex) { + this.annex = annex; + return this; + } + + @Override + public Contract root() { + return annex.root(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/BaseSignature.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/BaseSignature.java new file mode 100644 index 000000000..d69d639be --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/BaseSignature.java @@ -0,0 +1,62 @@ +package com.vladmihalcea.hpjp.spring.transaction.contract.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; + +import java.time.LocalDate; + +/** + * @author Vlad Mihalcea + */ +@MappedSuperclass +public class BaseSignature> { + + @Id + private Long id; + + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + @Column(name = "signed_on") + private LocalDate signedOn = LocalDate.now(); + + public Long getId() { + return id; + } + + public T setId(Long id) { + this.id = id; + return (T) this; + } + + public String getFirstName() { + return firstName; + } + + public T setFirstName(String firstName) { + this.firstName = firstName; + return (T) this; + } + + public String getLastName() { + return lastName; + } + + public T setLastName(String lastName) { + this.lastName = lastName; + return (T) this; + } + + public LocalDate getSignedOn() { + return signedOn; + } + + public T setSignedOn(LocalDate signedOn) { + this.signedOn = signedOn; + return (T) this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/Contract.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/Contract.java new file mode 100644 index 000000000..e1af24e63 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/Contract.java @@ -0,0 +1,40 @@ +package com.vladmihalcea.hpjp.spring.transaction.contract.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "contract") +public class Contract { + + @Id + private Long id; + + private String title; + + @Version + private Short version; + + public Long getId() { + return id; + } + + public Contract setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Contract setTitle(String title) { + this.title = title; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/ContractSignature.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/ContractSignature.java new file mode 100644 index 000000000..3846ed526 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/ContractSignature.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.hpjp.spring.transaction.contract.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "contract_signature") +public class ContractSignature extends BaseSignature + implements RootAware { + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @OnDelete(action = OnDeleteAction.CASCADE) + private Contract contract; + + public Contract getContract() { + return contract; + } + + public ContractSignature setContract(Contract contract) { + this.contract = contract; + return this; + } + + @Override + public Contract root() { + return contract; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/RootAware.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/RootAware.java new file mode 100644 index 000000000..337040109 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/domain/RootAware.java @@ -0,0 +1,8 @@ +package com.vladmihalcea.hpjp.spring.transaction.contract.domain; + +/** + * @author Vlad Mihalcea + */ +public interface RootAware { + T root(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/event/RootAwareEventListenerIntegrator.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/event/RootAwareEventListenerIntegrator.java new file mode 100644 index 000000000..f45859714 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/event/RootAwareEventListenerIntegrator.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.spring.transaction.contract.event; + +import org.hibernate.boot.Metadata; +import org.hibernate.boot.spi.BootstrapContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; + +public class RootAwareEventListenerIntegrator implements Integrator { + + public static final RootAwareEventListenerIntegrator INSTANCE = + new RootAwareEventListenerIntegrator(); + + @Override + public void integrate(Metadata metadata, BootstrapContext bootstrapContext, SessionFactoryImplementor sessionFactory) { + final EventListenerRegistry eventListenerRegistry = sessionFactory + .getServiceRegistry() + .getService(EventListenerRegistry.class); + + eventListenerRegistry.appendListeners( + EventType.PERSIST, + RootAwareInsertEventListener.INSTANCE + ); + eventListenerRegistry.appendListeners( + EventType.FLUSH_ENTITY, + RootAwareUpdateAndDeleteEventListener.INSTANCE + ); + } + + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/event/RootAwareInsertEventListener.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/event/RootAwareInsertEventListener.java new file mode 100644 index 000000000..05b29c6f4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/event/RootAwareInsertEventListener.java @@ -0,0 +1,43 @@ +package com.vladmihalcea.hpjp.spring.transaction.contract.event; + +import com.vladmihalcea.hpjp.spring.transaction.contract.domain.RootAware; +import jakarta.persistence.LockModeType; +import org.hibernate.HibernateException; +import org.hibernate.event.spi.PersistContext; +import org.hibernate.event.spi.PersistEvent; +import org.hibernate.event.spi.PersistEventListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Vlad Mihalcea + */ +public class RootAwareInsertEventListener implements PersistEventListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(RootAwareInsertEventListener.class); + + public static final RootAwareInsertEventListener INSTANCE = new RootAwareInsertEventListener(); + + @Override + public void onPersist(PersistEvent event) throws HibernateException { + final Object entity = event.getObject(); + + if (entity instanceof RootAware rootAware) { + Object root = rootAware.root(); + event.getSession().lock(root, LockModeType.OPTIMISTIC_FORCE_INCREMENT); + + LOGGER.info( + "Incrementing the [{}] entity version " + + "because the [{}] child entity has been inserted", + root, + entity + ); + } + } + + @Override + public void onPersist(PersistEvent event, PersistContext persistContext) + throws HibernateException { + onPersist(event); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/event/RootAwareUpdateAndDeleteEventListener.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/event/RootAwareUpdateAndDeleteEventListener.java new file mode 100644 index 000000000..c7cd5d54e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/event/RootAwareUpdateAndDeleteEventListener.java @@ -0,0 +1,84 @@ +package com.vladmihalcea.hpjp.spring.transaction.contract.event; + +import com.vladmihalcea.hpjp.spring.transaction.contract.domain.RootAware; +import jakarta.persistence.LockModeType; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.Status; +import org.hibernate.event.spi.FlushEntityEvent; +import org.hibernate.event.spi.FlushEntityEventListener; +import org.hibernate.persister.entity.EntityPersister; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Vlad Mihalcea + */ +public class RootAwareUpdateAndDeleteEventListener implements FlushEntityEventListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(RootAwareUpdateAndDeleteEventListener.class); + + public static final RootAwareUpdateAndDeleteEventListener INSTANCE = new RootAwareUpdateAndDeleteEventListener(); + + @Override + public void onFlushEntity(FlushEntityEvent event) throws HibernateException { + final EntityEntry entry = event.getEntityEntry(); + final Object entity = event.getEntity(); + final boolean mightBeDirty = entry.requiresDirtyCheck(entity); + + if (mightBeDirty && entity instanceof RootAware rootAware) { + if (isEntityUpdated(event)) { + Object root = rootAware.root(); + LOGGER.info( + "Incrementing the [{}] entity version " + + "because the [{}] child entity has been updated", + root, + entity + ); + event.getSession().lock(root, LockModeType.OPTIMISTIC_FORCE_INCREMENT); + } else if (isEntityDeleted(event)) { + Object root = rootAware.root(); + LOGGER.info( + "Incrementing the [{}] entity version " + + "because the [{}] child entity has been deleted", + root, + entity + ); + event.getSession().lock(root, LockModeType.OPTIMISTIC_FORCE_INCREMENT); + } + } + } + + private boolean isEntityUpdated(FlushEntityEvent event) { + final EntityEntry entry = event.getEntityEntry(); + final Object entity = event.getEntity(); + + int[] dirtyProperties; + EntityPersister persister = entry.getPersister(); + final Object[] values = event.getPropertyValues(); + SessionImplementor session = event.getSession(); + + if (event.hasDatabaseSnapshot()) { + dirtyProperties = persister.findModified( + event.getDatabaseSnapshot(), + values, + entity, + session + ); + } else { + dirtyProperties = persister.findDirty( + values, + entry.getLoadedState(), + entity, + session + ); + } + + return dirtyProperties != null; + } + + private boolean isEntityDeleted(FlushEntityEvent event) { + return event.getEntityEntry().getStatus() == Status.DELETED; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/repository/ContractRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/repository/ContractRepository.java new file mode 100644 index 000000000..77097595b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/contract/repository/ContractRepository.java @@ -0,0 +1,15 @@ +package com.vladmihalcea.hpjp.spring.transaction.contract.repository; + +import com.vladmihalcea.hpjp.spring.transaction.contract.domain.Contract; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Vlad Mihalcea + */ +@Repository +@Transactional(readOnly = true) +public interface ContractRepository extends JpaRepository { + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/HibernateTransactionManagerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/HibernateTransactionManagerTest.java new file mode 100644 index 000000000..57527c305 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/HibernateTransactionManagerTest.java @@ -0,0 +1,80 @@ +package com.vladmihalcea.hpjp.spring.transaction.hibernate; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.PostComment; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.PostDetails; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.transaction.hibernate.config.HibernateTransactionManagerConfiguration; +import com.vladmihalcea.hpjp.spring.transaction.hibernate.service.ForumService; +import org.hibernate.SessionFactory; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = HibernateTransactionManagerConfiguration.class) +public class HibernateTransactionManagerTest extends AbstractSpringTest { + + @Autowired + private SessionFactory sessionFactory; + + @Autowired + private ForumService forumService; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + PostDetails.class, + Tag.class, + Post.class, + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Tag hibernate = new Tag(); + hibernate.setName("hibernate"); + sessionFactory.getCurrentSession().persist(hibernate); + + Tag jpa = new Tag(); + jpa.setName("jpa"); + sessionFactory.getCurrentSession().persist(jpa); + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void test() { + Post newPost = forumService.newPost("High-Performance Java Persistence", "hibernate", "jpa"); + assertNotNull(newPost.getId()); + + List posts = forumService.findAllByTitle("High-Performance Java Persistence"); + assertEquals(1, posts.size()); + + Post post = forumService.findById(newPost.getId()); + assertEquals("High-Performance Java Persistence", post.getTitle()); + + PostDTO postDTO = forumService.getPostDTOById(newPost.getId()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + + //Do nothing in the transaction to check the no statement warning + forumService.processData(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/config/HibernateTransactionManagerConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/config/HibernateTransactionManagerConfiguration.java new file mode 100644 index 000000000..e3fa2e453 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/config/HibernateTransactionManagerConfiguration.java @@ -0,0 +1,114 @@ +package com.vladmihalcea.hpjp.spring.transaction.hibernate.config; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.logging.LoggingStatementInspector; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.FlushMode; +import org.hibernate.SessionFactory; +import org.hibernate.cfg.AvailableSettings; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.orm.hibernate5.HibernateTransactionManager; +import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.Arrays; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +@ComponentScan(basePackages = "com.vladmihalcea.hpjp.spring.transaction.hibernate") +@EnableTransactionManagement +@EnableAspectJAutoProxy +public class HibernateTransactionManagerConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + @Bean + public Database database() { + return Database.POSTGRESQL; + } + + @Bean + public DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + public DataSource poolingDataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setMaximumPoolSize(64); + hikariConfig.setAutoCommit(false); + hikariConfig.setDataSource(dataSourceProvider().dataSource()); + return new HikariDataSource(hikariConfig); + } + + @Bean + public DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + DataSource dataSource = ProxyDataSourceBuilder + .create(poolingDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + return dataSource; + } + + @Bean + public LocalSessionFactoryBean sessionFactory(@Autowired DataSource dataSource) { + LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean(); + sessionFactoryBean.setDataSource(dataSource); + sessionFactoryBean.setPackagesToScan(packagesToScan()); + sessionFactoryBean.setHibernateProperties(additionalProperties()); + sessionFactoryBean.setHibernateIntegrators(new ClassImportIntegrator(Arrays.asList(PostDTO.class))); + return sessionFactoryBean; + } + + @Bean + public HibernateTransactionManager transactionManager(SessionFactory sessionFactory){ + //HibernateTransactionManager transactionManager = new HibernateTransactionManager(); + HibernateTransactionManager transactionManager = new MonitoringHibernateTransactionManager(); + transactionManager.setSessionFactory(sessionFactory); + return transactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate(SessionFactory sessionFactory) { + return new TransactionTemplate(transactionManager(sessionFactory)); + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + properties.put( + "hibernate.session_factory.statement_inspector", + new LoggingStatementInspector("com.vladmihalcea.hpjp.hibernate.transaction") + ); + properties.setProperty(AvailableSettings.FLUSH_MODE, FlushMode.ALWAYS.name()); + return properties; + } + + protected String[] packagesToScan() { + return new String[]{ + "com.vladmihalcea.hpjp.hibernate.transaction.forum" + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/config/MonitoringHibernateTransactionManager.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/config/MonitoringHibernateTransactionManager.java new file mode 100644 index 000000000..75cc0f75b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/config/MonitoringHibernateTransactionManager.java @@ -0,0 +1,49 @@ +package com.vladmihalcea.hpjp.spring.transaction.hibernate.config; + +import com.vladmihalcea.hpjp.util.StackTraceUtils; +import org.hibernate.BaseSessionEventListener; +import org.hibernate.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.orm.hibernate5.HibernateTransactionManager; +import org.springframework.transaction.TransactionDefinition; + +import java.util.concurrent.atomic.LongAdder; + +public class MonitoringHibernateTransactionManager extends HibernateTransactionManager { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + private static ThreadLocal statementCounterHolder = new ThreadLocal<>(); + + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) { + super.doBegin(transaction, definition); + Session session = getSessionFactory().getCurrentSession(); + final LongAdder statementCounter = new LongAdder(); + statementCounterHolder.set(statementCounter); + session.addEventListeners(new BaseSessionEventListener() { + @Override + public void jdbcPrepareStatementStart() { + statementCounter.increment(); + } + }); + } + + @Override + protected void doCleanupAfterCompletion(Object transaction) { + LongAdder statementCounter = statementCounterHolder.get(); + if (statementCounter.intValue() == 0) { + LOGGER.warn( + "Current transactional method {} didn't execute any SQL statement", + StackTraceUtils.stackTracePath( + StackTraceUtils.stackTraceElements( + "com.vladmihalcea" + ) + ) + ); + } + statementCounterHolder.remove(); + super.doCleanupAfterCompletion(transaction); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/GenericDAO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/GenericDAO.java new file mode 100644 index 000000000..b548471f1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/GenericDAO.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.transaction.hibernate.dao; + +import java.io.Serializable; + +/** + * @author Vlad Mihalcea + */ +public interface GenericDAO { + + T findById(ID id); + + T persist(T entity); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/GenericDAOImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/GenericDAOImpl.java new file mode 100644 index 000000000..18278c5d7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/GenericDAOImpl.java @@ -0,0 +1,49 @@ +package com.vladmihalcea.hpjp.spring.transaction.hibernate.dao; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; + +/** + * @author Vlad Mihalcea + */ +@Repository +@Transactional +public abstract class GenericDAOImpl implements GenericDAO { + + @Autowired + private SessionFactory sessionFactory; + + private final Class entityClass; + + protected SessionFactory getSessionFactory() { + return sessionFactory; + } + + protected Session getSession() { + return sessionFactory.getCurrentSession(); + } + + protected GenericDAOImpl(Class entityClass) { + this.entityClass = entityClass; + } + + public Class getEntityClass() { + return entityClass; + } + + @Override + public T findById(ID id) { + return getSession().get(entityClass, id); + } + + @Override + public T persist(T entity) { + getSession().persist(entity); + return entity; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/PostDAO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/PostDAO.java new file mode 100644 index 000000000..42afc2430 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/PostDAO.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.spring.transaction.hibernate.dao; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface PostDAO extends GenericDAO { + + List findByTitle(String title); + + PostDTO getPostDTOById(Long id); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/PostDAOImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/PostDAOImpl.java new file mode 100644 index 000000000..4f54ed996 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/PostDAOImpl.java @@ -0,0 +1,37 @@ +package com.vladmihalcea.hpjp.spring.transaction.hibernate.dao; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public class PostDAOImpl extends GenericDAOImpl implements PostDAO { + + protected PostDAOImpl() { + super(Post.class); + } + + @Override + public List findByTitle(String title) { + return getSession().createQuery( + "select p from Post p where p.title = :title", Post.class) + .setParameter("title", title) + .getResultList(); + } + + @Override + public PostDTO getPostDTOById(Long id) { + return getSession() + .createQuery( + "select new PostDTO(p.id, p.title) " + + "from Post p " + + "where p.id = :id", PostDTO.class) + .setParameter("id", id) + .getSingleResult(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/TagDAO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/TagDAO.java new file mode 100644 index 000000000..3c3554889 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/TagDAO.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.transaction.hibernate.dao; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface TagDAO extends GenericDAO { + + List findByName(String... tags); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/TagDAOImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/TagDAOImpl.java new file mode 100644 index 000000000..97219e675 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/dao/TagDAOImpl.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hpjp.spring.transaction.hibernate.dao; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; +import org.springframework.stereotype.Repository; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public class TagDAOImpl extends GenericDAOImpl implements TagDAO { + + protected TagDAOImpl() { + super(Tag.class); + } + + @Override + public List findByName(String... tags) { + if(tags.length == 0) { + throw new IllegalArgumentException("There's no tag name to search for!"); + } + return getSession().createQuery( + "select t " + + "from Tag t " + + "where t.name in :tags") + .setParameterList("tags", Arrays.asList(tags)) + .list(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/service/ForumService.java new file mode 100644 index 000000000..121eb74a3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/service/ForumService.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.spring.transaction.hibernate.service; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Service +public interface ForumService { + + Post newPost(String title, String... tags); + + List findAllByTitle(String title); + + Post findById(Long id); + + PostDTO getPostDTOById(Long id); + + void processData(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/service/ForumServiceImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/service/ForumServiceImpl.java new file mode 100644 index 000000000..67c030805 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/hibernate/service/ForumServiceImpl.java @@ -0,0 +1,89 @@ +package com.vladmihalcea.hpjp.spring.transaction.hibernate.service; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import com.vladmihalcea.hpjp.spring.transaction.hibernate.dao.PostDAO; +import com.vladmihalcea.hpjp.spring.transaction.hibernate.dao.TagDAO; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +@Service +public class ForumServiceImpl implements ForumService { + + @Autowired + private PostDAO postDAO; + + @Autowired + private TagDAO tagDAO; + + @Autowired + private SessionFactory sessionFactory; + + @Override + @Transactional + public Post newPost(String title, String... tags) { + Post post = new Post(); + post.setTitle(title); + post.getTags().addAll(tagDAO.findByName(tags)); + return postDAO.persist(post); + } + + @Override + @Transactional(readOnly = true) + public List findAllByTitle(String title) { + List posts = postDAO.findByTitle(title); + + Session session = sessionFactory.getCurrentSession(); + PersistenceContext persistenceContext = ((SharedSessionContractImplementor) session) + .getPersistenceContext(); + + for(Post post : posts) { + assertTrue(session.contains(post)); + + EntityEntry entityEntry = persistenceContext.getEntry(post); + assertNull(entityEntry.getLoadedState()); + } + + return posts; + } + + @Override + @Transactional + public Post findById(Long id) { + Post post = postDAO.findById(id); + + Session session = sessionFactory.getCurrentSession(); + PersistenceContext persistenceContext = ((SharedSessionContractImplementor) session) + .getPersistenceContext(); + + EntityEntry entityEntry = persistenceContext.getEntry(post); + assertNotNull(entityEntry.getLoadedState()); + + return post; + } + + @Override + @Transactional(readOnly = true) + public PostDTO getPostDTOById(Long id) { + return postDAO.getPostDTOById(id); + } + + @Override + @Transactional(readOnly = true) + public void processData() { + //Application-level processing + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/JPATransactionManagerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/JPATransactionManagerTest.java new file mode 100644 index 000000000..51e14392e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/JPATransactionManagerTest.java @@ -0,0 +1,131 @@ +package com.vladmihalcea.hpjp.spring.transaction.jpa; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.PostComment; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.PostDetails; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.transaction.jpa.config.JPATransactionManagerConfiguration; +import com.vladmihalcea.hpjp.spring.transaction.jpa.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.transaction.jpa.repository.TagRepository; +import com.vladmihalcea.hpjp.spring.transaction.jpa.service.ForumService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = JPATransactionManagerConfiguration.class) +public class JPATransactionManagerTest extends AbstractSpringTest { + + @Autowired + private ForumService forumService; + + @Autowired + private PostRepository postRepository; + + @Autowired + private TagRepository tagDAO; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + PostDetails.class, + Post.class, + Tag.class, + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Tag hibernate = new Tag(); + hibernate.setName("hibernate"); + tagDAO.persist(hibernate); + + Tag jpa = new Tag(); + jpa.setName("jpa"); + tagDAO.persist(jpa); + + postRepository.savePosts(); + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void test() { + Post newPost = forumService.newPost("High-Performance Java Persistence", "hibernate", "jpa"); + assertNotNull(newPost.getId()); + + List posts = forumService.findAllByTitle("High-Performance Java Persistence"); + assertEquals(1, posts.size()); + + Post post = forumService.findById(newPost.getId()); + //Check if the post was updated + assertEquals( + "High-Performance Java Persistence", + entityManager.find(Post.class, post.getId()).getTitle() + ); + + PostDTO postDTO = forumService.getPostDTOById(newPost.getId()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + + postDTO = forumService.savePostTitle(newPost.getId(), "High-Performance Java Persistence, 2nd edition"); + assertEquals("High-Performance Java Persistence, 2nd edition", postDTO.getTitle()); + } + + @Test + public void testJdbcTemplate() { + transactionTemplate.execute(status -> { + int postCountBeforePersist = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM post", Number.class).intValue(); + + Post post = new Post(); + post.setTitle("Latest post!"); + entityManager.persist(post); + entityManager.flush(); + + int postCountAfterPersist = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM post", Number.class).intValue(); + + assertEquals(postCountAfterPersist, postCountBeforePersist + 1); + return null; + }); + } + + @Test + public void testTransactionNoStatement() { + transactionTemplate.execute(status -> null); + } + + @Test + public void testJdbcTemplateWithoutTransaction() { + int postCountBefore = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM post", Number.class).intValue(); + + transactionTemplate.execute(status -> { + jdbcTemplate.execute("DELETE FROM post"); + + return null; + }); + + int postCountAfter = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM post", Number.class).intValue(); + + assertEquals(0, postCountAfter); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/ResourceLocalReleaseAfterStatementTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/ResourceLocalReleaseAfterStatementTest.java new file mode 100644 index 000000000..6a7338117 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/ResourceLocalReleaseAfterStatementTest.java @@ -0,0 +1,74 @@ +package com.vladmihalcea.hpjp.spring.transaction.jpa; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.PostComment; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.PostDetails; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.transaction.jpa.config.ResourceLocalReleaseAfterStatementConfiguration; +import com.vladmihalcea.hpjp.spring.transaction.jpa.service.ReleaseAfterStatementForumService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; + +import static org.junit.Assert.assertEquals; + +/** + * This test case demonstrates what happens when you enable the AFTER_STATEMENT + * Hibernate connection release mode when using a RESOURCE_LOCAL JPA transaction + * with Spring. + * + * Currently, this mode is disabled, as it will make the test fail. + * To enable the AFTER_STATEMENT release mode, open the {@link ResourceLocalReleaseAfterStatementConfiguration} + * file and pass the {@code DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT} setting to the + * {@code CONNECTION_HANDLING} Hibernate configuration property. + * + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = ResourceLocalReleaseAfterStatementConfiguration.class) +public class ResourceLocalReleaseAfterStatementTest extends AbstractSpringTest { + + @Autowired + private ReleaseAfterStatementForumService releaseAfterStatementForumService; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + PostDetails.class, + Post.class, + Tag.class, + }; + } + + @Test + public void test() { + Post newPost = releaseAfterStatementForumService.newPost( + "High-Performance Java Persistence" + ); + + /* + * At this point, if we enable the {@code DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT} + * connection release mode, there won't be any Post available because the previous @Transactional + * block did not commit the database transaction for the same JDBC Connection that was used + * to persist the Post entity. + * + * So, basically, the Post entity is persisted using one JDBC Connection, which is also sent + * back to the pool after the flush is done, and by the time the TransactionInterceptor + * tries to commit the connection, no {@code physicalConnection} will be found in + * {@link LogicalConnectionManagedImpl}, so a new JDBC Connection will be fetched from the pool + * only to commit that instead of the one that contained the modifications. + */ + + PostDTO postDTO = releaseAfterStatementForumService.savePostTitle( + newPost.getId(), + "High-Performance Java Persistence, 2nd edition" + ); + + assertEquals( + "High-Performance Java Persistence, 2nd edition", + postDTO.getTitle() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/config/JPATransactionManagerConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/config/JPATransactionManagerConfiguration.java new file mode 100644 index 000000000..743a83462 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/config/JPATransactionManagerConfiguration.java @@ -0,0 +1,173 @@ +package com.vladmihalcea.hpjp.spring.transaction.jpa.config; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.logging.LoggingStatementInspector; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import jakarta.persistence.EntityManagerFactory; +import net.ttddyy.dsproxy.ExecutionInfo; +import net.ttddyy.dsproxy.QueryInfo; +import net.ttddyy.dsproxy.listener.MethodExecutionContext; +import net.ttddyy.dsproxy.listener.lifecycle.JdbcLifecycleEventListenerAdapter; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.atomic.LongAdder; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +@ComponentScan(basePackages = { + "com.vladmihalcea.hpjp.spring.transaction.jpa.repository", + "com.vladmihalcea.hpjp.spring.transaction.jpa.service", +}) +@EnableJpaRepositories( + value = "com.vladmihalcea.hpjp.spring.transaction.jpa.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +@EnableTransactionManagement +@EnableAspectJAutoProxy +public class JPATransactionManagerConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + @Bean + public Database database() { + return Database.POSTGRESQL; + } + + @Bean + public DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + @Bean(destroyMethod = "close") + public DataSource dataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setMaximumPoolSize(64); + hikariConfig.setAutoCommit(false); + hikariConfig.setDataSource(dataSourceProvider().dataSource()); + HikariDataSource hikariDataSource = new HikariDataSource(hikariConfig); + + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + DataSource dataSource = ProxyDataSourceBuilder + .create(hikariDataSource) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .listener(new JdbcLifecycleEventListenerAdapter() { + private final ThreadLocal queryCountHolder = new ThreadLocal<>(); + + private final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + @Override + public void afterGetConnection(MethodExecutionContext executionContext) { + queryCountHolder.set(new LongAdder()); + } + + @Override + public void beforeQuery(ExecutionInfo execInfo, List queryInfoList) { + queryCountHolder.get().increment(); + } + + @Override + public void afterCommit(MethodExecutionContext executionContext) { + if(queryCountHolder.get().intValue() == 0) { + LOGGER.warn("Transaction didn't execute any SQL statement!"); + } + } + + @Override + public void afterClose(MethodExecutionContext executionContext) { + if(executionContext.getTarget() instanceof Connection) { + queryCountHolder.remove(); + } + } + }) + .build(); + return dataSource; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + + entityManagerFactoryBean.setDataSource(dataSource()); + entityManagerFactoryBean.setPackagesToScan(packagesToScan()); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + entityManagerFactoryBean.setJpaProperties(additionalProperties()); + return entityManagerFactoryBean; + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { + return new TransactionTemplate(transactionManager(entityManagerFactory)); + } + + @Bean + public JdbcTemplate jdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + properties.put( + "hibernate.session_factory.statement_inspector", + new LoggingStatementInspector("com.vladmihalcea.hpjp.hibernate.transaction") + ); + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator(Arrays.asList(PostDTO.class)) + ) + ); + return properties; + } + + protected String[] packagesToScan() { + return new String[]{ + "com.vladmihalcea.hpjp.hibernate.transaction.forum" + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/config/ResourceLocalReleaseAfterStatementConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/config/ResourceLocalReleaseAfterStatementConfiguration.java new file mode 100644 index 000000000..8e48ca955 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/config/ResourceLocalReleaseAfterStatementConfiguration.java @@ -0,0 +1,148 @@ +package com.vladmihalcea.hpjp.spring.transaction.jpa.config; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl; +import jakarta.persistence.EntityManagerFactory; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.*; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.Arrays; +import java.util.Collections; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +@PropertySource({"/META-INF/jdbc-postgresql.properties"}) +@ComponentScan(basePackages = { + "com.vladmihalcea.hpjp.spring.transaction.jpa.repository", + "com.vladmihalcea.hpjp.spring.transaction.jpa.service", +}) +@EnableJpaRepositories( + value = "com.vladmihalcea.hpjp.spring.transaction.jpa.repository", + repositoryBaseClass = BaseJpaRepositoryImpl.class +) +@EnableTransactionManagement +@EnableAspectJAutoProxy +public class ResourceLocalReleaseAfterStatementConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + @Value("${jdbc.dataSourceClassName}") + private String dataSourceClassName; + + @Value("${jdbc.url}") + private String jdbcUrl; + + @Value("${jdbc.username}") + private String jdbcUser; + + @Value("${jdbc.password}") + private String jdbcPassword; + + @Bean + public static PropertySourcesPlaceholderConfigurer properties() { + return new PropertySourcesPlaceholderConfigurer(); + } + + public DataSource poolingDataSource() { + Properties driverProperties = new Properties(); + driverProperties.setProperty("url", jdbcUrl); + driverProperties.setProperty("user", jdbcUser); + driverProperties.setProperty("password", jdbcPassword); + + Properties properties = new Properties(); + properties.put("dataSourceClassName", dataSourceClassName); + properties.put("dataSourceProperties", driverProperties); + properties.setProperty("maximumPoolSize", String.valueOf(5)); + + HikariConfig hikariConfig = new HikariConfig(properties); + hikariConfig.setAutoCommit(false); + return new HikariDataSource(hikariConfig); + } + + @Bean + public DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + return ProxyDataSourceBuilder + .create(poolingDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory( + @Autowired DataSource dataSource) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setPackagesToScan(packagesToScan()); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + entityManagerFactoryBean.setJpaProperties(additionalProperties()); + return entityManagerFactoryBean; + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { + return new TransactionTemplate(transactionManager(entityManagerFactory)); + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator(Arrays.asList(PostDTO.class)) + ) + ); + properties.put( + AvailableSettings.CONNECTION_HANDLING, + //PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT + PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION + ); + return properties; + } + + protected String[] packagesToScan() { + return new String[]{ + "com.vladmihalcea.hpjp.hibernate.transaction.forum" + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/repository/CustomPostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/repository/CustomPostRepository.java new file mode 100644 index 000000000..d88558a83 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/repository/CustomPostRepository.java @@ -0,0 +1,9 @@ +package com.vladmihalcea.hpjp.spring.transaction.jpa.repository; + +/** + * @author Vlad Mihalcea + */ +public interface CustomPostRepository { + + void savePosts(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/repository/CustomPostRepositoryImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/repository/CustomPostRepositoryImpl.java new file mode 100644 index 000000000..e119b2a25 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/repository/CustomPostRepositoryImpl.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.hpjp.spring.transaction.jpa.repository; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.hibernate.Session; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Vlad Mihalcea + */ +public class CustomPostRepositoryImpl implements CustomPostRepository { + + @PersistenceContext + private EntityManager entityManager; + + int entityCount = 10; + + @Transactional + public void savePosts() { + entityManager.unwrap(Session.class).setJdbcBatchSize(10); + try { + for ( long i = 0; i < entityCount; ++i ) { + Post post = new Post(); + post.setTitle(String.format( "Post nr %d", i )); + entityManager.persist( post ); + } + entityManager.flush(); + } finally { + entityManager.unwrap(Session.class).setJdbcBatchSize(null); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/repository/PostRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/repository/PostRepository.java new file mode 100644 index 000000000..68d5a06d0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/repository/PostRepository.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.hpjp.spring.transaction.jpa.repository; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface PostRepository extends BaseJpaRepository, CustomPostRepository { + + @Query(""" + select p + from Post p + where p.title = :title + """ + ) + List findByTitle(@Param("title") String title); + + @Query(""" + select new PostDTO(p.id, p.title) + from Post p + where p.id = :id + """ + ) + PostDTO getPostDTOById(@Param("id") Long id); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/repository/TagRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/repository/TagRepository.java new file mode 100644 index 000000000..b4fab91d3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/repository/TagRepository.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.spring.transaction.jpa.repository; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; +import io.hypersistence.utils.spring.repository.BaseJpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface TagRepository extends BaseJpaRepository { + + @Query(""" + select t + from Tag t + where t.name in :tags + """ + ) + List findByName(@Param("tags") String... tags); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/service/ForumService.java new file mode 100644 index 000000000..f09952f6f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/service/ForumService.java @@ -0,0 +1,24 @@ +package com.vladmihalcea.hpjp.spring.transaction.jpa.service; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Service +public interface ForumService { + + Post newPost(String title, String... tags); + + List findAllByTitle(String title); + + Post findById(Long id); + + PostDTO getPostDTOById(Long id); + + PostDTO savePostTitle(Long id, String title); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/service/ForumServiceImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/service/ForumServiceImpl.java new file mode 100644 index 000000000..bd967d7cc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/service/ForumServiceImpl.java @@ -0,0 +1,97 @@ +package com.vladmihalcea.hpjp.spring.transaction.jpa.service; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import com.vladmihalcea.hpjp.spring.transaction.jpa.repository.PostRepository; +import com.vladmihalcea.hpjp.spring.transaction.jpa.repository.TagRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +@Service +public class ForumServiceImpl implements ForumService { + + @Autowired + private PostRepository postRepository; + + @Autowired + private TagRepository tagRepository; + + @PersistenceContext + private EntityManager entityManager; + + @Override + @Transactional + public Post newPost(String title, String... tags) { + Post post = new Post(); + post.setTitle(title); + post.getTags().addAll(tagRepository.findByName(tags)); + return postRepository.persist(post); + } + + @Override + @Transactional(readOnly = true) + public List findAllByTitle(String title) { + List posts = postRepository.findByTitle(title); + + org.hibernate.engine.spi.PersistenceContext persistenceContext = getHibernatePersistenceContext(); + + for(Post post : posts) { + assertTrue(entityManager.contains(post)); + + EntityEntry entityEntry = persistenceContext.getEntry(post); + assertNull(entityEntry.getLoadedState()); + } + + return posts; + } + + @Override + @Transactional(readOnly = true) + public Post findById(Long id) { + Post post = postRepository.findById(id).orElseThrow(); + + org.hibernate.engine.spi.PersistenceContext persistenceContext = getHibernatePersistenceContext(); + + EntityEntry entityEntry = persistenceContext.getEntry(post); + assertNull(entityEntry.getLoadedState()); + + post.setTitle(null); + return post; + } + + @Override + @Transactional(readOnly = true) + public PostDTO getPostDTOById(Long id) { + return postRepository.getPostDTOById(id); + } + + @Override + @Transactional + public PostDTO savePostTitle(Long id, String title) { + Post post = postRepository.findById(id).orElseThrow(); + + post.setTitle(title); + + return postRepository.getPostDTOById(id); + } + + private org.hibernate.engine.spi.PersistenceContext getHibernatePersistenceContext() { + SharedSessionContractImplementor session = entityManager.unwrap( + SharedSessionContractImplementor.class + ); + return session.getPersistenceContext(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/service/ReleaseAfterStatementForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/service/ReleaseAfterStatementForumService.java new file mode 100644 index 000000000..c69a56bfd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/service/ReleaseAfterStatementForumService.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.spring.transaction.jpa.service; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import org.springframework.stereotype.Service; + +/** + * @author Vlad Mihalcea + */ +@Service +public interface ReleaseAfterStatementForumService { + + Post newPost(String title); + + PostDTO savePostTitle(Long id, String title); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/service/ReleaseAfterStatementForumServiceImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/service/ReleaseAfterStatementForumServiceImpl.java new file mode 100644 index 000000000..01e32f3a1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jpa/service/ReleaseAfterStatementForumServiceImpl.java @@ -0,0 +1,105 @@ +package com.vladmihalcea.hpjp.spring.transaction.jpa.service; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import com.vladmihalcea.hpjp.spring.transaction.jpa.repository.PostRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.hibernate.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.Assert.assertNotEquals; + +/** + * @author Vlad Mihalcea + */ +@Service +public class ReleaseAfterStatementForumServiceImpl implements ReleaseAfterStatementForumService { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + private static final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + private static final CountDownLatch latch1 = new CountDownLatch(1); + + private static final CountDownLatch latch2 = new CountDownLatch(1); + + private static final CountDownLatch latch3 = new CountDownLatch(1); + + @Autowired + private PostRepository postDAO; + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private TransactionTemplate transactionTemplate; + + @Override + @Transactional + public Post newPost(String title) { + Post post = new Post(); + post.setTitle(title); + return postDAO.persist(post); + } + + @Override + @Transactional + public PostDTO savePostTitle(Long id, String title) { + Post post = postDAO.findById(id).orElseThrow(); + + post.setTitle(title); + entityManager.flush(); + + executorService.submit(() -> { + transactionTemplate.execute(new TransactionCallback() { + @Nullable + @Override + public Void doInTransaction(TransactionStatus status) { + awaitOnLatch(latch1); + try { + PostDTO _postDTO = postDAO.getPostDTOById(id); + assertNotEquals(title, _postDTO.getTitle()); + } catch (Throwable e) { + LOGGER.error("Failure", e); + } + + entityManager.unwrap(Session.class).doWork(connection -> { + latch2.countDown(); + awaitOnLatch(latch3); + }); + + return null; + } + }); + }); + + latch1.countDown(); + awaitOnLatch(latch2); + PostDTO postDTO = postDAO.getPostDTOById(id); + latch3.countDown(); + executorService.shutdownNow(); + + return postDTO; + } + + private static void awaitOnLatch(CountDownLatch latch) { + try { + latch.await(); + } catch (InterruptedException e) { + Thread.interrupted(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/AromikosJTATransactionManagerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/AromikosJTATransactionManagerTest.java new file mode 100644 index 000000000..ebf22450f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/AromikosJTATransactionManagerTest.java @@ -0,0 +1,79 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.atomikos; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.PostComment; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.PostDetails; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.config.AtomikosJTATransactionManagerConfiguration; +import com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.dao.TagDAO; +import com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.service.ForumService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = AtomikosJTATransactionManagerConfiguration.class) +public class AromikosJTATransactionManagerTest extends AbstractSpringTest { + + @Autowired + private ForumService forumService; + + @Autowired + private TagDAO tagDAO; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + PostDetails.class, + Post.class, + Tag.class, + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Tag hibernate = new Tag(); + hibernate.setName("hibernate"); + tagDAO.persist(hibernate); + + Tag jpa = new Tag(); + jpa.setName("jpa"); + tagDAO.persist(jpa); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + + } + + @Test + public void test() { + Post newPost = forumService.newPost("High-Performance Java Persistence", "hibernate", "jpa"); + assertNotNull(newPost.getId()); + + List posts = forumService.findAllByTitle("High-Performance Java Persistence"); + assertEquals(1, posts.size()); + + Post post = forumService.findById(newPost.getId()); + assertEquals("High-Performance Java Persistence", post.getTitle()); + + PostDTO postDTO = forumService.getPostDTOById(newPost.getId()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/config/AtomikosJTATransactionManagerConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/config/AtomikosJTATransactionManagerConfiguration.java new file mode 100644 index 000000000..ef6779fe5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/config/AtomikosJTATransactionManagerConfiguration.java @@ -0,0 +1,152 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.config; + +import com.atomikos.icatch.jta.UserTransactionManager; +import com.atomikos.jdbc.AtomikosDataSourceBean; +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.logging.LoggingStatementInspector; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import jakarta.transaction.SystemException; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.engine.transaction.jta.platform.internal.AtomikosJtaPlatform; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.postgresql.xa.PGXADataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.*; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.jta.JtaTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.Arrays; +import java.util.Collections; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +@PropertySource({"/META-INF/jta-postgresql.properties"}) +@ComponentScan(basePackages = "com.vladmihalcea.hpjp.spring.transaction.jta.atomikos") +@EnableTransactionManagement +@EnableAspectJAutoProxy +public class AtomikosJTATransactionManagerConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + @Value("${jdbc.dataSourceClassName}") + protected String dataSourceClassName; + + @Value("${jdbc.username}") + protected String jdbcUser; + + @Value("${jdbc.password}") + protected String jdbcPassword; + + @Value("${jdbc.database}") + protected String jdbcDatabase; + + @Value("${jdbc.host}") + protected String jdbcHost; + + @Value("${jdbc.port}") + protected String jdbcPort; + + @Bean + public static PropertySourcesPlaceholderConfigurer properties() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @DependsOn("actualDataSource") + public DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + return ProxyDataSourceBuilder + .create(actualDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + } + + @Bean(initMethod = "init", destroyMethod = "close") + public AtomikosDataSourceBean actualDataSource() { + AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean(); + dataSource.setUniqueResourceName("PostgreSQL"); + PGXADataSource xaDataSource = new PGXADataSource(); + xaDataSource.setUser(jdbcUser); + xaDataSource.setPassword(jdbcPassword); + dataSource.setXaDataSource(xaDataSource); + dataSource.setPoolSize(5); + return dataSource; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + localContainerEntityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + localContainerEntityManagerFactoryBean.setJtaDataSource(dataSource()); + localContainerEntityManagerFactoryBean.setPackagesToScan(packagesToScan()); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + localContainerEntityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + localContainerEntityManagerFactoryBean.setJpaProperties(additionalProperties()); + return localContainerEntityManagerFactoryBean; + } + + @Bean(initMethod = "init", destroyMethod = "close") + public UserTransactionManager userTransactionManager() throws SystemException { + UserTransactionManager userTransactionManager = new UserTransactionManager(); + userTransactionManager.setTransactionTimeout(300); + userTransactionManager.setForceShutdown(true); + return userTransactionManager; + } + + @Bean + public JtaTransactionManager transactionManager() throws SystemException { + JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(); + jtaTransactionManager.setTransactionManager(userTransactionManager()); + jtaTransactionManager.setUserTransaction(userTransactionManager()); + jtaTransactionManager.setAllowCustomIsolationLevels(true); + return jtaTransactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate() throws SystemException { + return new TransactionTemplate(transactionManager()); + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.put( + AvailableSettings.JTA_PLATFORM, + AtomikosJtaPlatform.class + ); + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + properties.put( + "hibernate.session_factory.statement_inspector", + new LoggingStatementInspector("com.vladmihalcea.hpjp.hibernate.transaction") + ); + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator(Arrays.asList(PostDTO.class)) + ) + ); + return properties; + } + + protected String[] packagesToScan() { + return new String[]{ + "com.vladmihalcea.hpjp.hibernate.transaction.forum" + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/GenericDAO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/GenericDAO.java new file mode 100644 index 000000000..558f8dfef --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/GenericDAO.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.dao; + +import java.io.Serializable; + +/** + * @author Vlad Mihalcea + */ +public interface GenericDAO { + + T findById(ID id); + + T persist(T entity); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/GenericDAOImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/GenericDAOImpl.java new file mode 100644 index 000000000..766e117b7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/GenericDAOImpl.java @@ -0,0 +1,38 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.dao; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.stereotype.Repository; + +import java.io.Serializable; + +/** + * @author Vlad Mihalcea + */ +@Repository +public abstract class GenericDAOImpl implements GenericDAO { + + @PersistenceContext + private EntityManager entityManager; + + private final Class entityClass; + + protected EntityManager getEntityManager() { + return entityManager; + } + + protected GenericDAOImpl(Class entityClass) { + this.entityClass = entityClass; + } + + @Override + public T findById(ID id) { + return entityManager.find(entityClass, id); + } + + @Override + public T persist(T entity) { + entityManager.persist(entity); + return entity; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/PostBatchDAO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/PostBatchDAO.java new file mode 100644 index 000000000..6249c8ee3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/PostBatchDAO.java @@ -0,0 +1,11 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.dao; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; + +/** + * @author Vlad Mihalcea + */ +public interface PostBatchDAO extends GenericDAO { + + void savePosts(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/PostBatchDAOImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/PostBatchDAOImpl.java new file mode 100644 index 000000000..cbbd8898c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/PostBatchDAOImpl.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.dao; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceContextType; +import org.hibernate.Session; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Vlad Mihalcea + */ +@Repository +public class PostBatchDAOImpl extends GenericDAOImpl implements PostBatchDAO { + + @PersistenceContext(type = PersistenceContextType.EXTENDED) + private EntityManager entityManager; + + int entityCount = 10; + + protected PostBatchDAOImpl() { + super(Post.class); + } + + @Transactional + public void savePosts() { + entityManager.unwrap(Session.class).setJdbcBatchSize(10); + try { + for (long i = 0; i < entityCount; ++i) { + Post post = new Post(); + post.setTitle(String.format("Post nr %d", i)); + entityManager.persist(post); + } + } finally { + entityManager.unwrap(Session.class).setJdbcBatchSize(null); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/PostDAO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/PostDAO.java new file mode 100644 index 000000000..68fde2d99 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/PostDAO.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.dao; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface PostDAO extends GenericDAO { + + List findByTitle(String title); + + PostDTO getPostDTOById(Long id); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/PostDAOImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/PostDAOImpl.java new file mode 100644 index 000000000..6d7b15d76 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/PostDAOImpl.java @@ -0,0 +1,40 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.dao; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public class PostDAOImpl extends GenericDAOImpl implements PostDAO { + + protected PostDAOImpl() { + super(Post.class); + } + + @Override + public List findByTitle(String title) { + return getEntityManager() + .createQuery( + "select p " + + "from Post p " + + "where p.title = :title", Post.class) + .setParameter("title", title) + .getResultList(); + } + + @Override + public PostDTO getPostDTOById(Long id) { + return getEntityManager() + .createQuery( + "select new PostDTO(p.id, p.title) " + + "from Post p " + + "where p.id = :id", PostDTO.class) + .setParameter("id", id) + .getSingleResult(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/TagDAO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/TagDAO.java new file mode 100644 index 000000000..1e825046b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/TagDAO.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.dao; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface TagDAO extends GenericDAO { + + List findByName(String... tags); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/TagDAOImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/TagDAOImpl.java new file mode 100644 index 000000000..7813bb4be --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/dao/TagDAOImpl.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.dao; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; +import org.springframework.stereotype.Repository; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public class TagDAOImpl extends GenericDAOImpl implements TagDAO { + + protected TagDAOImpl() { + super(Tag.class); + } + + @Override + public List findByName(String... tags) { + if(tags.length == 0) { + throw new IllegalArgumentException("There's no tag name to search for!"); + } + return getEntityManager().createQuery( + "select t " + + "from Tag t " + + "where t.name in :tags", Tag.class) + .setParameter("tags", Arrays.asList(tags)) + .getResultList(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/service/ForumService.java new file mode 100644 index 000000000..be8f9e635 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/service/ForumService.java @@ -0,0 +1,21 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.service; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Service +public interface ForumService { + + Post newPost(String title, String... tags); + + List findAllByTitle(String title); + + Post findById(Long id); + + PostDTO getPostDTOById(Long id);} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/service/ForumServiceImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/service/ForumServiceImpl.java new file mode 100644 index 000000000..42c25ec81 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/atomikos/service/ForumServiceImpl.java @@ -0,0 +1,74 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.service; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.dao.PostDAO; +import com.vladmihalcea.hpjp.spring.transaction.jta.atomikos.dao.TagDAO; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +@Service +public class ForumServiceImpl implements ForumService { + + @Autowired + private PostDAO postDAO; + + @Autowired + private TagDAO tagDAO; + + @PersistenceContext + private EntityManager entityManager; + + @Override + @Transactional + public Post newPost(String title, String... tags) { + Post post = new Post(); + post.setTitle(title); + post.getTags().addAll(tagDAO.findByName(tags)); + return postDAO.persist(post); + } + + @Override + @Transactional(readOnly = true) + public List findAllByTitle(String title) { + return postDAO.findByTitle(title); + } + + @Override + @Transactional + public Post findById(Long id) { + Post post = postDAO.findById(id); + + org.hibernate.engine.spi.PersistenceContext persistenceContext = getHibernatePersistenceContext(); + + EntityEntry entityEntry = persistenceContext.getEntry(post); + assertNotNull(entityEntry.getLoadedState()); + + return post; + } + + @Override + @Transactional(readOnly = true) + public PostDTO getPostDTOById(Long id) { + return postDAO.getPostDTOById(id); + } + + private org.hibernate.engine.spi.PersistenceContext getHibernatePersistenceContext() { + SharedSessionContractImplementor session = entityManager.unwrap( + SharedSessionContractImplementor.class + ); + return session.getPersistenceContext(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/NarayanaJTATransactionManagerTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/NarayanaJTATransactionManagerTest.java new file mode 100644 index 000000000..1bc3d1662 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/NarayanaJTATransactionManagerTest.java @@ -0,0 +1,79 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.narayana; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.PostComment; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.PostDetails; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.transaction.jta.narayana.config.NarayanaJTATransactionManagerConfiguration; +import com.vladmihalcea.hpjp.spring.transaction.jta.narayana.dao.TagDAO; +import com.vladmihalcea.hpjp.spring.transaction.jta.narayana.service.ForumService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = NarayanaJTATransactionManagerConfiguration.class) +public class NarayanaJTATransactionManagerTest extends AbstractSpringTest { + + @Autowired + private ForumService forumService; + + @Autowired + private TagDAO tagDAO; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + PostDetails.class, + Post.class, + Tag.class, + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + Tag hibernate = new Tag(); + hibernate.setName("hibernate"); + tagDAO.persist(hibernate); + + Tag jpa = new Tag(); + jpa.setName("jpa"); + tagDAO.persist(jpa); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + + } + + @Test + public void test() { + Post newPost = forumService.newPost("High-Performance Java Persistence", "hibernate", "jpa"); + assertNotNull(newPost.getId()); + + List posts = forumService.findAllByTitle("High-Performance Java Persistence"); + assertEquals(1, posts.size()); + + Post post = forumService.findById(newPost.getId()); + assertEquals("High-Performance Java Persistence", post.getTitle()); + + PostDTO postDTO = forumService.getPostDTOById(newPost.getId()); + assertEquals("High-Performance Java Persistence", postDTO.getTitle()); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/config/NarayanaJTATransactionManagerConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/config/NarayanaJTATransactionManagerConfiguration.java new file mode 100644 index 000000000..8b70f0c2a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/config/NarayanaJTATransactionManagerConfiguration.java @@ -0,0 +1,170 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.narayana.config; + +import com.arjuna.ats.internal.jta.recovery.arjunacore.XARecoveryModule; +import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple; +import com.arjuna.ats.internal.jta.transaction.arjunacore.UserTransactionImple; +import com.atomikos.jdbc.AtomikosDataSourceBean; +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.logging.LoggingStatementInspector; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import dev.snowdrop.boot.narayana.core.jdbc.GenericXADataSourceWrapper; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.engine.transaction.jta.platform.internal.JBossStandAloneJtaPlatform; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.postgresql.xa.PGXADataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.*; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.jta.JtaTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.Arrays; +import java.util.Collections; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +@PropertySource({"/META-INF/jta-postgresql.properties"}) +@ComponentScan(basePackages = "com.vladmihalcea.hpjp.spring.transaction.jta.narayana") +@EnableTransactionManagement +@EnableAspectJAutoProxy +public class NarayanaJTATransactionManagerConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + @Value("${jdbc.dataSourceClassName}") + protected String dataSourceClassName; + + @Value("${jdbc.username}") + protected String jdbcUser; + + @Value("${jdbc.password}") + protected String jdbcPassword; + + @Value("${jdbc.database}") + protected String jdbcDatabase; + + @Value("${jdbc.host}") + protected String jdbcHost; + + @Value("${jdbc.port}") + protected String jdbcPort; + + @Bean + public static PropertySourcesPlaceholderConfigurer properties() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @DependsOn("actualDataSource") + public DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + return ProxyDataSourceBuilder + .create(actualDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + } + + @Bean + public DataSource actualDataSource() { + try { + PGXADataSource dataSource = new PGXADataSource(); + dataSource.setUrl( + String.format( + "jdbc:postgresql://%s:%s/%s", + jdbcHost, + jdbcPort, + jdbcDatabase + ) + ); + dataSource.setUser(jdbcUser); + dataSource.setPassword(jdbcPassword); + + XARecoveryModule xaRecoveryModule = new XARecoveryModule(); + GenericXADataSourceWrapper wrapper = new GenericXADataSourceWrapper(xaRecoveryModule); + return wrapper.wrapDataSource(dataSource); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + localContainerEntityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + localContainerEntityManagerFactoryBean.setJtaDataSource(dataSource()); + localContainerEntityManagerFactoryBean.setPackagesToScan(packagesToScan()); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + localContainerEntityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + localContainerEntityManagerFactoryBean.setJpaProperties(additionalProperties()); + return localContainerEntityManagerFactoryBean; + } + + @Bean + public UserTransactionImple jtaUserTransaction() { + UserTransactionImple userTransactionManager = new UserTransactionImple(); + return userTransactionManager; + } + + @Bean + public TransactionManagerImple jtaTransactionManager() { + TransactionManagerImple transactionManager = new TransactionManagerImple(); + return transactionManager; + } + + @Bean + public JtaTransactionManager transactionManager() { + JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(); + jtaTransactionManager.setTransactionManager(jtaTransactionManager()); + jtaTransactionManager.setUserTransaction(jtaUserTransaction()); + jtaTransactionManager.setAllowCustomIsolationLevels(true); + return jtaTransactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate() { + return new TransactionTemplate(transactionManager()); + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.put( + AvailableSettings.JTA_PLATFORM, + JBossStandAloneJtaPlatform.class + ); + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + properties.put( + "hibernate.session_factory.statement_inspector", + new LoggingStatementInspector("com.vladmihalcea.hpjp.hibernate.transaction") + ); + properties.put( + "hibernate.integrator_provider", + (IntegratorProvider) () -> Collections.singletonList( + new ClassImportIntegrator(Arrays.asList(PostDTO.class)) + ) + ); + return properties; + } + + protected String[] packagesToScan() { + return new String[]{ + "com.vladmihalcea.hpjp.hibernate.transaction.forum" + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/GenericDAO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/GenericDAO.java new file mode 100644 index 000000000..66ecfb99c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/GenericDAO.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.narayana.dao; + +import java.io.Serializable; + +/** + * @author Vlad Mihalcea + */ +public interface GenericDAO { + + T findById(ID id); + + T persist(T entity); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/GenericDAOImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/GenericDAOImpl.java new file mode 100644 index 000000000..d1af188c1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/GenericDAOImpl.java @@ -0,0 +1,38 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.narayana.dao; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.stereotype.Repository; + +import java.io.Serializable; + +/** + * @author Vlad Mihalcea + */ +@Repository +public abstract class GenericDAOImpl implements GenericDAO { + + @PersistenceContext + private EntityManager entityManager; + + private final Class entityClass; + + protected EntityManager getEntityManager() { + return entityManager; + } + + protected GenericDAOImpl(Class entityClass) { + this.entityClass = entityClass; + } + + @Override + public T findById(ID id) { + return entityManager.find(entityClass, id); + } + + @Override + public T persist(T entity) { + entityManager.persist(entity); + return entity; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/PostBatchDAO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/PostBatchDAO.java new file mode 100644 index 000000000..2c9ab4cf9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/PostBatchDAO.java @@ -0,0 +1,11 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.narayana.dao; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; + +/** + * @author Vlad Mihalcea + */ +public interface PostBatchDAO extends GenericDAO { + + void savePosts(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/PostBatchDAOImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/PostBatchDAOImpl.java new file mode 100644 index 000000000..b4c66ae88 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/PostBatchDAOImpl.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.narayana.dao; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceContextType; +import org.hibernate.Session; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Vlad Mihalcea + */ +@Repository +public class PostBatchDAOImpl extends GenericDAOImpl implements PostBatchDAO { + + @PersistenceContext(type = PersistenceContextType.EXTENDED) + private EntityManager entityManager; + + int entityCount = 10; + + protected PostBatchDAOImpl() { + super(Post.class); + } + + @Transactional + public void savePosts() { + entityManager.unwrap(Session.class).setJdbcBatchSize(10); + try { + for (long i = 0; i < entityCount; ++i) { + Post post = new Post(); + post.setTitle(String.format("Post nr %d", i)); + entityManager.persist(post); + } + } finally { + entityManager.unwrap(Session.class).setJdbcBatchSize(null); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/PostDAO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/PostDAO.java new file mode 100644 index 000000000..304fab1ba --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/PostDAO.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.narayana.dao; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface PostDAO extends GenericDAO { + + List findByTitle(String title); + + PostDTO getPostDTOById(Long id); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/PostDAOImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/PostDAOImpl.java new file mode 100644 index 000000000..b32b6ca28 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/PostDAOImpl.java @@ -0,0 +1,40 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.narayana.dao; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public class PostDAOImpl extends GenericDAOImpl implements PostDAO { + + protected PostDAOImpl() { + super(Post.class); + } + + @Override + public List findByTitle(String title) { + return getEntityManager() + .createQuery( + "select p " + + "from Post p " + + "where p.title = :title", Post.class) + .setParameter("title", title) + .getResultList(); + } + + @Override + public PostDTO getPostDTOById(Long id) { + return getEntityManager() + .createQuery( + "select new PostDTO(p.id, p.title) " + + "from Post p " + + "where p.id = :id", PostDTO.class) + .setParameter("id", id) + .getSingleResult(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/TagDAO.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/TagDAO.java new file mode 100644 index 000000000..032aa4072 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/TagDAO.java @@ -0,0 +1,13 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.narayana.dao; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public interface TagDAO extends GenericDAO { + + List findByName(String... tags); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/TagDAOImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/TagDAOImpl.java new file mode 100644 index 000000000..596db6c7c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/dao/TagDAOImpl.java @@ -0,0 +1,31 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.narayana.dao; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; +import org.springframework.stereotype.Repository; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Repository +public class TagDAOImpl extends GenericDAOImpl implements TagDAO { + + protected TagDAOImpl() { + super(Tag.class); + } + + @Override + public List findByName(String... tags) { + if(tags.length == 0) { + throw new IllegalArgumentException("There's no tag name to search for!"); + } + return getEntityManager().createQuery( + "select t " + + "from Tag t " + + "where t.name in :tags", Tag.class) + .setParameter("tags", Arrays.asList(tags)) + .getResultList(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/service/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/service/ForumService.java new file mode 100644 index 000000000..c1fa57576 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/service/ForumService.java @@ -0,0 +1,21 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.narayana.service; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Service +public interface ForumService { + + Post newPost(String title, String... tags); + + List findAllByTitle(String title); + + Post findById(Long id); + + PostDTO getPostDTOById(Long id);} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/service/ForumServiceImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/service/ForumServiceImpl.java new file mode 100644 index 000000000..eff41e87a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/jta/narayana/service/ForumServiceImpl.java @@ -0,0 +1,74 @@ +package com.vladmihalcea.hpjp.spring.transaction.jta.narayana.service; + +import com.vladmihalcea.hpjp.hibernate.forum.dto.PostDTO; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import com.vladmihalcea.hpjp.spring.transaction.jta.narayana.dao.PostDAO; +import com.vladmihalcea.hpjp.spring.transaction.jta.narayana.dao.TagDAO; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +@Service +public class ForumServiceImpl implements ForumService { + + @Autowired + private PostDAO postDAO; + + @Autowired + private TagDAO tagDAO; + + @PersistenceContext + private EntityManager entityManager; + + @Override + @Transactional + public Post newPost(String title, String... tags) { + Post post = new Post(); + post.setTitle(title); + post.getTags().addAll(tagDAO.findByName(tags)); + return postDAO.persist(post); + } + + @Override + @Transactional(readOnly = true) + public List findAllByTitle(String title) { + return postDAO.findByTitle(title); + } + + @Override + @Transactional + public Post findById(Long id) { + Post post = postDAO.findById(id); + + org.hibernate.engine.spi.PersistenceContext persistenceContext = getHibernatePersistenceContext(); + + EntityEntry entityEntry = persistenceContext.getEntry(post); + assertNotNull(entityEntry.getLoadedState()); + + return post; + } + + @Override + @Transactional(readOnly = true) + public PostDTO getPostDTOById(Long id) { + return postDAO.getPostDTOById(id); + } + + private org.hibernate.engine.spi.PersistenceContext getHibernatePersistenceContext() { + SharedSessionContractImplementor session = entityManager.unwrap( + SharedSessionContractImplementor.class + ); + return session.getPersistenceContext(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/mdc/SpringMdcTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/mdc/SpringMdcTest.java new file mode 100644 index 000000000..246571d97 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/mdc/SpringMdcTest.java @@ -0,0 +1,57 @@ +package com.vladmihalcea.hpjp.spring.transaction.mdc; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.common.domain.Post; +import com.vladmihalcea.hpjp.spring.common.domain.PostComment; +import com.vladmihalcea.hpjp.spring.common.service.ForumService; +import com.vladmihalcea.hpjp.spring.transaction.mdc.config.TransactionInfoMdcConfiguration; +import org.junit.Test; +import org.slf4j.MDC; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = TransactionInfoMdcConfiguration.class) +public class SpringMdcTest extends AbstractSpringTest { + + @Autowired + private ForumService forumService; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + Post.class, + }; + } + + @Test + public void testAutoMDC() { + Post post = forumService.createPost( + "High-Performance Java Persistence", + "high-performance-java-persistence" + ); + + Long postId = post.getId(); + + forumService.addComment(postId, "Awesome"); + } + + @Test + public void testManualMDC() { + try(MDC.MDCCloseable mdc = MDC + .putCloseable( + "txId", + String.format( + " Persistence Context Id: [%d], DB Transaction Id: [%s]", + 123456, + 7890 + ) + ) + ) { + LOGGER.info("Fetch Post by title"); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/mdc/config/TransactionInfoMdcConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/mdc/config/TransactionInfoMdcConfiguration.java new file mode 100644 index 000000000..3942addaf --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/mdc/config/TransactionInfoMdcConfiguration.java @@ -0,0 +1,32 @@ +package com.vladmihalcea.hpjp.spring.transaction.mdc.config; + +import com.vladmihalcea.hpjp.spring.common.config.CommonSpringConfiguration; +import com.vladmihalcea.hpjp.spring.transaction.mdc.event.TransactionInfoSessionEventListener; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.hibernate.cfg.AvailableSettings; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +public class TransactionInfoMdcConfiguration extends CommonSpringConfiguration { + + @Bean + public Database database() { + return Database.POSTGRESQL; + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.put( + AvailableSettings.AUTO_SESSION_EVENTS_LISTENER, + TransactionInfoSessionEventListener.class.getName() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/mdc/event/TransactionInfo.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/mdc/event/TransactionInfo.java new file mode 100644 index 000000000..3ac85ab39 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/mdc/event/TransactionInfo.java @@ -0,0 +1,48 @@ +package com.vladmihalcea.hpjp.spring.transaction.mdc.event; + +import com.vladmihalcea.hpjp.util.TsidUtils; +import org.slf4j.MDC; + +/** + * @author Vlad Mihalcea + */ +class TransactionInfo { + + private final Long persistenceContextId; + + private String transactionId; + + private MDC.MDCCloseable mdc; + + public TransactionInfo() { + this.persistenceContextId = TsidUtils.randomTsid().toLong(); + setMdc(); + } + + public boolean hasTransactionId() { + return transactionId != null; + } + + public TransactionInfo setTransactionId(String transactionId) { + this.transactionId = transactionId; + setMdc(); + return this; + } + + private void setMdc() { + this.mdc = MDC.putCloseable( + "txId", + String.format( + " Persistence Context Id: [%d], DB Transaction Id: [%s]", + persistenceContextId, + transactionId + ) + ); + } + + public void close() { + if(mdc != null) { + mdc.close(); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/mdc/event/TransactionInfoSessionEventListener.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/mdc/event/TransactionInfoSessionEventListener.java new file mode 100644 index 000000000..02e14a452 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/mdc/event/TransactionInfoSessionEventListener.java @@ -0,0 +1,84 @@ +package com.vladmihalcea.hpjp.spring.transaction.mdc.event; + +import com.vladmihalcea.hpjp.util.SpringTransactionUtils; +import com.vladmihalcea.hpjp.util.providers.Database; +import jakarta.persistence.EntityManager; +import org.hibernate.BaseSessionEventListener; +import org.hibernate.Session; +import org.hibernate.engine.spi.SessionFactoryImplementor; + +import java.sql.ResultSet; +import java.sql.Statement; + +/** + * @author Vlad Mihalcea + */ +public class TransactionInfoSessionEventListener + extends BaseSessionEventListener { + + private final TransactionInfo transactionInfo; + + private EntityManager entityManager; + + /** + * Executes when a JPA EntityManager is created. + */ + public TransactionInfoSessionEventListener() { + transactionInfo = new TransactionInfo(); + } + + /** + * Executes after a JDBC statement was executed. + */ + @Override + public void jdbcExecuteStatementEnd() { + if (!transactionInfo.hasTransactionId()) { + resolveTransactionId(); + } + } + + /** + * Executes after the commit or rollback was called + * on the JPA EntityTransaction. + */ + @Override + public void transactionCompletion(boolean successful) { + transactionInfo.setTransactionId(null); + } + + /** + * Executes after JPA EntityManager is closed. + */ + @Override + public void end() { + transactionInfo.close(); + } + + private EntityManager getEntityManager() { + if (entityManager == null) { + entityManager = SpringTransactionUtils.currentEntityManager(); + } + return entityManager; + } + + private void resolveTransactionId() { + EntityManager entityManager = getEntityManager(); + SessionFactoryImplementor sessionFactory = entityManager + .getEntityManagerFactory() + .unwrap(SessionFactoryImplementor.class); + + entityManager.unwrap(Session.class).doWork(connection -> { + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery( + Database.of(sessionFactory.getJdbcServices().getDialect()) + .dataSourceProvider() + .queries() + .transactionId() + )) { + if (resultSet.next()) { + transactionInfo.setTransactionId(resultSet.getString(1)); + } + } + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/SpringDataJPAReadOnlyLazyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/SpringDataJPAReadOnlyLazyTest.java new file mode 100644 index 000000000..d88a93550 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/SpringDataJPAReadOnlyLazyTest.java @@ -0,0 +1,74 @@ +package com.vladmihalcea.hpjp.spring.transaction.readonly; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.transaction.readonly.config.SpringDataJPAReadOnlyLazyConfiguration; +import com.vladmihalcea.hpjp.spring.transaction.readonly.domain.Product; +import com.vladmihalcea.hpjp.spring.transaction.readonly.service.ProductService; +import com.vladmihalcea.hpjp.spring.transaction.readonly.service.fxrate.FxCurrency; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.math.BigDecimal; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAReadOnlyLazyConfiguration.class) +public class SpringDataJPAReadOnlyLazyTest extends AbstractSpringTest { + + @Autowired + private ProductService productService; + + @Override + protected Class[] entities() { + return new Class[]{ + Product.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + entityManager.persist( + new Product() + .setId(1L) + .setName("High-Performance Java Persistence eBook") + .setPrice(BigDecimal.valueOf(24.9)) + .setCurrency(FxCurrency.USD) + ); + + entityManager.persist( + new Product() + .setId(2L) + .setName("Hypersistence Optimizer") + .setPrice(BigDecimal.valueOf(49)) + .setCurrency(FxCurrency.USD) + ); + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void testReadOnly() { + Product ebook = productService.getAsCurrency(1L, FxCurrency.EUR); + assertEquals(FxCurrency.EUR, ebook.getCurrency()); + LOGGER.info("The book price is {} {}", ebook.getPrice(), ebook.getCurrency()); + } + + @Test + public void testReadWrite() { + Product ebook = productService.convertToCurrency(1L, FxCurrency.EUR); + assertEquals(FxCurrency.EUR, ebook.getCurrency()); + LOGGER.info("The book price is {} {}", ebook.getPrice(), ebook.getCurrency()); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/SpringDataJPAReadOnlyTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/SpringDataJPAReadOnlyTest.java new file mode 100644 index 000000000..8bc213dfb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/SpringDataJPAReadOnlyTest.java @@ -0,0 +1,74 @@ +package com.vladmihalcea.hpjp.spring.transaction.readonly; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.transaction.readonly.config.SpringDataJPAReadOnlyConfiguration; +import com.vladmihalcea.hpjp.spring.transaction.readonly.domain.Product; +import com.vladmihalcea.hpjp.spring.transaction.readonly.service.ProductService; +import com.vladmihalcea.hpjp.spring.transaction.readonly.service.fxrate.FxCurrency; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.math.BigDecimal; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = SpringDataJPAReadOnlyConfiguration.class) +public class SpringDataJPAReadOnlyTest extends AbstractSpringTest { + + @Autowired + private ProductService productService; + + @Override + protected Class[] entities() { + return new Class[]{ + Product.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + entityManager.persist( + new Product() + .setId(1L) + .setName("High-Performance Java Persistence eBook") + .setPrice(BigDecimal.valueOf(24.9)) + .setCurrency(FxCurrency.USD) + ); + + entityManager.persist( + new Product() + .setId(2L) + .setName("Hypersistence Optimizer") + .setPrice(BigDecimal.valueOf(49)) + .setCurrency(FxCurrency.USD) + ); + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + } + + @Test + public void testReadOnly() { + Product ebook = productService.getAsCurrency(1L, FxCurrency.EUR); + assertEquals(FxCurrency.EUR, ebook.getCurrency()); + LOGGER.info("The book price is {} {}", ebook.getPrice(), ebook.getCurrency()); + } + + @Test + public void testReadWrite() { + Product ebook = productService.convertToCurrency(1L, FxCurrency.EUR); + assertEquals(FxCurrency.EUR, ebook.getCurrency()); + LOGGER.info("The book price is {} {}", ebook.getPrice(), ebook.getCurrency()); + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/SpringDataJPAReadOnlyConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/SpringDataJPAReadOnlyConfiguration.java new file mode 100644 index 000000000..b18d965dd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/SpringDataJPAReadOnlyConfiguration.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.spring.transaction.readonly.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.transaction.readonly.config.stats.SpringTransactionStatisticsFactory; +import com.vladmihalcea.hpjp.spring.transaction.readonly.domain.Product; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.StatisticsSettings; +import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; +import org.hibernate.stat.internal.StatisticsInitiator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.web.client.RestTemplate; + +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.transaction.readonly", + } +) +@EnableJpaRepositories( + basePackages = { + "com.vladmihalcea.hpjp.spring.transaction.readonly.repository" + } +) +public class SpringDataJPAReadOnlyConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Product.class.getPackageName(); + } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.setProperty( + AvailableSettings.CONNECTION_PROVIDER_DISABLES_AUTOCOMMIT, + Boolean.TRUE.toString() + ); + properties.setProperty( + AvailableSettings.GENERATE_STATISTICS, + Boolean.TRUE.toString() + ); + properties.setProperty( + StatisticsSettings.STATS_BUILDER, + SpringTransactionStatisticsFactory.class.getName() + ); + properties.setProperty( + AvailableSettings.CONNECTION_HANDLING, + PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION.name() + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/SpringDataJPAReadOnlyLazyConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/SpringDataJPAReadOnlyLazyConfiguration.java new file mode 100644 index 000000000..7859bb2b7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/SpringDataJPAReadOnlyLazyConfiguration.java @@ -0,0 +1,67 @@ +package com.vladmihalcea.hpjp.spring.transaction.readonly.config; + +import com.vladmihalcea.hpjp.spring.data.base.config.SpringDataJPABaseConfiguration; +import com.vladmihalcea.hpjp.spring.transaction.readonly.config.stats.SpringTransactionStatisticsFactory; +import com.vladmihalcea.hpjp.spring.transaction.readonly.domain.Product; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.StatisticsSettings; +import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; +import org.springframework.web.client.RestTemplate; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.transaction.readonly", + } +) +@EnableJpaRepositories( + basePackages = { + "com.vladmihalcea.hpjp.spring.transaction.readonly.repository" + } +) +public class SpringDataJPAReadOnlyLazyConfiguration extends SpringDataJPABaseConfiguration { + + @Override + protected String packageToScan() { + return Product.class.getPackageName(); + } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Override + protected void additionalProperties(Properties properties) { + super.additionalProperties(properties); + properties.setProperty( + AvailableSettings.CONNECTION_PROVIDER_DISABLES_AUTOCOMMIT, + Boolean.TRUE.toString() + ); + properties.setProperty( + AvailableSettings.GENERATE_STATISTICS, + Boolean.TRUE.toString() + ); + properties.setProperty( + StatisticsSettings.STATS_BUILDER, + SpringTransactionStatisticsFactory.class.getName() + ); + } + + @Override + public DataSource dataSource(){ + return new LazyConnectionDataSourceProxy(super.dataSource()); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/stats/SpringTransactionStatistics.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/stats/SpringTransactionStatistics.java new file mode 100644 index 000000000..e3e29b3c0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/stats/SpringTransactionStatistics.java @@ -0,0 +1,51 @@ +package com.vladmihalcea.hpjp.spring.transaction.readonly.config.stats; + + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.stat.internal.StatisticsImpl; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author Vlad Mihalcea + */ +public class SpringTransactionStatistics extends StatisticsImpl { + + private static final ThreadLocal reportHolder = new ThreadLocal<>(); + + private static final ThreadLocal startNanos = ThreadLocal.withInitial(AtomicLong::new); + + private SpringTransactionStatisticsReport report; + + public SpringTransactionStatistics(SessionFactoryImplementor sessionFactory) { + super(sessionFactory); + } + + @Override + public void openSession() { + super.openSession(); + report = new SpringTransactionStatisticsReport(); + reportHolder.set(report); + } + + @Override + public void connect() { + startNanos.get().compareAndSet(0, System.nanoTime()); + super.connect(); + } + + @Override + public void endTransaction(boolean success) { + try { + report.transactionTime(System.nanoTime() - startNanos.get().get()); + report.generate(); + } finally { + startNanos.remove(); + } + super.endTransaction(success); + } + + public static SpringTransactionStatisticsReport report() { + return reportHolder.get(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/stats/SpringTransactionStatisticsFactory.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/stats/SpringTransactionStatisticsFactory.java new file mode 100644 index 000000000..9935fb4ef --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/stats/SpringTransactionStatisticsFactory.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.spring.transaction.readonly.config.stats; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.stat.spi.StatisticsFactory; +import org.hibernate.stat.spi.StatisticsImplementor; + +/** + * @author Vlad Mihalcea + */ +public class SpringTransactionStatisticsFactory implements StatisticsFactory { + + @Override + public StatisticsImplementor buildStatistics(SessionFactoryImplementor sessionFactory) { + return new SpringTransactionStatistics(sessionFactory); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/stats/SpringTransactionStatisticsReport.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/stats/SpringTransactionStatisticsReport.java new file mode 100644 index 000000000..707b5d615 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/config/stats/SpringTransactionStatisticsReport.java @@ -0,0 +1,41 @@ +package com.vladmihalcea.hpjp.spring.transaction.readonly.config.stats; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * @author Vlad Mihalcea + */ +public class SpringTransactionStatisticsReport { + + public static Logger LOGGER = LoggerFactory.getLogger(SpringTransactionStatisticsReport.class); + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .build(); + + private Timer transactionTimer = metricRegistry. + timer("transactionTimer"); + + private Timer fxRateTimer = metricRegistry.timer("fxRateTimer"); + + public void transactionTime(long nanos) { + transactionTimer.update(nanos, TimeUnit.NANOSECONDS); + } + + public void fxRateTime(long nanos) { + fxRateTimer.update(nanos, TimeUnit.NANOSECONDS); + } + + public void generate() { + logReporter.report(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/domain/Product.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/domain/Product.java new file mode 100644 index 000000000..6428b1f4d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/domain/Product.java @@ -0,0 +1,73 @@ +package com.vladmihalcea.hpjp.spring.transaction.readonly.domain; + +import com.vladmihalcea.hpjp.spring.transaction.readonly.service.fxrate.FxCurrency; +import com.vladmihalcea.hpjp.spring.transaction.readonly.service.fxrate.FxRate; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; + +/** + * @author Vlad Mihalcea + */ +@Entity +@Table(name = "product") +public class Product { + + private static final MathContext CENTS = new MathContext(4, RoundingMode.HALF_UP); + + @Id + private Long id; + + private String name; + + private BigDecimal price; + + @Enumerated + private FxCurrency currency; + + public Long getId() { + return id; + } + + public Product setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Product setName(String title) { + this.name = title; + return this; + } + + public BigDecimal getPrice() { + return price; + } + + public Product setPrice(BigDecimal priceInCents) { + this.price = priceInCents; + return this; + } + + public FxCurrency getCurrency() { + return currency; + } + + public Product setCurrency(FxCurrency currency) { + this.currency = currency; + return this; + } + + public void convertTo(FxCurrency currency, FxRate fxRate) { + setPrice(price.multiply(fxRate.convert(this.currency, currency), CENTS)); + setCurrency(currency); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/repository/ProductRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/repository/ProductRepository.java new file mode 100644 index 000000000..8e2cb93ba --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/repository/ProductRepository.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.spring.transaction.readonly.repository; + +import com.vladmihalcea.hpjp.spring.transaction.readonly.domain.Product; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vlad Mihalcea + */ +@Repository +public interface ProductRepository extends JpaRepository { +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/service/ProductService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/service/ProductService.java new file mode 100644 index 000000000..5591c9b59 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/service/ProductService.java @@ -0,0 +1,72 @@ +package com.vladmihalcea.hpjp.spring.transaction.readonly.service; + +import com.vladmihalcea.hpjp.spring.transaction.readonly.config.stats.SpringTransactionStatistics; +import com.vladmihalcea.hpjp.spring.transaction.readonly.domain.Product; +import com.vladmihalcea.hpjp.spring.transaction.readonly.repository.ProductRepository; +import com.vladmihalcea.hpjp.spring.transaction.readonly.service.fxrate.FxCurrency; +import com.vladmihalcea.hpjp.spring.transaction.readonly.service.fxrate.FxRate; +import com.vladmihalcea.hpjp.spring.transaction.readonly.service.fxrate.FxRateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ProductService { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + private final RestTemplate restTemplate; + + private ProductRepository productRepository; + + public ProductService( + @Autowired RestTemplate restTemplate, + @Autowired ProductRepository productRepository) { + this.restTemplate = restTemplate; + this.productRepository = productRepository; + } + + @Transactional(readOnly = true) + public Product getAsCurrency(Long productId, FxCurrency currency) { + FxRate fxRate = getFxRate(); + Product product = productRepository.findById(productId).orElseThrow(); + FxCurrency productCurrency = product.getCurrency(); + if (!productCurrency.equals(currency)) { + product.convertTo(currency, fxRate); + } + return product; + } + + @Transactional + public Product convertToCurrency(Long productId, FxCurrency currency) { + return getAsCurrency(productId, currency); + } + + private FxRate getFxRate() { + long startNanos = System.nanoTime(); + String fxRateXmlString = restTemplate.getForObject(FxRateUtil.FX_RATE_XML_URL, String.class); + FxRate fxRate = null; + if (fxRateXmlString != null) { + fxRate = FxRateUtil.parseFxRate( + fxRateXmlString.getBytes( + StandardCharsets.UTF_8 + ) + ); + } + LOGGER.debug( + "FxRate loading took: [{}] ms", + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos) + ); + return fxRate; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/service/fxrate/FxCurrency.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/service/fxrate/FxCurrency.java new file mode 100644 index 000000000..ff1b8ccf2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/service/fxrate/FxCurrency.java @@ -0,0 +1,105 @@ +package com.vladmihalcea.hpjp.spring.transaction.readonly.service.fxrate; + +import java.util.Locale; + +/** + * @author Vlad Mihalcea + */ +public enum FxCurrency { + USD("American dollar"), + EUR("EURO"), + GBP("Pound sterling"), + CHF("Swiss Franc"), + DKK("Danish rigsdaler"), + SEK("Swedish riksdaler"), + NOK("Norwegian speciedaler"), + HUF("Hungarian pengő"), + CZK("Czech koruna"), + PLN("Polish marka"), + BGN("Bulgarian lev"), + HRK("Croatian kuna"), + RON("Romanian leu"), + ; + + private String description; + + FxCurrency(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + public static FxCurrency resolve(String token) { + FxCurrency fxCurrency = resolveOrNull(token); + if(fxCurrency != null) { + return fxCurrency; + } else { + throw new IllegalArgumentException( + String.format("The [%s] currency is not supported!", token) + ); + } + } + + public static FxCurrency resolveOrNull(String token) { + for(FxCurrency currency : values()) { + if(currency.name().toLowerCase(Locale.ROOT).contains(token.toLowerCase(Locale.ROOT))) { + return currency; + } + } + return null; + } + + public static FxCurrency ofCountryCode(String countryCode) { + switch (countryCode) { + case "AT": + case "BE": + case "CY": + case "EE": + case "ES": + case "FI": + case "FR": + case "DE": + case "GR": + case "IE": + case "IT": + case "LV": + case "LT": + case "LU": + case "MT": + case "NL": + case "PT": + case "SK": + case "SI": + return EUR; + case "BG": + return BGN; + case "HR": + return HRK; + case "CZ": + return CZK; + case "DK": + return DKK; + case "GB": + return GBP; + case "HU": + return HUF; + case "CH": + case "LI": + return CHF; + case "NO": + return NOK; + case "PL": + return PLN; + case "RO": + return RON; + case "SE": + return SEK; + case "US": + return USD; + default: + return null; + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/service/fxrate/FxRate.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/service/fxrate/FxRate.java new file mode 100644 index 000000000..2cf6678ac --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/service/fxrate/FxRate.java @@ -0,0 +1,179 @@ +package com.vladmihalcea.hpjp.spring.transaction.readonly.service.fxrate; + +import com.vladmihalcea.hpjp.util.ReflectionUtils; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; + +public class FxRate { + + private LocalDate date; + + private BigDecimal USD; + + private BigDecimal EUR; + + private BigDecimal GBP; + + private BigDecimal CHF; + + private BigDecimal DKK; + + private BigDecimal SEK; + + private BigDecimal NOK; + + private BigDecimal HUF; + + private BigDecimal CZK; + + private BigDecimal PLN; + + private BigDecimal BGN; + + private BigDecimal HRK; + + private BigDecimal RON = BigDecimal.ONE; + + public LocalDate getDate() { + return date; + } + + public FxRate setDate(LocalDate date) { + this.date = date; + return this; + } + + public BigDecimal getUSD() { + return USD; + } + + public FxRate setUSD(BigDecimal USD) { + this.USD = USD; + return this; + } + + public BigDecimal getEUR() { + return EUR; + } + + public FxRate setEUR(BigDecimal EUR) { + this.EUR = EUR; + return this; + } + + public BigDecimal getGBP() { + return GBP; + } + + public FxRate setGBP(BigDecimal GBP) { + this.GBP = GBP; + return this; + } + + public BigDecimal getCHF() { + return CHF; + } + + public FxRate setCHF(BigDecimal CHF) { + this.CHF = CHF; + return this; + } + + public BigDecimal getDKK() { + return DKK; + } + + public FxRate setDKK(BigDecimal DKK) { + this.DKK = DKK; + return this; + } + + public BigDecimal getSEK() { + return SEK; + } + + public FxRate setSEK(BigDecimal SEK) { + this.SEK = SEK; + return this; + } + + public BigDecimal getNOK() { + return NOK; + } + + public FxRate setNOK(BigDecimal NOK) { + this.NOK = NOK; + return this; + } + + public BigDecimal getHUF() { + return HUF; + } + + public FxRate setHUF(BigDecimal HUF) { + this.HUF = HUF; + return this; + } + + public BigDecimal getCZK() { + return CZK; + } + + public FxRate setCZK(BigDecimal CZK) { + this.CZK = CZK; + return this; + } + + public BigDecimal getPLN() { + return PLN; + } + + public FxRate setPLN(BigDecimal PLN) { + this.PLN = PLN; + return this; + } + + public BigDecimal getBGN() { + return BGN; + } + + public FxRate setBGN(BigDecimal BGN) { + this.BGN = BGN; + return this; + } + + public BigDecimal getHRK() { + return HRK; + } + + public FxRate setHRK(BigDecimal HRK) { + this.HRK = HRK; + return this; + } + + public BigDecimal getRON() { + return RON; + } + + public FxRate setRON(BigDecimal RON) { + this.RON = RON; + return this; + } + + public BigDecimal convert(FxCurrency from, FxCurrency to) { + BigDecimal fromRate = ReflectionUtils.getFieldValue(this, from.name()); + BigDecimal toRate = ReflectionUtils.getFieldValue(this, to.name()); + return fromRate.divide(toRate, 4, RoundingMode.HALF_UP); + } + + public void setRate(String currency, BigDecimal fxRateValue) { + Method setter = ReflectionUtils.getSetterOrNull(this, currency, fxRateValue.getClass()); + if (setter != null) { + ReflectionUtils.invokeMethod(this, setter, fxRateValue); + } + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/service/fxrate/FxRateUtil.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/service/fxrate/FxRateUtil.java new file mode 100644 index 000000000..1631abc17 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/readonly/service/fxrate/FxRateUtil.java @@ -0,0 +1,50 @@ +package com.vladmihalcea.hpjp.spring.transaction.readonly.service.fxrate; + +import com.vladmihalcea.hpjp.util.XmlUtils; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public class FxRateUtil { + + public static final String FX_RATE_XML_URL = "https://www.bnr.ro/nbrfxrates.xml"; + + public static class Names { + public static final String CUBE = "Cube"; + public static final String DATE = "date"; + public static final String CURRENCY = "currency"; + public static final String MULTIPLIER = "multiplier"; + } + + public static FxRate parseFxRate(byte[] fxRateXmlBytes) { + FxRate fxRate = new FxRate(); + Document fxRateDocument = XmlUtils.readXmlDocument(fxRateXmlBytes); + NodeList cubes = fxRateDocument.getElementsByTagName(Names.CUBE); + if(cubes.getLength() > 0) { + Node cubeNode = cubes.item(0); + String date = cubeNode.getAttributes().getNamedItem(Names.DATE).getNodeValue(); + fxRate.setDate(LocalDate.parse(date)); + NodeList rateNodes = cubeNode.getChildNodes(); + for (int j = 0; j < rateNodes.getLength(); j++) { + Node rateNode = rateNodes.item(j); + if (!"Rate".equals(rateNode.getNodeName())) { + continue; + } + NamedNodeMap attributes = rateNode.getAttributes(); + String currency = attributes.getNamedItem(Names.CURRENCY).getNodeValue(); + BigDecimal fxRateValue = new BigDecimal(rateNode.getTextContent()); + Node multiplierAttribute = attributes.getNamedItem(Names.MULTIPLIER); + if (multiplierAttribute != null) { + fxRateValue = new BigDecimal(multiplierAttribute.getNodeValue()); + } + fxRate.setRate(currency, fxRateValue); + } + } + return fxRate; + } +} + diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/DataSourceType.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/DataSourceType.java new file mode 100644 index 000000000..50714d5ad --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/DataSourceType.java @@ -0,0 +1,9 @@ +package com.vladmihalcea.hpjp.spring.transaction.routing; + +/** + * @author Vlad Mihalcea + */ +public enum DataSourceType { + READ_WRITE, + READ_ONLY +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/ForumService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/ForumService.java new file mode 100644 index 000000000..6da4a3afb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/ForumService.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hpjp.spring.transaction.routing; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Service +public interface ForumService { + + Post newPost(String title, String... tags); + + List findAllPostsByTitle(String title); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/ForumServiceImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/ForumServiceImpl.java new file mode 100644 index 000000000..6a91bbf60 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/ForumServiceImpl.java @@ -0,0 +1,54 @@ +package com.vladmihalcea.hpjp.spring.transaction.routing; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@Service +@Transactional(readOnly = true) +public class ForumServiceImpl implements ForumService { + + @PersistenceContext + private EntityManager entityManager; + + @Override + @Transactional + public Post newPost(String title, String... tags) { + Post post = new Post(); + post.setTitle(title); + + post.getTags().addAll( + entityManager.createQuery(""" + select t + from Tag t + where t.name in :tags + """, Tag.class) + .setParameter("tags", Arrays.asList(tags)) + .getResultList() + ); + + entityManager.persist(post); + + return post; + } + + @Override + public List findAllPostsByTitle(String title) { + return entityManager.createQuery(""" + select p + from Post p + where p.title = :title + """, Post.class) + .setParameter("title", title) + .getResultList(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/TransactionRoutingConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/TransactionRoutingConfiguration.java new file mode 100644 index 000000000..332c24c0a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/TransactionRoutingConfiguration.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.vladmihalcea.hpjp.spring.transaction.routing; + +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.spring.config.jpa.AbstractJPAConfiguration; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +@Configuration +@ComponentScan(basePackages = "com.vladmihalcea.hpjp.spring.transaction.routing") +@PropertySource("/META-INF/jdbc-postgresql-replication.properties") +public class TransactionRoutingConfiguration extends AbstractJPAConfiguration { + + @Value("${jdbc.url.primary}") + private String primaryUrl; + + @Value("${jdbc.url.replica}") + private String replicaUrl; + + @Value("${jdbc.username}") + private String username; + + @Value("${jdbc.password}") + private String password; + + public TransactionRoutingConfiguration() { + super(Database.POSTGRESQL); + } + + @Bean + public DataSource readWriteDataSource() { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setURL(primaryUrl); + dataSource.setUser(username); + dataSource.setPassword(password); + return connectionPoolDataSource(dataSource); + } + + @Bean + public DataSource readOnlyDataSource() { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setURL(replicaUrl); + dataSource.setUser(username); + dataSource.setPassword(password); + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + statement.execute(""" + create sequence if not exists hibernate_sequence start 1 increment 1; + create table if not exists post (id int8 not null, title varchar(255), primary key (id)); + create table if not exists post_comment (id int8 not null, review varchar(255), post_id int8, primary key (id)); + create table if not exists post_details (created_by varchar(255), created_on timestamp, post_id int8 not null, primary key (post_id)); + create table if not exists post_tag (post_id int8 not null, tag_id int8 not null); + create table if not exists tag (id int8 not null, name varchar(255), primary key (id)); + alter table if exists post_comment drop constraint if exists FKna4y825fdc5hw8aow65ijexm0; + alter table if exists post_details drop constraint if exists FKmcgdm1k7iriyxsq4kukebj4ei; + alter table if exists post_tag drop constraint if exists FKac1wdchd2pnur3fl225obmlg0; + alter table if exists post_tag drop constraint if exists FKc2auetuvsec0k566l0eyvr9cs; + alter table if exists post_comment add constraint FKna4y825fdc5hw8aow65ijexm0 foreign key (post_id) references post; + alter table if exists post_details add constraint FKmcgdm1k7iriyxsq4kukebj4ei foreign key (post_id) references post; + alter table if exists post_tag add constraint FKac1wdchd2pnur3fl225obmlg0 foreign key (tag_id) references tag; + alter table if exists post_tag add constraint FKc2auetuvsec0k566l0eyvr9cs foreign key (post_id) references post; + """); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + return connectionPoolDataSource(dataSource); + } + + @Bean + public TransactionRoutingDataSource actualDataSource() { + TransactionRoutingDataSource routingDataSource = new TransactionRoutingDataSource(); + + Map dataSourceMap = new HashMap<>(); + dataSourceMap.put(DataSourceType.READ_WRITE, readWriteDataSource()); + dataSourceMap.put(DataSourceType.READ_ONLY, readOnlyDataSource()); + + routingDataSource.setTargetDataSources(dataSourceMap); + return routingDataSource; + } + + @Override + protected Properties additionalProperties() { + Properties properties = super.additionalProperties(); + properties.setProperty( + "hibernate.connection.provider_disables_autocommit", + Boolean.TRUE.toString() + ); + return properties; + } + + @Override + protected String[] packagesToScan() { + return new String[]{ + "com.vladmihalcea.hpjp.hibernate.transaction.forum" + }; + } + + protected HikariConfig hikariConfig(DataSource dataSource) { + HikariConfig hikariConfig = new HikariConfig(); + int cpuCores = Runtime.getRuntime().availableProcessors(); + hikariConfig.setMaximumPoolSize(cpuCores * 4); + hikariConfig.setDataSource(dataSource); + + hikariConfig.setAutoCommit(false); + return hikariConfig; + } + + protected HikariDataSource connectionPoolDataSource(DataSource dataSource) { + return new HikariDataSource(hikariConfig(dataSource)); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/TransactionRoutingDataSource.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/TransactionRoutingDataSource.java new file mode 100644 index 000000000..f54e56cc1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/TransactionRoutingDataSource.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hpjp.spring.transaction.routing; + +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * @author Vlad Mihalcea + */ +public class TransactionRoutingDataSource extends AbstractRoutingDataSource { + + @Nullable + @Override + protected Object determineCurrentLookupKey() { + return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? + DataSourceType.READ_ONLY : + DataSourceType.READ_WRITE; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/TransactionRoutingDataSourceTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/TransactionRoutingDataSourceTest.java new file mode 100644 index 000000000..cc9873679 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/routing/TransactionRoutingDataSourceTest.java @@ -0,0 +1,60 @@ +package com.vladmihalcea.hpjp.spring.transaction.routing; + +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Post; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.PostComment; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.PostDetails; +import com.vladmihalcea.hpjp.hibernate.transaction.forum.Tag; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; + +import java.util.List; + +@ContextConfiguration(classes = TransactionRoutingConfiguration.class) +public class TransactionRoutingDataSourceTest extends AbstractSpringTest { + + @Autowired + private ForumService forumService; + + @Override + protected Class[] entities() { + return new Class[]{ + PostComment.class, + PostDetails.class, + Post.class, + Tag.class, + }; + } + + @Override + public void afterInit() { + transactionTemplate.execute(status -> { + Tag jdbc = new Tag(); + jdbc.setName("JDBC"); + entityManager.persist(jdbc); + + Tag jpa = new Tag(); + jpa.setName("JPA"); + entityManager.persist(jpa); + + Tag hibernate = new Tag(); + hibernate.setName("Hibernate"); + entityManager.persist(hibernate); + + return null; + }); + } + + @Test + public void test() { + Post post = forumService.newPost( + "High-Performance Java Persistence", + "JDBC", "JPA", "Hibernate" + ); + + List posts = forumService.findAllPostsByTitle( + "High-Performance Java Persistence" + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/ACIDRaceConditionTransferTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/ACIDRaceConditionTransferTest.java new file mode 100644 index 000000000..1aa9503d9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/ACIDRaceConditionTransferTest.java @@ -0,0 +1,152 @@ +package com.vladmihalcea.hpjp.spring.transaction.transfer; + +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.transaction.transfer.config.ACIDRaceConditionTransferConfiguration; +import com.vladmihalcea.hpjp.spring.transaction.transfer.domain.Account; +import com.vladmihalcea.hpjp.spring.transaction.transfer.repository.AccountRepository; +import com.vladmihalcea.hpjp.spring.transaction.transfer.service.TransferService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.*; +import java.util.function.IntFunction; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = ACIDRaceConditionTransferConfiguration.class) +public class ACIDRaceConditionTransferTest extends AbstractSpringTest { + + @Autowired + private TransferService transferService; + + @Autowired + private AccountRepository accountRepository; + + @Override + protected Class[] entities() { + return new Class[]{ + Account.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + entityManager.persist( + new Account() + .setId("Alice-123") + .setOwner("Alice") + .setBalance(10) + ); + + entityManager.persist( + new Account() + .setId("Bob-456") + .setOwner("Bob") + .setBalance(0) + ); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + + } + + @Test + public void testSerialExecution() { + assertEquals(10L, accountRepository.getBalance("Alice-123")); + assertEquals(0L, accountRepository.getBalance("Bob-456")); + + transferService.transfer("Alice-123", "Bob-456", 5L); + + assertEquals(5L, accountRepository.getBalance("Alice-123")); + assertEquals(5L, accountRepository.getBalance("Bob-456")); + + transferService.transfer("Alice-123", "Bob-456", 5L); + + assertEquals(0L, accountRepository.getBalance("Alice-123")); + assertEquals(10L, accountRepository.getBalance("Bob-456")); + + transferService.transfer("Alice-123", "Bob-456", 5L); + + assertEquals(0L, accountRepository.getBalance("Alice-123")); + assertEquals(10L, accountRepository.getBalance("Bob-456")); + } + + //Maximum connection count is limited to 64 due to Hikari maximum pool size + private int threadCount = 16; + + @Test + public void testParallelExecution() throws InterruptedException { + assertEquals(10L, accountRepository.getBalance("Alice-123")); + assertEquals(0L, accountRepository.getBalance("Bob-456")); + + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch endLatch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + new Thread(() -> { + try { + startLatch.await(); + + transferService.transfer("Alice-123", "Bob-456", 5L); + } catch (Exception e) { + LOGGER.error("Transfer failed", e); + } finally { + endLatch.countDown(); + } + }).start(); + } + LOGGER.info("Starting threads"); + startLatch.countDown(); + endLatch.await(); + + LOGGER.info("Alice's balance: {}", accountRepository.getBalance("Alice-123")); + LOGGER.info("Bob's balance: {}", accountRepository.getBalance("Bob-456")); + } + + @Test + public void testParallelExecutionUsingExecutorService() throws InterruptedException { + + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + + assertEquals(10L, accountRepository.getBalance("Alice-123")); + assertEquals(0L, accountRepository.getBalance("Bob-456")); + + LOGGER.info("Starting threads"); + + Collection> callables = IntStream + .range(0, threadCount) + .mapToObj( i -> (Callable) () -> { + transferService.transfer("Alice-123", "Bob-456", 5L); + return null; + } + ) + .toList(); + + List> futures = executorService.invokeAll(callables); + for (Future future : futures) { + try { + future.get(); + } catch (InterruptedException| ExecutionException e) { + LOGGER.error(e.getMessage()); + } + } + + LOGGER.info("Alice's balance {}", accountRepository.getBalance("Alice-123")); + LOGGER.info("Bob's balance {}", accountRepository.getBalance("Bob-456")); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/FlexyPoolACIDRaceConditionTransferTest.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/FlexyPoolACIDRaceConditionTransferTest.java new file mode 100644 index 000000000..0929a9a65 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/FlexyPoolACIDRaceConditionTransferTest.java @@ -0,0 +1,112 @@ +package com.vladmihalcea.hpjp.spring.transaction.transfer; + +import com.vladmihalcea.flexypool.FlexyPoolDataSource; +import com.vladmihalcea.hpjp.spring.common.AbstractSpringTest; +import com.vladmihalcea.hpjp.spring.transaction.transfer.config.FlexyPoolACIDRaceConditionTransferConfiguration; +import com.vladmihalcea.hpjp.spring.transaction.transfer.domain.Account; +import com.vladmihalcea.hpjp.spring.transaction.transfer.repository.AccountRepository; +import com.vladmihalcea.hpjp.spring.transaction.transfer.service.TransferService; +import com.zaxxer.hikari.HikariDataSource; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.TransactionCallback; + +import javax.sql.DataSource; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@ContextConfiguration(classes = FlexyPoolACIDRaceConditionTransferConfiguration.class) +public class FlexyPoolACIDRaceConditionTransferTest extends AbstractSpringTest { + + @Autowired + private TransferService transferService; + + @Autowired + private AccountRepository accountRepository; + + @Autowired + private ProxyDataSource dataSource; + + @Override + protected Class[] entities() { + return new Class[]{ + Account.class + }; + } + + @Override + public void afterInit() { + try { + transactionTemplate.execute((TransactionCallback) transactionStatus -> { + entityManager.persist( + new Account() + .setId("Alice-123") + .setOwner("Alice") + .setBalance(10) + ); + + entityManager.persist( + new Account() + .setId("Bob-456") + .setOwner("Bob") + .setBalance(0) + ); + + return null; + }); + } catch (TransactionException e) { + LOGGER.error("Failure", e); + } + + } + + @Test + public void testParallelExecution() throws InterruptedException { + assertEquals(10L, accountRepository.getBalance("Alice-123")); + assertEquals(0L, accountRepository.getBalance("Bob-456")); + + long startNanos = System.nanoTime(); + int threadCount = 64; + + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch endLatch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + new Thread(() -> { + try { + startLatch.await(); + + transferService.transfer("Alice-123", "Bob-456", 5L); + } catch (Exception e) { + LOGGER.error("Transfer failed", e); + } finally { + endLatch.countDown(); + } + }).start(); + } + LOGGER.info("Starting threads"); + startLatch.countDown(); + endLatch.await(); + + FlexyPoolDataSource flexyPoolDataSource = (FlexyPoolDataSource) dataSource.getDataSource(); + HikariDataSource hikariDataSource = flexyPoolDataSource.getTargetDataSource(); + + LOGGER.info( + "The {} transfers were executed on {} database connections in {} ms", + threadCount, + hikariDataSource.getMaximumPoolSize(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos) + ); + + LOGGER.info("Alice's balance: {}", accountRepository.getBalance("Alice-123")); + LOGGER.info("Bob's balance: {}", accountRepository.getBalance("Bob-456")); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/config/ACIDRaceConditionTransferConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/config/ACIDRaceConditionTransferConfiguration.java new file mode 100644 index 000000000..6343de708 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/config/ACIDRaceConditionTransferConfiguration.java @@ -0,0 +1,120 @@ +package com.vladmihalcea.hpjp.spring.transaction.transfer.config; + +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import jakarta.persistence.EntityManagerFactory; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * + * @author Vlad Mihalcea + */ +@Configuration +@ComponentScan( + basePackages = { + "com.vladmihalcea.hpjp.spring.transaction.transfer.service", + } +) +@EnableJpaRepositories("com.vladmihalcea.hpjp.spring.transaction.transfer.repository") +@EnableTransactionManagement +@EnableAspectJAutoProxy +public class ACIDRaceConditionTransferConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + @Bean + public static PropertySourcesPlaceholderConfigurer properties() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Bean + public Database database() { + return Database.POSTGRESQL; + } + + @Bean + public DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + protected DataSource actualDataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setMaximumPoolSize(64); + hikariConfig.setAutoCommit(false); + hikariConfig.setDataSource(dataSourceProvider().dataSource()); + return new HikariDataSource(hikariConfig); + } + + @Bean + public DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + DataSource dataSource = ProxyDataSourceBuilder + .create(actualDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + return dataSource; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Autowired DataSource dataSource) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setPackagesToScan(packagesToScan()); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + entityManagerFactoryBean.setJpaProperties(additionalProperties()); + return entityManagerFactoryBean; + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { + return new TransactionTemplate(transactionManager(entityManagerFactory)); + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.setProperty("hibernate.dialect", dataSourceProvider().hibernateDialect()); + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + return properties; + } + + protected String[] packagesToScan() { + return new String[]{ + "com.vladmihalcea.hpjp.spring.transaction.transfer.domain" + }; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/config/FlexyPoolACIDRaceConditionTransferConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/config/FlexyPoolACIDRaceConditionTransferConfiguration.java new file mode 100644 index 000000000..8304892ad --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/config/FlexyPoolACIDRaceConditionTransferConfiguration.java @@ -0,0 +1,63 @@ +package com.vladmihalcea.hpjp.spring.transaction.transfer.config; + +import com.vladmihalcea.flexypool.FlexyPoolDataSource; +import com.vladmihalcea.flexypool.adaptor.HikariCPPoolAdapter; +import com.vladmihalcea.flexypool.config.FlexyPoolConfiguration; +import com.vladmihalcea.flexypool.strategy.IncrementPoolOnTimeoutConnectionAcquisitionStrategy; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import javax.sql.DataSource; + +/** + * + * @author Vlad Mihalcea + */ +public class FlexyPoolACIDRaceConditionTransferConfiguration extends ACIDRaceConditionTransferConfiguration { + + @Override + protected DataSource actualDataSource() { + System.setProperty("com.zaxxer.hikari.timeoutMs.floor", "5"); + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setDataSource(dataSourceProvider().dataSource()); + hikariConfig.setAutoCommit(false); + hikariConfig.setMaximumPoolSize(1); + hikariConfig.setConnectionTimeout(150); + HikariDataSource poolingDataSource = new HikariDataSource(hikariConfig); + + int maxOverflowPoolSize = 5; + int connectionAcquisitionThresholdMillis = 50; + FlexyPoolDataSource dataSource = new FlexyPoolDataSource<>( + new FlexyPoolConfiguration.Builder<>( + getClass().getSimpleName(), + poolingDataSource, + HikariCPPoolAdapter.FACTORY) + .build(), + new IncrementPoolOnTimeoutConnectionAcquisitionStrategy.Factory<>( + maxOverflowPoolSize, + connectionAcquisitionThresholdMillis + ) + ); + + return dataSource; + } + + /*@Override + protected DataSource actualDataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setDataSource(dataSourceProvider().dataSource()); + hikariConfig.setAutoCommit(false); + hikariConfig.setMaximumPoolSize(3); + HikariDataSource poolingDataSource = new HikariDataSource(hikariConfig); + + FlexyPoolDataSource dataSource = new FlexyPoolDataSource<>( + new FlexyPoolConfiguration.Builder<>( + getClass().getSimpleName(), + poolingDataSource, + HikariCPPoolAdapter.FACTORY) + .build() + ); + + return dataSource; + }*/ +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/domain/Account.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/domain/Account.java new file mode 100644 index 000000000..6eb56a692 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/domain/Account.java @@ -0,0 +1,51 @@ +package com.vladmihalcea.hpjp.spring.transaction.transfer.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * @author Vlad Mihalcea + */ +@Entity(name = "Account") +@Table(name = "account") +public class Account { + + @Id + private String id; + + private String owner; + + private long balance; + + @Version + private short version; + + public String getId() { + return id; + } + + public Account setId(String iban) { + this.id = iban; + return this; + } + + public String getOwner() { + return owner; + } + + public Account setOwner(String owner) { + this.owner = owner; + return this; + } + + public long getBalance() { + return balance; + } + + public Account setBalance(long balance) { + this.balance = balance; + return this; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/repository/AccountRepository.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/repository/AccountRepository.java new file mode 100644 index 000000000..b78533152 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/repository/AccountRepository.java @@ -0,0 +1,25 @@ +package com.vladmihalcea.hpjp.spring.transaction.transfer.repository; + +import com.vladmihalcea.hpjp.spring.transaction.transfer.domain.Account; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Vlad Mihalcea + */ +@Repository +@Transactional(readOnly = true) +public interface AccountRepository extends JpaRepository { + + @Query(value = "SELECT balance FROM account WHERE id = :id", nativeQuery = true) + long getBalance(@Param("id") String id); + + @Query(value = "UPDATE account SET balance = balance + :amount WHERE id = :id", nativeQuery = true) + @Modifying + @Transactional + int addToBalance(@Param("id") String id, @Param("amount") long cents); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/service/TransferService.java b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/service/TransferService.java new file mode 100644 index 000000000..9d9f669ee --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/spring/transaction/transfer/service/TransferService.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.spring.transaction.transfer.service; + +import com.vladmihalcea.hpjp.spring.transaction.transfer.domain.Account; +import com.vladmihalcea.hpjp.spring.transaction.transfer.repository.AccountRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Vlad Mihalcea + */ +@Service +public class TransferService { + + @Autowired + private AccountRepository accountRepository; + + @Transactional(isolation = Isolation.REPEATABLE_READ) + public void transfer(String sourceAccount, String destinationAccount, long amount) { + if(accountRepository.getBalance(sourceAccount) >= amount) { + accountRepository.addToBalance(sourceAccount, (-1) * amount); + accountRepository.addToBalance(destinationAccount, amount); + } + } + + @Transactional + public void transferOptimisticLocking(String fromIban, String toIban, long cents) { + Account fromAccount = accountRepository.findById(fromIban).orElse(null); + Account toAccount = accountRepository.findById(toIban).orElse(null); + long fromBalance = fromAccount.getBalance(); + + if(fromBalance >= cents) { + + fromAccount.setBalance(fromAccount.getBalance() - cents); + toAccount.setBalance(toAccount.getBalance() + cents); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractCockroachDBIntegrationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractCockroachDBIntegrationTest.java new file mode 100644 index 000000000..43e558e20 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractCockroachDBIntegrationTest.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.util; + +import com.vladmihalcea.hpjp.util.providers.Database; + +/** + * AbstractCockroachDBIntegrationTest - Abstract CockroachDB IntegrationTest + * + * @author Vlad Mihalcea + */ +public abstract class AbstractCockroachDBIntegrationTest extends AbstractTest { + + @Override + protected Database database() { + return Database.COCKROACHDB; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractMySQLIntegrationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractMySQLIntegrationTest.java new file mode 100644 index 000000000..50c646db4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractMySQLIntegrationTest.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.util; + +import com.vladmihalcea.hpjp.util.providers.Database; + +/** + * AbstractMySQLIntegrationTest - Abstract MySQL IntegrationTest + * + * @author Vlad Mihalcea + */ +public abstract class AbstractMySQLIntegrationTest extends AbstractTest { + + @Override + protected Database database() { + return Database.MYSQL; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractOracleIntegrationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractOracleIntegrationTest.java new file mode 100644 index 000000000..d9b889251 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractOracleIntegrationTest.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.util; + +import com.vladmihalcea.hpjp.util.providers.Database; + +/** + * AbstractOracleXEIntegrationTest - Abstract Orcale XE IntegrationTest + * + * @author Vlad Mihalcea + */ +public abstract class AbstractOracleIntegrationTest extends AbstractTest { + + @Override + protected Database database() { + return Database.ORACLE; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractPostgreSQLIntegrationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractPostgreSQLIntegrationTest.java new file mode 100644 index 000000000..f15a09e64 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractPostgreSQLIntegrationTest.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.util; + +import com.vladmihalcea.hpjp.util.providers.Database; + +/** + * AbstractPostgreSQLIntegrationTest - Abstract PostgreSQL IntegrationTest + * + * @author Vlad Mihalcea + */ +public abstract class AbstractPostgreSQLIntegrationTest extends AbstractTest { + + @Override + protected Database database() { + return Database.POSTGRESQL; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractSQLServerIntegrationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractSQLServerIntegrationTest.java new file mode 100644 index 000000000..282ed75a0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractSQLServerIntegrationTest.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.hpjp.util; + +import com.vladmihalcea.hpjp.util.providers.Database; + +/** + * AbstractSQLServerIntegrationTest - Abstract SQL Server IntegrationTest + * + * @author Vlad Mihalcea + */ +public abstract class AbstractSQLServerIntegrationTest extends AbstractTest { + + @Override + protected Database database() { + return Database.SQLSERVER; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractTest.java b/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractTest.java new file mode 100644 index 000000000..93a9a80d7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/AbstractTest.java @@ -0,0 +1,1268 @@ +package com.vladmihalcea.hpjp.util; + +import com.vladmihalcea.hpjp.util.exception.DataAccessException; +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.LockType; +import com.vladmihalcea.hpjp.util.transaction.*; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.EntityTransaction; +import jakarta.persistence.spi.PersistenceUnitInfo; +import net.steppschuh.markdowngenerator.table.Table; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.ehcache.core.spi.store.tiering.CachingTier; +import org.ehcache.impl.internal.store.tiering.TieredStore; +import org.ehcache.transactions.xa.internal.XAStore; +import org.hibernate.*; +import org.hibernate.boot.MetadataBuilder; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.SessionFactoryBuilder; +import org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl; +import org.hibernate.boot.registry.BootstrapServiceRegistry; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.cache.internal.QueryResultsCacheImpl; +import org.hibernate.cache.jcache.internal.JCacheAccessImpl; +import org.hibernate.cache.spi.entry.CollectionCacheEntry; +import org.hibernate.cache.spi.entry.StandardCacheEntryImpl; +import org.hibernate.cache.spi.support.*; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; +import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.hibernate.jpa.boot.spi.TypeContributorList; +import org.hibernate.stat.CacheRegionStatistics; +import org.hibernate.stat.Statistics; +import org.hibernate.usertype.UserType; +import org.junit.After; +import org.junit.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.io.Closeable; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.sql.*; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static org.junit.Assert.fail; + +public abstract class AbstractTest { + + public static final boolean ENABLE_LONG_RUNNING_TESTS = true; + + static { + Thread.currentThread().setName("Alice"); + } + + protected final ExecutorService executorService = Executors.newSingleThreadExecutor(r -> { + Thread bob = new Thread(r); + bob.setName("Bob"); + return bob; + }); + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + private DataSource dataSource; + + private EntityManagerFactory emf; + + private SessionFactory sf; + + private List closeables = new ArrayList<>(); + + @Before + public void init() { + beforeInit(); + if (nativeHibernateSessionFactoryBootstrap()) { + sf = newSessionFactory(); + } else { + emf = newEntityManagerFactory(); + } + afterInit(); + } + + protected void beforeInit() { + + } + + protected void afterInit() { + + } + + @After + public void destroy() { + if (nativeHibernateSessionFactoryBootstrap()) { + if (sf != null) { + sf.close(); + } + } else { + if (emf != null) { + emf.close(); + } + } + for (Closeable closeable : closeables) { + try { + closeable.close(); + } catch (IOException e) { + LOGGER.error("Failure", e); + } + } + closeables.clear(); + afterDestroy(); + } + + protected void afterDestroy() { + + } + + public EntityManagerFactory entityManagerFactory() { + return nativeHibernateSessionFactoryBootstrap() ? sf : emf; + } + + public SessionFactory sessionFactory() { + if (nativeHibernateSessionFactoryBootstrap()) { + return sf; + } + EntityManagerFactory entityManagerFactory = entityManagerFactory(); + if (entityManagerFactory == null) { + return null; + } + return entityManagerFactory.unwrap(SessionFactory.class); + } + + protected boolean nativeHibernateSessionFactoryBootstrap() { + return false; + } + + protected Class[] entities() { + return new Class[]{}; + } + + protected List entityClassNames() { + return Arrays.asList(entities()).stream().map(Class::getName).collect(Collectors.toList()); + } + + protected String[] packages() { + return null; + } + + protected String[] resources() { + return null; + } + + protected Interceptor interceptor() { + return null; + } + + private SessionFactory newSessionFactory() { + final BootstrapServiceRegistryBuilder bsrb = new BootstrapServiceRegistryBuilder() + .enableAutoClose(); + + Integrator integrator = integrator(); + if (integrator != null) { + bsrb.applyIntegrator(integrator); + } + + final BootstrapServiceRegistry bsr = bsrb.build(); + + final StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder(bsr) + .applySettings(properties()) + .build(); + + final MetadataSources metadataSources = new MetadataSources(serviceRegistry); + + for (Class annotatedClass : entities()) { + metadataSources.addAnnotatedClass(annotatedClass); + } + + String[] packages = packages(); + if (packages != null) { + for (String annotatedPackage : packages) { + metadataSources.addPackage(annotatedPackage); + } + } + + String[] resources = resources(); + if (resources != null) { + for (String resource : resources) { + metadataSources.addResource(resource); + } + } + + final MetadataBuilder metadataBuilder = metadataSources.getMetadataBuilder() + .applyImplicitNamingStrategy(ImplicitNamingStrategyLegacyJpaImpl.INSTANCE); + + final List> additionalTypes = additionalTypes(); + if (additionalTypes != null) { + additionalTypes.forEach(type -> { + metadataBuilder.applyTypes((typeContributions, sr) -> typeContributions.contributeType(type)); + }); + } + + additionalMetadata(metadataBuilder); + + MetadataImplementor metadata = (MetadataImplementor) metadataBuilder.build(); + + final SessionFactoryBuilder sfb = metadata.getSessionFactoryBuilder(); + Interceptor interceptor = interceptor(); + if (interceptor != null) { + sfb.applyInterceptor(interceptor); + } + + return sfb.build(); + } + + private SessionFactory newLegacySessionFactory() { + Properties properties = properties(); + Configuration configuration = new Configuration().addProperties(properties); + for (Class entityClass : entities()) { + configuration.addAnnotatedClass(entityClass); + } + String[] packages = packages(); + if (packages != null) { + for (String scannedPackage : packages) { + configuration.addPackage(scannedPackage); + } + } + String[] resources = resources(); + if (resources != null) { + for (String resource : resources) { + configuration.addResource(resource); + } + } + Interceptor interceptor = interceptor(); + if (interceptor != null) { + configuration.setInterceptor(interceptor); + } + + final List> additionalTypes = additionalTypes(); + if (additionalTypes != null) { + configuration.registerTypeContributor( + (typeContributions, serviceRegistry) -> + additionalTypes.forEach(typeContributions::contributeType) + ); + } + return configuration.buildSessionFactory( + new StandardServiceRegistryBuilder() + .applySettings(properties) + .build() + ); + } + + protected EntityManagerFactory newEntityManagerFactory() { + PersistenceUnitInfo persistenceUnitInfo = persistenceUnitInfo(getClass().getSimpleName()); + Map configuration = properties(); + Interceptor interceptor = interceptor(); + if (interceptor != null) { + configuration.put(AvailableSettings.INTERCEPTOR, interceptor); + } + Integrator integrator = integrator(); + if (integrator != null) { + configuration.put("hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList(integrator)); + } + + List> additionalTypes = additionalTypes(); + if (additionalTypes != null) { + configuration.put("hibernate.type_contributors", + (TypeContributorList) () -> Collections.singletonList( + (typeContributions, serviceRegistry) -> { + additionalTypes.forEach(typeContributions::contributeType); + } + )); + } + + EntityManagerFactoryBuilderImpl entityManagerFactoryBuilder = new EntityManagerFactoryBuilderImpl( + new PersistenceUnitInfoDescriptor(persistenceUnitInfo), configuration + ); + return entityManagerFactoryBuilder.build(); + } + + protected Integrator integrator() { + return null; + } + + protected PersistenceUnitInfoImpl persistenceUnitInfo(String name) { + PersistenceUnitInfoImpl persistenceUnitInfo = new PersistenceUnitInfoImpl( + name, entityClassNames(), properties() + ); + String[] resources = resources(); + if (resources != null) { + persistenceUnitInfo.getMappingFileNames().addAll(Arrays.asList(resources)); + } + return persistenceUnitInfo; + } + + protected Properties properties() { + Properties properties = new Properties(); + //log settings + properties.put("hibernate.hbm2ddl.auto", "create-drop"); + properties.put("hibernate.dialect", dataSourceProvider().hibernateDialect()); + //data source settings + DataSource dataSource = dataSource(); + if (dataSource != null) { + properties.put("hibernate.connection.datasource", dataSource); + } + properties.put("hibernate.generate_statistics", Boolean.TRUE.toString()); + + properties.put("net.sf.ehcache.configurationResourceName", Thread.currentThread().getContextClassLoader().getResource("ehcache.xml").toString()); + //properties.put("hibernate.ejb.metamodel.population", "disabled"); + additionalProperties(properties); + return properties; + } + + protected Dialect dialect() { + SessionFactory sessionFactory = sessionFactory(); + return sessionFactory != null ? + sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect() : + ReflectionUtils.newInstance(dataSourceProvider().hibernateDialect()); + } + + protected Map propertiesMap() { + return (Map) properties(); + } + + protected void additionalProperties(Properties properties) { + + } + + protected DataSourceProxyType dataSourceProxyType() { + return DataSourceProxyType.DATA_SOURCE_PROXY; + } + + protected DataSource dataSource() { + if (dataSource == null) { + dataSource = newDataSource(); + } + return dataSource; + } + + protected DataSource newDataSource() { + DataSource dataSource = dataSourceProvider().dataSource(); + if(proxyDataSource()) { + dataSource = dataSourceProxy(dataSource); + } + if (connectionPooling()) { + HikariDataSource poolingDataSource = connectionPoolDataSource(dataSource); + closeables.add(poolingDataSource::close); + return poolingDataSource; + } else { + return dataSource; + } + } + + protected DataSource dataSourceProxy(DataSource dataSource) { + return dataSourceProxyType().dataSource(dataSource); + } + + protected boolean proxyDataSource() { + return true; + } + + protected HikariDataSource connectionPoolDataSource(DataSource dataSource) { + return new HikariDataSource(hikariConfig(dataSource)); + } + + protected HikariConfig hikariConfig(DataSource dataSource) { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setMaximumPoolSize(connectionPoolSize()); + hikariConfig.setDataSource(dataSource); + return hikariConfig; + } + + protected boolean connectionPooling() { + return false; + } + + protected int connectionPoolSize() { + int cpuCores = Runtime.getRuntime().availableProcessors(); + return cpuCores * 4; + } + + protected DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + protected Database database() { + return Database.HSQLDB; + } + + protected List> additionalTypes() { + return null; + } + + protected void additionalMetadata(MetadataBuilder metadataBuilder) { + + } + + protected T doInHibernate(HibernateTransactionFunction callable) { + T result = null; + Session session = null; + Transaction txn = null; + try { + session = sessionFactory().openSession(); + callable.beforeTransactionCompletion(); + txn = session.beginTransaction(); + + result = callable.apply(session); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + callable.afterTransactionCompletion(); + if (session != null) { + session.close(); + } + } + return result; + } + + protected void doInHibernate(HibernateTransactionConsumer callable) { + Session session = null; + Transaction txn = null; + try { + session = sessionFactory().openSession(); + callable.beforeTransactionCompletion(); + txn = session.beginTransaction(); + + callable.accept(session); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + callable.afterTransactionCompletion(); + if (session != null) { + session.close(); + } + } + } + + protected T doInStatelessSession(HibernateStatelessTransactionFunction callable) { + T result = null; + StatelessSession session = null; + Transaction transaction = null; + try { + session = sessionFactory().withStatelessOptions().openStatelessSession(); + transaction = session.beginTransaction(); + result = callable.apply(session); + if (!transaction.getRollbackOnly()) { + transaction.commit(); + } else { + try { + transaction.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (transaction != null && transaction.isActive()) { + try { + transaction.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + if (session != null) { + session.close(); + } + } + return result; + } + + protected void doInStatelessSession(HibernateStatelessTransactionConsumer callable) { + StatelessSession session = null; + Transaction transaction = null; + try { + session = sessionFactory().withStatelessOptions().openStatelessSession(); + transaction = session.beginTransaction(); + callable.accept(session); + if (!transaction.getRollbackOnly()) { + transaction.commit(); + } else { + try { + transaction.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (transaction != null && transaction.isActive()) { + try { + transaction.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + if (session != null) { + session.close(); + } + } + } + + protected T doInJPA(JPATransactionFunction function) { + T result = null; + EntityManager entityManager = null; + EntityTransaction txn = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + function.beforeTransactionCompletion(); + txn = entityManager.getTransaction(); + txn.begin(); + result = function.apply(entityManager); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + function.afterTransactionCompletion(); + if (entityManager != null) { + entityManager.close(); + } + } + return result; + } + + protected void doInJPA(JPATransactionVoidFunction function) { + EntityManager entityManager = null; + EntityTransaction txn = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + function.beforeTransactionCompletion(); + txn = entityManager.getTransaction(); + txn.begin(); + function.accept(entityManager); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + function.afterTransactionCompletion(); + if (entityManager != null) { + entityManager.close(); + } + } + } + + protected T doInJDBC(ConnectionCallable callable) { + AtomicReference result = new AtomicReference<>(); + Session session = null; + Transaction txn = null; + try { + session = sessionFactory().openSession(); + txn = session.beginTransaction(); + session.doWork(connection -> { + result.set(callable.execute(connection)); + }); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + if (session != null) { + session.close(); + } + } + return result.get(); + } + + protected void doInJDBC(ConnectionVoidCallable callable) { + Session session = null; + Transaction txn = null; + try { + session = sessionFactory().openSession(); + session.setDefaultReadOnly(true); + session.setHibernateFlushMode(FlushMode.MANUAL); + txn = session.beginTransaction(); + session.doWork(callable::execute); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + if (session != null) { + session.close(); + } + } + } + + protected void executeSync(VoidCallable callable) { + executeSync(Collections.singleton(callable)); + } + + protected T executeSync(Callable callable) { + try { + return executorService.submit(callable).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + protected void executeSync(Collection callables) { + try { + List> futures = executorService.invokeAll(callables); + for (Future future : futures) { + future.get(); + } + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + protected void executeAsync(Runnable callable, final Runnable completionCallback) { + final Future future = executorService.submit(callable); + new Thread(() -> { + while (!future.isDone()) { + try { + Thread.sleep(100); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + try { + completionCallback.run(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }).start(); + } + + protected Future executeAsync(Runnable callable) { + return executorService.submit(callable); + } + + protected void transact(Consumer callback) { + transact(callback, null); + } + + protected void transact(Consumer callback, Consumer before) { + Connection connection = null; + try { + connection = dataSource().getConnection(); + if (before != null) { + before.accept(connection); + } + connection.setAutoCommit(false); + callback.accept(connection); + connection.commit(); + } catch (Exception e) { + if (connection != null) { + try { + connection.rollback(); + } catch (SQLException ex) { + throw new DataAccessException(e); + } + } + throw (e instanceof DataAccessException ? + (DataAccessException) e : new DataAccessException(e)); + } finally { + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + throw new DataAccessException(e); + } + } + } + } + + protected LockType lockType() { + return LockType.LOCKS; + } + + protected void awaitOnLatch(CountDownLatch latch) { + try { + latch.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + + protected void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + protected V sleep(int millis, Callable callable) { + V result = null; + try { + if (callable != null) { + result = callable.call(); + } + Thread.sleep(millis); + } catch (Exception e) { + throw new IllegalStateException(e); + } + return result; + } + + protected void awaitTermination(long timeout, TimeUnit unit) { + try { + executorService.awaitTermination(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + protected String selectStringColumn(Connection connection, String sql) { + try { + try (Statement statement = connection.createStatement()) { + statement.setQueryTimeout(1); + ResultSet resultSet = statement.executeQuery(sql); + if (!resultSet.next()) { + throw new IllegalArgumentException("There was no row to be selected!"); + } + return resultSet.getString(1); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + protected T selectColumn(Connection connection, String sql, Class clazz) { + return selectColumn(connection, sql, clazz, null); + } + + protected T selectColumn(Connection connection, String sql, Class clazz, Duration timeout) { + try { + try (Statement statement = connection.createStatement()) { + if (timeout != null) { + statement.setQueryTimeout((int) timeout.toSeconds()); + } + ResultSet resultSet = statement.executeQuery(sql); + if (!resultSet.next()) { + throw new IllegalArgumentException("There was no row to be selected!"); + } + return clazz.cast(resultSet.getObject(1)); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + protected List selectColumnList(Connection connection, String sql, Class clazz) { + List result = new ArrayList<>(); + try { + try (Statement statement = connection.createStatement()) { + statement.setQueryTimeout(1); + ResultSet resultSet = statement.executeQuery(sql); + while (resultSet.next()) { + result.add(clazz.cast(resultSet.getObject(1))); + } + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + return result; + } + + protected int update(Connection connection, String sql) { + try { + try (Statement statement = connection.createStatement()) { + statement.setQueryTimeout(1); + return statement.executeUpdate(sql); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + protected void executeStatement(String sql) { + try (Connection connection = dataSource().getConnection(); + Statement statement = connection.createStatement()) { + statement.executeLargeUpdate(sql); + } catch (SQLException e) { + LOGGER.error("Statement failed", e); + } + } + + protected void executeQuery(String sql) { + try (Connection connection = dataSource().getConnection(); + Statement statement = connection.createStatement()) { + statement.executeQuery(sql); + } catch (SQLException e) { + LOGGER.error("Statement failed", e); + } + } + + protected void executeStatement(String... sqls) { + try (Connection connection = dataSource().getConnection(); + Statement statement = connection.createStatement()) { + for(String sql : sqls) { + statement.executeUpdate(sql); + } + } catch (SQLException e) { + LOGGER.error("Statement failed", e); + } + } + + protected void executeStatement(Connection connection, String sql) { + try { + try (Statement statement = connection.createStatement()) { + statement.execute(sql); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + protected void executeStatement(Connection connection, String sql, int timeout) { + try { + try (Statement statement = connection.createStatement()) { + statement.setQueryTimeout(timeout); + statement.execute(sql); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + protected void executeStatement(Connection connection, String... sqls) { + try { + try (Statement statement = connection.createStatement()) { + for (String sql : sqls) { + statement.execute(sql); + } + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + protected void executeStatement(EntityManager entityManager, String... sqls) { + Session session = entityManager.unwrap(Session.class); + for (String sql : sqls) { + try { + session.doWork(connection -> { + executeStatement(connection, sql); + }); + } catch (Exception e) { + LOGGER.error( + String.format("Error executing statement: %s", sql), e + ); + } + } + } + + protected int update(Connection connection, String sql, Object[] params) { + try { + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setQueryTimeout(1); + for (int i = 0; i < params.length; i++) { + statement.setObject(i + 1, params[i]); + } + return statement.executeUpdate(); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + protected int count(Connection connection, String sql) { + try { + try (Statement statement = connection.createStatement()) { + statement.setQueryTimeout(1); + ResultSet resultSet = statement.executeQuery(sql); + if (!resultSet.next()) { + throw new IllegalArgumentException("There was no row to be selected!"); + } + return ((Number) resultSet.getObject(1)).intValue(); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + /** + * Set JDBC Connection or Statement timeout + * + * @param connection JDBC Connection time out + */ + public void setJdbcTimeout(Connection connection) { + setJdbcTimeout(connection, 1000); + } + + /** + * Set JDBC Connection or Statement timeout + * + * @param connection JDBC Connection time out + * @param timoutMillis millis to wait + */ + public void setJdbcTimeout(Connection connection, long timoutMillis) { + try (Statement st = connection.createStatement()) { + DataSourceProvider dataSourceProvider = dataSourceProvider(); + + switch (dataSourceProvider.database()) { + case POSTGRESQL: + st.execute(String.format("SET statement_timeout TO %d", timoutMillis)); + break; + case MYSQL: + st.execute(String.format("SET SESSION innodb_lock_wait_timeout = %d", TimeUnit.MILLISECONDS.toSeconds(timoutMillis))); + connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), (int) timoutMillis); + break; + case SQLSERVER: + st.execute(String.format("SET LOCK_TIMEOUT %d", timoutMillis)); + connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), (int) timoutMillis); + break; + default: + try { + connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), (int) timoutMillis); + } catch (Throwable ignore) { + ignore.fillInStackTrace(); + } + } + } catch (SQLException e) { + fail(e.getMessage()); + } + } + + protected String transactionId(EntityManager entityManager) { + return String.valueOf( + entityManager.createNativeQuery( + dataSourceProvider() + .queries() + .transactionId() + ) + .getSingleResult() + ); + } + + protected void printEntityCacheRegionStatistics(Class entityClass) { + printCacheRegionStatisticsEntries(entityClass.getName()); + } + + protected void printCollectionCacheRegionStatistics(Class entityClass, String collection) { + printCacheRegionStatisticsEntries(entityClass.getName() + "." + collection); + } + + protected void printQueryCacheRegionStatistics() { + printCacheRegionStatisticsEntries("default-query-results-region"); + } + + protected void printNaturalIdCacheRegionStatistics(Class entityClass) { + printCacheRegionStatistics(entityClass.getName() + "##NaturalId"); + } + + protected void printCacheRegionStatistics(String region) { + printCacheRegionStatisticsEntries(region); + } + + private void printCacheRegionStatisticsEntries(String regionName) { + SessionFactory sessionFactory = sessionFactory(); + Statistics statistics = sessionFactory.getStatistics(); + if (sessionFactory.getSessionFactoryOptions().isQueryCacheEnabled()) { + ReflectionUtils.invokeMethod(statistics, "getQueryRegionStats", "default-query-results-region"); + } + + CacheRegionStatistics cacheRegionStatistics = "default-query-results-region".equals(regionName) ? + statistics.getQueryRegionStatistics(regionName) : + statistics.getDomainDataRegionStatistics(regionName); + + if (cacheRegionStatistics != null) { + AbstractRegion region = ReflectionUtils.getFieldValue(cacheRegionStatistics, "region"); + + StorageAccess storageAccess = getStorageAccess(region); + org.ehcache.core.Ehcache cache = getEhcache(storageAccess); + + if (cache != null) { + StringBuilder cacheEntriesBuilder = new StringBuilder(); + cacheEntriesBuilder.append("["); + + boolean firstEntry = true; + + Object onHeapStore = ReflectionUtils.getFieldValue(cache, "store"); + if(onHeapStore instanceof XAStore) { + onHeapStore = ReflectionUtils.getFieldValue(onHeapStore, "underlyingStore"); + } + if(onHeapStore instanceof TieredStore) { + onHeapStore = ReflectionUtils.getFieldValue(onHeapStore, "realCachingTier"); + onHeapStore = ReflectionUtils.getFieldValue(onHeapStore, "higher"); + } + Object onHeapStoreMap = ReflectionUtils.getFieldValueOrNull(onHeapStore, "map"); + if (onHeapStoreMap == null) { + return; + } + Iterable keySet = ReflectionUtils.invokeMethod(onHeapStoreMap, "keySet"); + for (Object key : keySet) { + Object cacheValue = storageAccess.getFromCache(key, null); + + if (!firstEntry) { + cacheEntriesBuilder.append(",\n"); + } else { + cacheEntriesBuilder.append("\n"); + firstEntry = false; + } + cacheEntriesBuilder.append("\t"); + + if (cacheValue instanceof Map) { + Map mapValue = (Map) cacheValue; + + cacheEntriesBuilder.append(mapValue); + } else if (cacheValue instanceof QueryResultsCacheImpl.CacheItem) { + QueryResultsCacheImpl.CacheItem queryValue = (QueryResultsCacheImpl.CacheItem) cacheValue; + + cacheEntriesBuilder.append( + ToStringBuilder.reflectionToString(queryValue, ToStringStyle.SHORT_PREFIX_STYLE) + ); + } else if (cacheValue instanceof StandardCacheEntryImpl) { + StandardCacheEntryImpl standardCacheEntry = (StandardCacheEntryImpl) cacheValue; + + cacheEntriesBuilder.append( + ToStringBuilder.reflectionToString(standardCacheEntry, ToStringStyle.SHORT_PREFIX_STYLE) + ); + } else if (cacheValue instanceof CollectionCacheEntry) { + CollectionCacheEntry collectionCacheEntry = (CollectionCacheEntry) cacheValue; + + cacheEntriesBuilder.append( + ToStringBuilder.reflectionToString(collectionCacheEntry, ToStringStyle.SHORT_PREFIX_STYLE) + ); + } else if (cacheValue instanceof AbstractReadWriteAccess.Item) { + AbstractReadWriteAccess.Item valueItem = (AbstractReadWriteAccess.Item) cacheValue; + Object value = valueItem.getValue(); + + if (value instanceof StandardCacheEntryImpl) { + StandardCacheEntryImpl standardCacheEntry = ((StandardCacheEntryImpl) value); + cacheEntriesBuilder.append( + ToStringBuilder.reflectionToString(standardCacheEntry, ToStringStyle.SHORT_PREFIX_STYLE) + ); + } else if (value.getClass().getPackageName().startsWith("java")) { + cacheEntriesBuilder.append(value); + } else { + cacheEntriesBuilder.append( + ToStringBuilder.reflectionToString(valueItem.getValue(), ToStringStyle.SHORT_PREFIX_STYLE) + ); + } + } else if (cacheValue instanceof AbstractReadWriteAccess.Lockable) { + cacheEntriesBuilder.append( + ToStringBuilder.reflectionToString(cacheValue, ToStringStyle.SHORT_PREFIX_STYLE) + ); + } + } + + cacheEntriesBuilder.append("\n]"); + + LOGGER.debug( + "\nRegion: {},\nStatistics: {},\nEntries: {}", + regionName, + cacheRegionStatistics, + cacheEntriesBuilder + ); + } + } + } + + private org.ehcache.core.Ehcache getEhcache(StorageAccess storageAccess) { + Object cacheHolder = storageAccess; + if (storageAccess instanceof JCacheAccessImpl) { + cacheHolder = ReflectionUtils.getFieldValue(storageAccess, "underlyingCache"); + } + return ReflectionUtils.getFieldValue(cacheHolder, "ehCache"); + } + + + private StorageAccess getStorageAccess(AbstractRegion region) { + if (region instanceof DirectAccessRegionTemplate) { + DirectAccessRegionTemplate directAccessRegionTemplate = (DirectAccessRegionTemplate) region; + return directAccessRegionTemplate.getStorageAccess(); + } else if (region instanceof DomainDataRegionTemplate) { + DomainDataRegionTemplate domainDataRegionTemplate = (DomainDataRegionTemplate) region; + return domainDataRegionTemplate.getCacheStorageAccess(); + } + throw new IllegalArgumentException("Unsupported region: " + region); + } + + public static String stringValue(Object value) { + return value.toString(); + } + + public static int intValue(Object number) { + return ((Number) number).intValue(); + } + + public static long longValue(Object number) { + if (number instanceof String) { + return Long.parseLong((String) number); + } + return ((Number) number).longValue(); + } + + public static double doubleValue(Object number) { + return ((Number) number).doubleValue(); + } + + public static URL urlValue(String url) { + try { + return url != null ? new URL(url) : null; + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + + public static LocalDateTime localDateTimeValue(Object value) { + return (LocalDateTime) value; + } + + protected List> parseResultSet(ResultSet resultSet) { + List> rows = new ArrayList<>(); + + try { + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + while (resultSet.next()) { + Map row = new LinkedHashMap<>(); + for (int i = 1; i <= columnCount; i++) { + row.put(metaData.getColumnName(i), resultSet.getString(i)); + } + rows.add(row); + } + } catch (SQLException e) { + throw new IllegalArgumentException(e); + } + + return rows; + } + + protected String resultSetToString(ResultSet resultSet) { + Table.Builder tableBuilder = new Table.Builder(); + + try { + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + String[] columnNames = new String[columnCount]; + + for (int i = 0; i < columnCount; i++) { + columnNames[i] = metaData.getColumnName(i + 1); + } + + tableBuilder.addRow(columnNames); + + while (resultSet.next()) { + String[] columnValues = new String[columnCount]; + for (int i = 0; i < columnCount; i++) { + columnValues[i] = resultSet.getString(i + 1); + } + tableBuilder.addRow(columnValues); + } + } catch (SQLException e) { + throw new IllegalArgumentException(e); + } + + return tableBuilder.build().serialize(); + } + + protected boolean isHypersistenceOptimizer() { + return ReflectionUtils.getClassOrNull("io.hypersistence.optimizer.HypersistenceOptimizer") != null; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/CollectionUtils.java b/core/src/test/java/com/vladmihalcea/hpjp/util/CollectionUtils.java new file mode 100644 index 000000000..5f43352ef --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/CollectionUtils.java @@ -0,0 +1,42 @@ +package com.vladmihalcea.hpjp.util; + +import java.util.List; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * CollectionUtils - Collection utilities holder. + * + * @author Vlad Mihalcea + */ +public final class CollectionUtils { + + /** + * Prevent any instantiation. + */ + private CollectionUtils() { + throw new UnsupportedOperationException("The " + getClass() + " is not instantiable!"); + } + + /** + * Split an element collection into batches. + * + * @param elements elements to split in batches + * @param class type + * @return the Stream of batches + */ + public static Stream> spitInBatches(List elements, int batchSize) { + int elementCount = elements.size(); + if (elementCount <= 0) { + return Stream.empty(); + } + int batchCount = (elementCount - 1) / batchSize; + return IntStream.range(0, batchCount + 1) + .mapToObj( + batchNumber -> elements.subList( + batchNumber * batchSize, + batchNumber == batchCount ? elementCount : (batchNumber + 1) * batchSize + ) + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/CryptoUtils.java b/core/src/test/java/com/vladmihalcea/hpjp/util/CryptoUtils.java new file mode 100644 index 000000000..da8cbbcee --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/CryptoUtils.java @@ -0,0 +1,85 @@ +package com.vladmihalcea.hpjp.util; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.security.*; +import java.util.Arrays; +import java.util.Base64; + +/** + * @author Vlad Mihalcea + */ +public final class CryptoUtils { + + private static String ENCODING = "UTF-8"; + + private static byte[] ENCRYPT_KEY_BYTES = new byte[] { + 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -33, 58, -60, -67, 82, -114, 43, -3, -15, 74, -69, -14, 123, -51, 29, -33, -106, -94, -115, -57, 52, -124, 106, -56, 55, 0, -98, -56, -124, -106, -122, 101, 117, -49, 96, 126, 4, -3, -91, -31, -100, -42, 10, 103, -93, -128, -82, 34, 63, 33, 48, -69, 45, 121, 33, 99, -50, 18, -119, -102, -24, 122, -103, 107, 124, -16, 34, 83, -30, -51, -54, -38, -50, -82, 86, 101, -9, -72, 28, 42, 66, 14, 76, -107, -91, -53, -30, 21, 80, 109, -1, -41, -61, -69, 39, 87, -17, 35, 48, 51, -58, -91, -109, 29, -18, -54, 104, -30, -114, -120, -10, 11, -47, 35, -112, -121, 54, 20, -47, 127, 39, 76, -86, 1, -71, 64, -56, -49, -113, 65, 120, -67, 59, 126, 25, -71, -24, -63, -33, 36, -44, 110, -14, 46, -120, 73, 55, -86, -110, 98, -71, -124, -67, 17, -37, -122, 68, -36, 116, -65, -32, 8, 104, -17, -65, 96, 85, -16, -7, 24, 19, -91, 38, 111, 91, -17, 39, -9, 89, -95, -54, -38, -20, 113, 82, 64, -24, -114, 8, 72, -96, -79, 116, -12, 63, 61, 59, 119, 28, -98, 86, -55, -99, 12, 123, 17, -29, -35, 3, -118, -120, -87, 4, 123, -46, -28, 15, -54, -26, -81, -47, 40, 28, 109, 98, 78, 16, 113, -11, -59, 82, 34, 41, 69, 54, 16, -24, 89, -95, -40, 58, 32, 72, 124, 13, 21, -34, 24, -33, -66, 89, 74, 21, 38, 118, 27, 2, 3, 1, 0, 1 + }; + + public static SecretKeySpec getEncryptionKey() { + MessageDigest sha; + try { + sha = MessageDigest.getInstance("SHA-256"); + byte[] key = sha.digest(ENCRYPT_KEY_BYTES); + key = Arrays.copyOf(key, 16); + return new SecretKeySpec(key, "AES"); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + public static String encrypt(Object message) { + if(message == null) { + throw new IllegalArgumentException("Only not-null values can be encrypted!"); + } + try { + Cipher cipher = getCipher(); + cipher.init(Cipher.ENCRYPT_MODE, getEncryptionKey()); + String messageValue = (message instanceof String) ? + (String) message : + String.valueOf(message); + return Base64.getEncoder().encodeToString( + cipher.doFinal(messageValue.getBytes(ENCODING)) + ); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static String decrypt(String message) { + try { + Cipher cipher = getCipher(); + cipher.init(Cipher.DECRYPT_MODE, getEncryptionKey()); + return new String(cipher.doFinal(Base64.getDecoder().decode(message))); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static T decrypt(String message, Class clazz) { + try { + Cipher cipher = getCipher(); + cipher.init(Cipher.DECRYPT_MODE, getEncryptionKey()); + String decryptedValue = new String(cipher.doFinal(Base64.getDecoder().decode(message))); + if (String.class.equals(clazz)) { + return (T) decryptedValue; + } else { + return ReflectionUtils.invokeStaticMethod( + ReflectionUtils.getMethodOrNull(clazz, "valueOf", String.class), + decryptedValue + ); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static Cipher getCipher() { + try { + return Cipher.getInstance("AES/ECB/PKCS5PADDING"); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/CryptoUtilsTest.java b/core/src/test/java/com/vladmihalcea/hpjp/util/CryptoUtilsTest.java new file mode 100644 index 000000000..ea4c233fc --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/CryptoUtilsTest.java @@ -0,0 +1,120 @@ +package com.vladmihalcea.hpjp.util; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.vladmihalcea.hpjp.spring.transaction.readonly.config.stats.SpringTransactionStatisticsReport; +import org.hibernate.internal.util.collections.BoundedConcurrentHashMap; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.stream.LongStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public final class CryptoUtilsTest { + + public static Logger LOGGER = LoggerFactory.getLogger(SpringTransactionStatisticsReport.class); + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .convertDurationsTo(TimeUnit.MICROSECONDS) + .build(); + + private final Timer encryptTimer = metricRegistry.timer("encryptTimer"); + private final Timer decryptTimer = metricRegistry.timer("decryptTimer"); + + private final ThreadLocalRandom random = ThreadLocalRandom.current(); + private int MAX_COUNT = 100_000; + + private final BoundedConcurrentHashMap encryptCache = new BoundedConcurrentHashMap<>( + 100_000, + 20, + BoundedConcurrentHashMap.Eviction.LIRS + ); + + private final BoundedConcurrentHashMap decryptCache = new BoundedConcurrentHashMap<>( + 100_000, + 20, + BoundedConcurrentHashMap.Eviction.LIRS + ); + + private int encryptCallCount = 0; + private int decryptCallCount = 0; + + @Test + public void testPerformance() { + if(!AbstractTest.ENABLE_LONG_RUNNING_TESTS) { + return; + } + warmUp(); + + LongStream.rangeClosed(1, MAX_COUNT).forEach(i -> { + Long value = random.nextLong(i); + long startNanos = System.nanoTime(); + String encryptedValue = CryptoUtils.encrypt(value); + encryptTimer.update((System.nanoTime() - startNanos), TimeUnit.NANOSECONDS); + + startNanos = System.nanoTime(); + Long decryptedValue = CryptoUtils.decrypt(encryptedValue, Long.class); + decryptTimer.update((System.nanoTime() - startNanos), TimeUnit.NANOSECONDS); + assertEquals(value.longValue(), decryptedValue.longValue()); + }); + + logReporter.report(); + } + + @Test + public void testPerformanceUsingCache() { + if(!AbstractTest.ENABLE_LONG_RUNNING_TESTS) { + return; + } + warmUp(); + + LongStream.rangeClosed(1, MAX_COUNT).forEach(i -> { + //Hit ratio of 90.7% + long threshold = i > 10 ? i / 10 : i; + Long value = random.nextLong(threshold); + long startNanos = System.nanoTime(); + String encryptedValue = encryptCache.get(value); + if(encryptedValue == null) { + encryptCallCount++; + encryptedValue = CryptoUtils.encrypt(value); + encryptCache.put(value, encryptedValue); + } + encryptTimer.update((System.nanoTime() - startNanos), TimeUnit.NANOSECONDS); + + startNanos = System.nanoTime(); + Long decryptedValue = decryptCache.get(encryptedValue); + if(decryptedValue == null) { + decryptCallCount++; + decryptedValue = CryptoUtils.decrypt(encryptedValue, Long.class); + decryptCache.put(encryptedValue, decryptedValue); + } + decryptTimer.update((System.nanoTime() - startNanos), TimeUnit.NANOSECONDS); + assertEquals(value.longValue(), decryptedValue.longValue()); + }); + + logReporter.report(); + LOGGER.info("Encrypt was called {} times", encryptCallCount); + LOGGER.info("Decrypt was called {} times", decryptCallCount); + } + + private void warmUp() { + LongStream.rangeClosed(1, MAX_COUNT/10).forEach(i -> { + Long value = random.nextLong(); + String encryptedValue = CryptoUtils.encrypt(value); + Long decryptedValue = CryptoUtils.decrypt(encryptedValue, Long.class); + assertEquals(value.longValue(), decryptedValue.longValue()); + }); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/DataSourceProviderIntegrationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/util/DataSourceProviderIntegrationTest.java new file mode 100644 index 000000000..31a47ed75 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/DataSourceProviderIntegrationTest.java @@ -0,0 +1,40 @@ +package com.vladmihalcea.hpjp.util; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import org.assertj.core.util.Arrays; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public abstract class DataSourceProviderIntegrationTest extends AbstractTest { + + private final DataSourceProvider dataSourceProvider; + + public DataSourceProviderIntegrationTest(DataSourceProvider dataSourceProvider) { + this.dataSourceProvider = dataSourceProvider; + } + + @Parameterized.Parameters + public static Collection databases() { + List databases = new ArrayList<>(); + return databases; + } + + @Override + protected Database database() { + return dataSourceProvider.database(); + } + + @Override + protected DataSourceProvider dataSourceProvider() { + return dataSourceProvider; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/DataSourceProxyType.java b/core/src/test/java/com/vladmihalcea/hpjp/util/DataSourceProxyType.java new file mode 100644 index 000000000..edac28642 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/DataSourceProxyType.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.util; + +import com.p6spy.engine.spy.P6DataSource; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.listener.DataSourceQueryCountListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; + +import javax.sql.DataSource; + +/** + * @author Vlad Mihalcea + */ +public enum DataSourceProxyType { + DATA_SOURCE_PROXY { + @Override + public DataSource dataSource(DataSource dataSource) { + ChainListener listener = new ChainListener(); + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + listener.addListener(loggingListener); + listener.addListener(new DataSourceQueryCountListener()); + return ProxyDataSourceBuilder + .create(dataSource) + .name(name()) + .listener(listener) + .build(); + } + }, + P6SPY { + @Override + public DataSource dataSource(DataSource dataSource) { + return new P6DataSource(dataSource); + } + }; + + public abstract DataSource dataSource(DataSource dataSource); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/DatabaseProviderIntegrationTest.java b/core/src/test/java/com/vladmihalcea/hpjp/util/DatabaseProviderIntegrationTest.java new file mode 100644 index 000000000..4984eb106 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/DatabaseProviderIntegrationTest.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.util; + +import com.vladmihalcea.hpjp.util.providers.Database; +import org.assertj.core.util.Arrays; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +@RunWith(Parameterized.class) +public abstract class DatabaseProviderIntegrationTest extends AbstractTest { + + private final Database database; + + public DatabaseProviderIntegrationTest(Database database) { + this.database = database; + } + + @Parameterized.Parameters + public static Collection databases() { + List databases = new ArrayList<>(); + databases.add(Arrays.array(Database.ORACLE)); + databases.add(Arrays.array(Database.SQLSERVER)); + databases.add(Arrays.array(Database.POSTGRESQL)); + databases.add(Arrays.array(Database.MYSQL)); + //databases.add(Arrays.array(Database.YUGABYTEDB)); + return databases; + } + + @Override + protected Database database() { + return database; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/EntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/EntityProvider.java similarity index 83% rename from core/src/test/java/com/vladmihalcea/book/hpjp/util/EntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/util/EntityProvider.java index 275d9487d..a0bf41657 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/EntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/EntityProvider.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.util; +package com.vladmihalcea.hpjp.util; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/PersistenceUnitInfoImpl.java b/core/src/test/java/com/vladmihalcea/hpjp/util/PersistenceUnitInfoImpl.java new file mode 100644 index 000000000..d2a389d1b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/PersistenceUnitInfoImpl.java @@ -0,0 +1,144 @@ +package com.vladmihalcea.hpjp.util; + +import org.hibernate.jpa.HibernatePersistenceProvider; + +import jakarta.persistence.SharedCacheMode; +import jakarta.persistence.ValidationMode; +import jakarta.persistence.spi.ClassTransformer; +import jakarta.persistence.spi.PersistenceUnitInfo; +import jakarta.persistence.spi.PersistenceUnitTransactionType; +import javax.sql.DataSource; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class PersistenceUnitInfoImpl implements PersistenceUnitInfo { + + public static final String JPA_VERSION = "3.1"; + + private final String persistenceUnitName; + + private PersistenceUnitTransactionType transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL; + + private final List managedClassNames; + + private final List mappingFileNames = new ArrayList<>(); + + private final Properties properties; + + private DataSource jtaDataSource; + + private DataSource nonJtaDataSource; + + public PersistenceUnitInfoImpl( + String persistenceUnitName, + List managedClassNames, + Properties properties) { + this.persistenceUnitName = persistenceUnitName; + this.managedClassNames = managedClassNames; + this.properties = properties; + } + + @Override + public String getPersistenceUnitName() { + return persistenceUnitName; + } + + @Override + public String getPersistenceProviderClassName() { + return HibernatePersistenceProvider.class.getName(); + } + + @Override + public PersistenceUnitTransactionType getTransactionType() { + return transactionType; + } + + @Override + public DataSource getJtaDataSource() { + return jtaDataSource; + } + + public PersistenceUnitInfoImpl setJtaDataSource(DataSource jtaDataSource) { + this.jtaDataSource = jtaDataSource; + this.nonJtaDataSource = null; + transactionType = PersistenceUnitTransactionType.JTA; + return this; + } + + @Override + public DataSource getNonJtaDataSource() { + return nonJtaDataSource; + } + + public PersistenceUnitInfoImpl setNonJtaDataSource(DataSource nonJtaDataSource) { + this.nonJtaDataSource = nonJtaDataSource; + this.jtaDataSource = null; + transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL; + return this; + } + + @Override + public List getMappingFileNames() { + return mappingFileNames; + } + + @Override + public List getJarFileUrls() { + return Collections.emptyList(); + } + + @Override + public URL getPersistenceUnitRootUrl() { + return null; + } + + @Override + public List getManagedClassNames() { + return managedClassNames; + } + + @Override + public boolean excludeUnlistedClasses() { + return false; + } + + @Override + public SharedCacheMode getSharedCacheMode() { + return SharedCacheMode.UNSPECIFIED; + } + + @Override + public ValidationMode getValidationMode() { + return ValidationMode.AUTO; + } + + public Properties getProperties() { + return properties; + } + + @Override + public String getPersistenceXMLSchemaVersion() { + return JPA_VERSION; + } + + @Override + public ClassLoader getClassLoader() { + return Thread.currentThread().getContextClassLoader(); + } + + @Override + public void addTransformer(ClassTransformer transformer) { + + } + + @Override + public ClassLoader getNewTempClassLoader() { + return null; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/RandomUtils.java b/core/src/test/java/com/vladmihalcea/hpjp/util/RandomUtils.java new file mode 100644 index 000000000..721011341 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/RandomUtils.java @@ -0,0 +1,43 @@ +package com.vladmihalcea.hpjp.util; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author Vlad Mihalcea + */ +public class RandomUtils { + + public static final ThreadLocalRandom GENERATOR = ThreadLocalRandom.current(); + + private static final String[] STARTING = new String[] { + "Lorem", "Aenean", "Cum", "Donec", "Nulla", "Nullam", "Vivamus", "Aliquam", "Phasellus", "Quisque", "Aenean", "Etiam", "Curabitur", "Nam", "Maecenas", + }; + + private static final String[] SIMPLE_1 = new String[] { + "ipsum", "dolor", "sit", "consectetuer", "adipiscing", "commodo", "ligula", "eget", "sociis", "natoque", "penatibus", "et", "magnis", "dis", "parturient", "nascetur", "ridiculus", "quam", "ultricies", "pellentesque", "pretium", "consequat", "massa", "quis", "pede", "fringilla", "aliquet", "vulputate", "enim", "rhoncus", "fringilla", "mauris", "sit", "amet", "sodales", "sagittis", "leo", "eget", "bibendum", "augue", "velit", "cursus", "nunc" + }; + + private static final String[] SIMPLE_2 = new String[] { + "amet", "felis", "nec", "eu", "quis", "justo", "vel", "nec", "eget", "a", "venenatis", "vitae", "ligula", "eu", "vitae", "ac", "ante", "in", "quis", "a", "tempus", "libero", "vel", "pulvinar", "id", "consequat", "sodales" + }; + + private static final String[] ENDINGS = new String[] { + "elit.", "dolor.", "massa.", "mus.", "sem.", "enim.", "arcu.", "justo.", "pretium.", "tincidunt.", "dapibus.", "nisi.", "tellus.", "enim.", "tellus.", "laoreet.", "rutrum.", "augue.","nisi.", "dui.", "rhoncus.", "ipsum.", "lorem.", "tempus.", "ante.", "tincidunt.", "leo.", "magna." + }; + + public static String randomTitle() { + return String.format( + "%s %s %s %s %s %s %s %s %s %s", + STARTING[GENERATOR.nextInt(STARTING.length)], + SIMPLE_1[GENERATOR.nextInt(SIMPLE_1.length)], + SIMPLE_1[GENERATOR.nextInt(SIMPLE_1.length)], + SIMPLE_2[GENERATOR.nextInt(SIMPLE_2.length)], + SIMPLE_1[GENERATOR.nextInt(SIMPLE_1.length)], + SIMPLE_2[GENERATOR.nextInt(SIMPLE_2.length)], + SIMPLE_1[GENERATOR.nextInt(SIMPLE_1.length)], + SIMPLE_1[GENERATOR.nextInt(SIMPLE_1.length)], + SIMPLE_2[GENERATOR.nextInt(SIMPLE_2.length)], + ENDINGS[GENERATOR.nextInt(ENDINGS.length)] + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/ReflectionUtils.java b/core/src/test/java/com/vladmihalcea/hpjp/util/ReflectionUtils.java new file mode 100644 index 000000000..7ca2c3cb2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/ReflectionUtils.java @@ -0,0 +1,758 @@ +package com.vladmihalcea.hpjp.util; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +/** + * ReflectionUtils - Reflection utilities holder. + * + * @author Vlad Mihalcea + */ +public final class ReflectionUtils { + + private static final String GETTER_PREFIX = "get"; + + private static final String SETTER_PREFIX = "set"; + + /** + * Prevent any instantiation. + */ + private ReflectionUtils() { + throw new UnsupportedOperationException("The " + getClass() + " is not instantiable!"); + } + + /** + * Instantiate a new {@link Object} of the provided type. + * + * @param className The fully-qualified Java class name of the {@link Object} to instantiate + * @param class type + * @return new Java {@link Object} of the provided type + */ + public static T newInstance(String className) { + Class clazz = getClass(className); + return newInstance(clazz); + } + + /** + * Instantiate a new {@link Object} of the provided type. + * + * @param className The fully-qualified Java class name of the {@link Object} to instantiate + * @param args The arguments that need to be passed to the constructor + * @param argsTypes The argument types that need to be passed to the constructor + * @param class type + * @return new Java {@link Object} of the provided type + */ + public static T newInstance(String className, Object[] args, Class[] argsTypes) { + Class clazz = getClass(className); + return newInstance(clazz, args, argsTypes); + } + + /** + * Instantiate a new {@link Object} of the provided type. + * + * @param clazz The Java {@link Class} of the {@link Object} to instantiate + * @param class type + * @return new Java {@link Object} of the provided type + */ + @SuppressWarnings("unchecked") + public static T newInstance(Class clazz) { + try { + return (T) clazz.newInstance(); + } catch (InstantiationException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Instantiate a new {@link Object} of the provided type. + * + * @param clazz The Java {@link Class} of the {@link Object} to instantiate + * @param args The arguments that need to be passed to the constructor + * @param argsTypes The argument types that need to be passed to the constructor + * @param class type + * @return new Java {@link Object} of the provided type + */ + @SuppressWarnings("unchecked") + public static T newInstance(Class clazz, Object[] args, Class[] argsTypes) { + try { + Constructor constructor = clazz.getDeclaredConstructor(argsTypes); + constructor.setAccessible(true); + return constructor.newInstance(args); + } catch (InstantiationException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (NoSuchMethodException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Get the {@link Field} with the given name belonging to the provided Java {@link Class}. + * + * @param targetClass the provided Java {@link Class} the field belongs to + * @param fieldName the {@link Field} name + * @return the {@link Field} matching the given name + */ + public static Field getField(Class targetClass, String fieldName) { + Field field = null; + + try { + field = targetClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + try { + field = targetClass.getField(fieldName); + } catch (NoSuchFieldException ignore) { + } + + if (!targetClass.getSuperclass().equals(Object.class)) { + return getField(targetClass.getSuperclass(), fieldName); + } else { + throw handleException(e); + } + } finally { + if (field != null) { + field.setAccessible(true); + } + } + + return field; + } + + /** + * Get the {@link Field} with the given name belonging to the provided Java {@link Class} or {@code null} + * if no {@link Field} was found. + * + * @param targetClass the provided Java {@link Class} the field belongs to + * @param fieldName the {@link Field} name + * @return the {@link Field} matching the given name or {@code null} + */ + public static Field getFieldOrNull(Class targetClass, String fieldName) { + try { + return getField(targetClass, fieldName); + } catch (IllegalArgumentException e) { + return null; + } + } + + /** + * Get the value of the field matching the given name and belonging to target {@link Object}. + * + * @param target target {@link Object} whose field we are retrieving the value from + * @param fieldName field name + * @param field type + * @return field value + */ + public static T getFieldValue(Object target, String fieldName) { + try { + Field field = getField(target.getClass(), fieldName); + @SuppressWarnings("unchecked") + T returnValue = (T) field.get(target); + return returnValue; + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Get the value of the field matching the given name and belonging to target {@link Object} or {@code null} + * if no {@link Field} was found.. + * + * @param target target {@link Object} whose field we are retrieving the value from + * @param fieldName field name + * @param field type + * @return field value matching the given name or {@code null} + */ + public static T getFieldValueOrNull(Object target, String fieldName) { + try { + Field field = getField(target.getClass(), fieldName); + @SuppressWarnings("unchecked") + T returnValue = (T) field.get(target); + return returnValue; + } catch (Exception e) { + return null; + } + } + + /** + * Set the value of the field matching the given name and belonging to target {@link Object}. + * + * @param target target object + * @param fieldName field name + * @param value field value + */ + public static void setFieldValue(Object target, String fieldName, Object value) { + try { + Field field = getField(target.getClass(), fieldName); + field.set(target, value); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Object}. + * + * @param target target {@link Object} + * @param methodName method name + * @param parameterTypes method parameter types + * @return return {@link Method} matching the provided signature + */ + public static Method getMethod(Object target, String methodName, Class... parameterTypes) { + return getMethod(target.getClass(), methodName, parameterTypes); + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Object} or {@code null} if no {@link Method} was found. + * + * @param target target {@link Object} + * @param methodName method name + * @param parameterTypes method parameter types + * @return return {@link Method} matching the provided signature or {@code null} + */ + public static Method getMethodOrNull(Object target, String methodName, Class... parameterTypes) { + try { + return getMethod(target.getClass(), methodName, parameterTypes); + } catch (RuntimeException e) { + return null; + } + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Class}. + * + * @param targetClass target {@link Class} + * @param methodName method name + * @param parameterTypes method parameter types + * @return the {@link Method} matching the provided signature + */ + @SuppressWarnings("unchecked") + public static Method getMethod(Class targetClass, String methodName, Class... parameterTypes) { + try { + return targetClass.getDeclaredMethod(methodName, parameterTypes); + } catch (NoSuchMethodException e) { + try { + return targetClass.getMethod(methodName, parameterTypes); + } catch (NoSuchMethodException ignore) { + } + + if (!targetClass.getSuperclass().equals(Object.class)) { + return getMethod(targetClass.getSuperclass(), methodName, parameterTypes); + } else { + throw handleException(e); + } + } + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Object} or {@code null} if no {@link Method} was found. + * + * @param targetClass target {@link Class} + * @param methodName method name + * @param parameterTypes method parameter types + * @return return {@link Method} matching the provided signature or {@code null} + */ + public static Method getMethodOrNull(Class targetClass, String methodName, Class... parameterTypes) { + try { + return getMethod(targetClass, methodName, parameterTypes); + } catch (RuntimeException e) { + return null; + } + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Class}, excluding inherited ones, or {@code null} if no {@link Method} was found. + * + * @param targetClass target {@link Class} + * @param methodName method name + * @param parameterTypes method parameter types + * @return return {@link Method} matching the provided signature or {@code null} + */ + public static Method getDeclaredMethodOrNull(Class targetClass, String methodName, Class... parameterTypes) { + try { + return targetClass.getDeclaredMethod(methodName, parameterTypes); + } catch (NoSuchMethodException e) { + return null; + } + } + + /** + * Check if the provided Java {@link Class} contains a method matching + * the given signature (name and parameter types). + * + * @param targetClass target {@link Class} + * @param methodName method name + * @param parameterTypes method parameter types + * @return the provided Java {@link Class} contains a method with the given signature + */ + public static boolean hasMethod(Class targetClass, String methodName, Class... parameterTypes) { + try { + targetClass.getMethod(methodName, parameterTypes); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + /** + * Get the property setter {@link Method} with the given signature (name and parameter types) + * belonging to the provided Java {@link Object}. + * + * @param target target {@link Object} + * @param propertyName property name + * @param parameterType setter property type + * @return the setter {@link Method} matching the provided signature + */ + public static Method getSetter(Object target, String propertyName, Class parameterType) { + String setterMethodName = SETTER_PREFIX + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + Method setter = getMethod(target, setterMethodName, parameterType); + setter.setAccessible(true); + return setter; + } + + /** + * Get the property setter {@link Method} with the given signature (name and parameter types) + * belonging to the provided Java {@link Object} or {@code null} if no setter + * was found matching the provided name. + * + * @param target target {@link Object} + * @param propertyName property name + * @param parameterType setter property type + * @return the setter {@link Method} matching the provided signature or {@code null} + */ + public static Method getSetterOrNull(Object target, String propertyName, Class parameterType) { + try { + return getSetter(target, propertyName, parameterType); + } catch (Exception e) { + return null; + } + } + + /** + * Get the property getter {@link Method} with the given name belonging to + * the provided Java {@link Object}. + * + * @param target target {@link Object} + * @param propertyName property name + * @return the getter {@link Method} matching the provided name + */ + public static Method getGetter(Object target, String propertyName) { + String getterMethodName = GETTER_PREFIX + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + Method getter = getMethod(target, getterMethodName); + getter.setAccessible(true); + return getter; + } + + /** + * Invoke the provided {@link Method} on the given Java {@link Object}. + * + * @param target target {@link Object} whose method we are invoking + * @param method method to invoke + * @param parameters parameters passed to the method call + * @param return value object type + * @return the value return by the {@link Method} invocation + */ + public static T invokeMethod(Object target, Method method, Object... parameters) { + try { + method.setAccessible(true); + @SuppressWarnings("unchecked") + T returnValue = (T) method.invoke(target, parameters); + return returnValue; + } catch (InvocationTargetException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Invoke the method with the provided signature (name and parameter types) + * on the given Java {@link Object}. + * + * @param target target {@link Object} whose method we are invoking + * @param methodName method name to invoke + * @param parameters parameters passed to the method call + * @param return value object type + * @return the value return by the method invocation + */ + public static T invokeMethod(Object target, String methodName, Object... parameters) { + try { + Class[] parameterClasses = new Class[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + parameterClasses[i] = parameters[i].getClass(); + } + + Method method = getMethod(target, methodName, parameterClasses); + method.setAccessible(true); + @SuppressWarnings("unchecked") + T returnValue = (T) method.invoke(target, parameters); + return returnValue; + } catch (InvocationTargetException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Invoke the property getter with the provided name on the given Java {@link Object}. + * + * @param target target {@link Object} whose property getter we are invoking + * @param propertyName property name whose getter we are invoking + * @param return value object type + * @return the value return by the getter invocation + */ + public static T invokeGetter(Object target, String propertyName) { + Method setter = getGetter(target, propertyName); + try { + return (T) setter.invoke(target); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Invoke the property setter with the provided signature (name and parameter types) + * on the given Java {@link Object}. + * + * @param target target {@link Object} whose property setter we are invoking + * @param propertyName property name whose setter we are invoking + * @param parameter parameter passed to the setter call + */ + public static void invokeSetter(Object target, String propertyName, Object parameter) { + Method setter = getSetter(target, propertyName, parameter.getClass()); + try { + setter.invoke(target, parameter); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Invoke the {@link boolean} property setter with the provided name + * on the given Java {@link Object}. + * + * @param target target {@link Object} whose property setter we are invoking + * @param propertyName property name whose setter we are invoking + * @param parameter {@link boolean} parameter passed to the setter call + */ + public static void invokeSetter(Object target, String propertyName, boolean parameter) { + Method setter = getSetter(target, propertyName, boolean.class); + try { + setter.invoke(target, parameter); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Invoke the {@link int} property setter with the provided name + * on the given Java {@link Object}. + * + * @param target target {@link Object} whose property setter we are invoking + * @param propertyName property name whose setter we are invoking + * @param parameter {@link int} parameter passed to the setter call + */ + public static void invokeSetter(Object target, String propertyName, int parameter) { + Method setter = getSetter(target, propertyName, int.class); + try { + setter.invoke(target, parameter); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Invoke the {@code static} {@link Method} with the provided parameters. + * + * @param method target {@code static} {@link Method} to invoke + * @param parameters parameters passed to the method call + * @param return value object type + * @return the value return by the method invocation + */ + public static T invokeStaticMethod(Method method, Object... parameters) { + try { + method.setAccessible(true); + @SuppressWarnings("unchecked") + T returnValue = (T) method.invoke(null, parameters); + return returnValue; + } catch (InvocationTargetException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Get the Java {@link Class} with the given fully-qualified name. + * + * @param className the Java {@link Class} name to be retrieved + * @param {@link Class} type + * @return the Java {@link Class} object + */ + @SuppressWarnings("unchecked") + public static Class getClass(String className) { + try { + return (Class) Class.forName(className, false, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw handleException(e); + } + } + + /** + * Get the {@link URI} resource with the given fully-qualified name. + * + * @param name the {@link URI} resource to be retrieved + * @return the Java {@link Class} object + */ + public static URL getResource(String name) { + return Thread.currentThread().getContextClassLoader().getResource(name); + } + + /** + * Get the Java {@link Class} with the given fully-qualified name or or {@code null} + * if no {@link Class} was found matching the provided name. + * + * @param className the Java {@link Class} name to be retrieved + * @param {@link Class} type + * @return the Java {@link Class} object or {@code null} + */ + @SuppressWarnings("unchecked") + public static Class getClassOrNull(String className) { + try { + return (Class) getClass(className); + } catch (Exception e) { + return null; + } + } + + /** + * Get the Java Wrapper {@link Class} associated to the given primitive type. + * + * @param clazz primitive class + * @return the Java Wrapper {@link Class} + */ + public static Class getWrapperClass(Class clazz) { + if (!clazz.isPrimitive()) + return clazz; + + if (clazz == Integer.TYPE) + return Integer.class; + if (clazz == Long.TYPE) + return Long.class; + if (clazz == Boolean.TYPE) + return Boolean.class; + if (clazz == Byte.TYPE) + return Byte.class; + if (clazz == Character.TYPE) + return Character.class; + if (clazz == Float.TYPE) + return Float.class; + if (clazz == Double.TYPE) + return Double.class; + if (clazz == Short.TYPE) + return Short.class; + if (clazz == Void.TYPE) + return Void.class; + + return clazz; + } + + /** + * Get the first super class matching the provided package name. + * + * @param clazz Java class + * @param packageName package name + * @param class generic type + * @return the first super class matching the provided package name or {@code null}. + */ + public static Class getFirstSuperClassFromPackage(Class clazz, String packageName) { + if (clazz.getPackage().getName().equals(packageName)) { + return clazz; + } else { + Class superClass = clazz.getSuperclass(); + return (superClass == null || superClass.equals(Object.class)) ? + null : + (Class) getFirstSuperClassFromPackage(superClass, packageName); + } + } + + /** + * Get the generic types of a given Class. + * + * @param parameterizedType parameterized Type + * @return generic types for the given Class. + */ + public static Set getGenericTypes(ParameterizedType parameterizedType) { + Set genericTypes = new LinkedHashSet<>(); + for(Type genericType : parameterizedType.getActualTypeArguments()) { + if (genericType instanceof Class) { + genericTypes.add((Class) genericType); + } + } + return genericTypes; + } + + /** + * Get class package name. + * + * @param className Class name. + * @return class package name + */ + public static String getClassPackageName(String className) { + try { + Class clazz = getClassOrNull(className); + if(clazz == null) { + return null; + } + Package classPackage = clazz.getPackage(); + return classPackage != null ? classPackage.getName() : null; + } catch (Exception e) { + return null; + } + } + + /** + * Get the {@link Member} with the given name belonging to the provided Java {@link Class} or {@code null} + * if no {@link Member} was found. + * + * @param targetClass the provided Java {@link Class} the field or method belongs to + * @param memberName the {@link Field} or {@link Method} name + * @return the {@link Field} or {@link Method} matching the given name or {@code null} + */ + public static Member getMemberOrNull(Class targetClass, String memberName) { + Field field = getFieldOrNull(targetClass, memberName); + return (field != null) ? field : getMethodOrNull(targetClass, memberName); + } + + /** + * Get the generic {@link Type} of the {@link Member} with the given name belonging to the provided Java {@link Class} or {@code null} + * if no {@link Member} was found. + * + * @param targetClass the provided Java {@link Class} the field or method belongs to + * @param memberName the {@link Field} or {@link Method} name + * @return the generic {@link Type} of the {@link Field} or {@link Method} matching the given name or {@code null} + */ + public static Type getMemberGenericTypeOrNull(Class targetClass, String memberName) { + Field field = getFieldOrNull(targetClass, memberName); + return (field != null) ? field.getGenericType() : getMethodOrNull(targetClass, memberName).getGenericReturnType(); + } + + /** + * Get classes by their package name + * @param packageName package name + * @return classes + */ + public static List getClassesByPackage(String packageName) { + List classes = new ArrayList<>(); + + try { + final String packagePath = packageName.replace('.', File.separatorChar); + final String javaClassExtension = ".class"; + try (Stream allPaths = Files.walk(Paths.get(getResource(packagePath).toURI()))) { + allPaths.filter(Files::isRegularFile).forEach(file -> { + final String path = file.toString().replace(File.separatorChar, '.'); + final String name = path.substring( + path.indexOf(packageName), + path.length() - javaClassExtension.length() + ); + classes.add(ReflectionUtils.getClass(name)); + }); + } + } catch (URISyntaxException | IOException e) { + throw new IllegalArgumentException(e); + } + + return classes; + } + + /** + * Handle the {@link NoSuchFieldException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link NoSuchFieldException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(NoSuchFieldException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link NoSuchMethodException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link NoSuchMethodException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(NoSuchMethodException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link IllegalAccessException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link IllegalAccessException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(IllegalAccessException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link InvocationTargetException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link InvocationTargetException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(InvocationTargetException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link ClassNotFoundException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link ClassNotFoundException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(ClassNotFoundException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link InstantiationException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link InstantiationException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(InstantiationException e) { + return new IllegalArgumentException(e); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/SpringTransactionUtils.java b/core/src/test/java/com/vladmihalcea/hpjp/util/SpringTransactionUtils.java new file mode 100644 index 000000000..8bf39ddf2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/SpringTransactionUtils.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.hpjp.util; + +import jakarta.persistence.EntityManager; +import org.springframework.orm.jpa.EntityManagerHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * SpringTransactionUtils - Spring Transaction utilities holder. + * + * @author Vlad Mihalcea + */ +public final class SpringTransactionUtils { + + private SpringTransactionUtils() { + throw new UnsupportedOperationException("SpringTransactionUtils is not instantiable!"); + } + + /** + * Return the current {@link EntityManager} instance bound to the current running + * transaction. + * + * @return current {@link EntityManager} + */ + public static EntityManager currentEntityManager() { + return TransactionSynchronizationManager.getResourceMap() + .values() + .stream() + .filter(EntityManagerHolder.class::isInstance) + .map(eh -> ((EntityManagerHolder) eh).getEntityManager()) + .findAny() + .orElse(null); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/StackTraceUtils.java b/core/src/test/java/com/vladmihalcea/hpjp/util/StackTraceUtils.java new file mode 100644 index 000000000..4d0522f6a --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/StackTraceUtils.java @@ -0,0 +1,43 @@ +package com.vladmihalcea.hpjp.util; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * StackTraceUtils - Stack Trace utilities holder. + * + * @author Vlad Mihalcea + */ +public final class StackTraceUtils { + + private StackTraceUtils() { + throw new UnsupportedOperationException("StackTraceUtils is not instantiable!"); + } + + /** + * Filter the stack trace based on the provide package name prefix + * + * @param packageNamePrefix package name to match the {@link StackTraceElement} to be returned + * @return the {@link StackTraceElement} objects matching the provided package name + */ + public static List stackTraceElements(String packageNamePrefix) { + return Arrays.stream(Thread.currentThread().getStackTrace()).filter( + stackTraceElement -> { + String packageName = ReflectionUtils.getClassPackageName(stackTraceElement.getClassName()); + + return packageName != null && packageName.startsWith(packageNamePrefix); + } + ).collect(Collectors.toList()); + } + + /** + * Build a {@link String} path from the provided list of {@link StackTraceElement} objects. + * + * @param stackTraceElements list of {@link StackTraceElement} objects + * @return {@link String} path + */ + public static String stackTracePath(List stackTraceElements) { + return stackTraceElements.stream().map(StackTraceElement::toString).collect(Collectors.joining(" -> ")); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/TsidUtils.java b/core/src/test/java/com/vladmihalcea/hpjp/util/TsidUtils.java new file mode 100644 index 000000000..32ffd6865 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/TsidUtils.java @@ -0,0 +1,59 @@ +package com.vladmihalcea.hpjp.util; + +import io.hypersistence.tsid.TSID; + +/** + * TsidUtils - Tsid utilities holder. + * + * @author Vlad Mihalcea + */ +public class TsidUtils { + public static final String TSID_NODE_COUNT_PROPERTY = "tsid.node.count"; + public static final String TSID_NODE_COUNT_ENV = "TSID_NODE_COUNT"; + + public static TSID.Factory TSID_FACTORY; + + static { + String nodeCountSetting = System.getProperty( + TSID_NODE_COUNT_PROPERTY + ); + if (nodeCountSetting == null) { + nodeCountSetting = System.getenv( + TSID_NODE_COUNT_ENV + ); + } + + int nodeCount = nodeCountSetting != null ? + Integer.parseInt(nodeCountSetting) : + 256; + + TSID_FACTORY = getTsidFactory(nodeCount); + } + + private TsidUtils() { + throw new UnsupportedOperationException("TsidUtils is not instantiable!"); + } + + public static TSID randomTsid() { + return TSID_FACTORY.generate(); + } + + public static TSID.Factory getTsidFactory(int nodeCount) { + int nodeBits = ((int) (Math.log(nodeCount) / Math.log(2))) + 1; + + return TSID.Factory.builder() + .withRandomFunction(TSID.Factory.THREAD_LOCAL_RANDOM_FUNCTION) + .withNodeBits(nodeBits) + .build(); + } + + public static TSID.Factory getTsidFactory(int nodeCount, int nodeId) { + int nodeBits = ((int) (Math.log(nodeCount) / Math.log(2))) + 1; + + return TSID.Factory.builder() + .withRandomFunction(TSID.Factory.THREAD_LOCAL_RANDOM_FUNCTION) + .withNodeBits(nodeBits) + .withNode(nodeId) + .build(); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/XmlUtils.java b/core/src/test/java/com/vladmihalcea/hpjp/util/XmlUtils.java new file mode 100644 index 000000000..f423e56ff --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/XmlUtils.java @@ -0,0 +1,30 @@ +package com.vladmihalcea.hpjp.util; + +import org.w3c.dom.Document; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +/** + * @author Vlad Mihalcea + */ +public class XmlUtils { + + public static Document readXmlDocument(String xmlValue) { + return readXmlDocument(xmlValue.getBytes(StandardCharsets.UTF_8)); + } + + public static Document readXmlDocument(byte[] xmlBytes) { + try { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + Document doc = dBuilder.parse(new ByteArrayInputStream(xmlBytes)); + doc.getDocumentElement().normalize(); + return doc; + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/exception/DataAccessException.java b/core/src/test/java/com/vladmihalcea/hpjp/util/exception/DataAccessException.java similarity index 91% rename from core/src/test/java/com/vladmihalcea/book/hpjp/util/exception/DataAccessException.java rename to core/src/test/java/com/vladmihalcea/hpjp/util/exception/DataAccessException.java index a8585041c..2aeff4754 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/exception/DataAccessException.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/exception/DataAccessException.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.util.exception; +package com.vladmihalcea.hpjp.util.exception; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/exception/ExceptionUtil.java b/core/src/test/java/com/vladmihalcea/hpjp/util/exception/ExceptionUtil.java new file mode 100644 index 000000000..79d6cc6e2 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/exception/ExceptionUtil.java @@ -0,0 +1,155 @@ +package com.vladmihalcea.hpjp.util.exception; + +import org.hibernate.PessimisticLockException; +import org.hibernate.exception.LockAcquisitionException; + +import jakarta.persistence.LockTimeoutException; +import java.sql.SQLTimeoutException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author Vlad Mihalcea + */ +public interface ExceptionUtil { + + List> LOCK_TIMEOUT_EXCEPTIONS = Arrays.asList( + LockAcquisitionException.class, + LockTimeoutException.class, + PessimisticLockException.class, + jakarta.persistence.PessimisticLockException.class, + SQLTimeoutException.class + ); + + /** + * Get the root cause of a particular {@code Throwable} + * + * @param t exception + * @return exception root cause + */ + static T rootCause(Throwable t) { + Throwable cause = t.getCause(); + if (cause != null && cause != t) { + return rootCause(cause); + } + return (T) t; + } + + /** + * Is the given throwable caused by a database lock timeout? + * + * @param e exception + * @return is caused by a database lock timeout + */ + static boolean isLockTimeout(Throwable e) { + AtomicReference causeHolder = new AtomicReference<>(e); + do { + final Throwable cause = causeHolder.get(); + final String failureMessage = cause.getMessage().toLowerCase(); + if (LOCK_TIMEOUT_EXCEPTIONS.stream().anyMatch(c -> c.isInstance(cause)) || + failureMessage.contains("timeout") || + failureMessage.contains("timed out") || + failureMessage.contains("time out") || + failureMessage.contains("closed connection") || + failureMessage.contains("link failure") || + failureMessage.contains("expired or aborted by a conflict") + ) { + return true; + } else { + if (cause.getCause() == null || cause.getCause() == cause) { + break; + } else { + causeHolder.set(cause.getCause()); + } + } + } + while (true); + return false; + } + + /** + * Is the given throwable caused by the following exception type? + * + * @param e exception + * @param exceptionType exception type + * @return is caused by the given exception type + */ + static boolean isCausedBy(Throwable e, Class exceptionType) { + AtomicReference causeHolder = new AtomicReference<>(e); + do { + final Throwable cause = causeHolder.get(); + if (exceptionType.isInstance(cause)) { + return true; + } else { + if (cause.getCause() == null || cause.getCause() == cause) { + break; + } else { + causeHolder.set(cause.getCause()); + } + } + } + while (true); + return false; + } + + /** + * Is the given throwable caused by a database MVCC anomaly detection? + * + * @param e exception + * @return is caused by a database lock MVCC anomaly detection + */ + static boolean isMVCCAnomalyDetection(Throwable e) { + AtomicReference causeHolder = new AtomicReference<>(e); + do { + final Throwable cause = causeHolder.get(); + String lowerCaseMessage = cause.getMessage().toLowerCase(); + if ( + cause.getMessage().contains("ORA-08177: can't serialize access for this transaction") //Oracle + || lowerCaseMessage.contains("could not serialize access due to concurrent update") //PSQLException + || lowerCaseMessage.contains("ould not serialize access due to read/write dependencies among transactions") //PSQLException + || lowerCaseMessage.contains("snapshot isolation transaction aborted due to update conflict") //SQLServerException + || lowerCaseMessage.contains("kconflict") //YugabyteDB + || lowerCaseMessage.contains("unknown transaction, could be recently aborted") //YugabyteDB + || lowerCaseMessage.contains("conflicts with higher priority transaction") //YugabyteDB + ) { + return true; + } else { + if (cause.getCause() == null || cause.getCause() == cause) { + break; + } else { + causeHolder.set(cause.getCause()); + } + } + } + while (true); + return false; + } + + /** + * Was the given exception caused by a SQL connection close + * + * @param e exception + * @return is caused by a SQL connection close + */ + static boolean isConnectionClose(Exception e) { + Throwable cause = e; + do { + if (cause.getMessage().toLowerCase().contains("connection is close") + || cause.getMessage().toLowerCase().contains("closed connection") + || cause.getMessage().toLowerCase().contains("link failure") + || cause.getMessage().toLowerCase().contains("closed") + ) { + return true; + } else { + if (cause.getCause() == null || cause.getCause() == cause) { + break; + } else { + cause = cause.getCause(); + } + } + } + while (true); + return false; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/logging/InlineQueryLogEntryCreator.java b/core/src/test/java/com/vladmihalcea/hpjp/util/logging/InlineQueryLogEntryCreator.java new file mode 100644 index 000000000..3c2f12902 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/logging/InlineQueryLogEntryCreator.java @@ -0,0 +1,86 @@ +package com.vladmihalcea.hpjp.util.logging; + +import net.ttddyy.dsproxy.ExecutionInfo; +import net.ttddyy.dsproxy.QueryInfo; +import net.ttddyy.dsproxy.listener.logging.DefaultQueryLogEntryCreator; + +import java.util.*; + +/** + * @author Vlad Mihalcea + */ +public class InlineQueryLogEntryCreator extends DefaultQueryLogEntryCreator { + + @Override + protected void writeParamsEntry(StringBuilder sb, ExecutionInfo execInfo, List queryInfoList) { + sb.append("Params:["); + for (QueryInfo queryInfo : queryInfoList) { + boolean firstArg = true; + for (Map paramMap : queryInfo.getQueryArgsList()) { + + if (!firstArg) { + sb.append(", "); + } else { + firstArg = false; + } + + SortedMap sortedParamMap = new TreeMap<>(new CustomStringAsIntegerComparator()); + sortedParamMap.putAll(paramMap); + + sb.append("("); + boolean firstParam = true; + for (Map.Entry paramEntry : sortedParamMap.entrySet()) { + if (!firstParam) { + sb.append(", "); + } else { + firstParam = false; + } + Object parameter = paramEntry.getValue(); + if (parameter != null && parameter.getClass().isArray()) { + sb.append(arrayToString(parameter)); + } else { + sb.append(parameter); + } + } + sb.append(")"); + } + } + sb.append("]"); + } + + private String arrayToString(Object object) { + if (object.getClass().isArray()) { + if (object instanceof byte[]) { + return Arrays.toString((byte[]) object); + } + if (object instanceof short[]) { + return Arrays.toString((short[]) object); + } + if (object instanceof char[]) { + return Arrays.toString((char[]) object); + } + if (object instanceof int[]) { + return Arrays.toString((int[]) object); + } + if (object instanceof long[]) { + return Arrays.toString((long[]) object); + } + if (object instanceof float[]) { + return Arrays.toString((float[]) object); + } + if (object instanceof double[]) { + return Arrays.toString((double[]) object); + } + if (object instanceof boolean[]) { + return Arrays.toString((boolean[]) object); + } + if (object instanceof Object[]) { + return Arrays.toString((Object[]) object); + } + } + throw new UnsupportedOperationException("Array type not supported: " + object.getClass()); + } + + private static class CustomStringAsIntegerComparator extends StringAsIntegerComparator { + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/AbstractContainerDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/AbstractContainerDataSourceProvider.java new file mode 100644 index 000000000..ef7923d5e --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/AbstractContainerDataSourceProvider.java @@ -0,0 +1,47 @@ +package com.vladmihalcea.hpjp.util.providers; + +import org.testcontainers.containers.JdbcDatabaseContainer; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * To test the Docker integration. + * + * Check the available PostgreSQL service: + * + * sc queryex type=service state=all | find /i "SERVICE_NAME: postgres" + * + * net stop postgresql-x64-15 + * + * @author Vlad Mihalcea + */ +public abstract class AbstractContainerDataSourceProvider implements DataSourceProvider { + + @Override + public DataSource dataSource() { + DataSource dataSource = newDataSource(); + try(Connection connection = dataSource.getConnection()) { + return dataSource; + } catch (SQLException e) { + Database database = database(); + if(database.getContainer() == null) { + database.initContainer(username(), password()); + } + return newDataSource(); + } + } + + @Override + public String url() { + JdbcDatabaseContainer container = database().getContainer(); + return container != null ? + container.getJdbcUrl() : + defaultJdbcUrl(); + } + + protected abstract String defaultJdbcUrl(); + + protected abstract DataSource newDataSource(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/CockroachDBDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/CockroachDBDataSourceProvider.java new file mode 100644 index 000000000..dd8f125ed --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/CockroachDBDataSourceProvider.java @@ -0,0 +1,102 @@ +package com.vladmihalcea.hpjp.util.providers; + +import com.vladmihalcea.hpjp.util.providers.queries.PostgreSQLQueries; +import com.vladmihalcea.hpjp.util.providers.queries.Queries; +import com.zaxxer.hikari.util.DriverDataSource; +import org.hibernate.dialect.CockroachDialect; +import org.postgresql.Driver; +import org.postgresql.ds.PGSimpleDataSource; +import org.testcontainers.containers.JdbcDatabaseContainer; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class CockroachDBDataSourceProvider extends AbstractContainerDataSourceProvider { + + @Override + public String hibernateDialect() { + return CockroachDialect.class.getName(); + } + + @Override + protected String defaultJdbcUrl() { + return String.format( + "jdbc:postgresql://%s:%d/high_performance_java_persistence", + host(), + port() + ); + } + + protected DataSource newDataSource() { + JdbcDatabaseContainer container = database().getContainer(); + if (container != null) { + Properties properties = new Properties(); + return new DriverDataSource( + container.getJdbcUrl(), + container.getDriverClassName(), + properties, + container.getUsername(), + container.getPassword() + ); + } + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setURL(url()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + dataSource.setSsl(false); + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return PGSimpleDataSource.class; + } + + @Override + public Class driverClassName() { + return Driver.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("databaseName", "high_performance_java_persistence"); + properties.setProperty("serverName", host()); + properties.setProperty("portNumber", String.valueOf(port())); + properties.setProperty("user", username()); + properties.setProperty("password", password()); + properties.setProperty("sslmode", "disabled"); + return properties; + } + + public String host() { + return "127.0.0.1"; + } + + public int port() { + return 26257; + } + + @Override + public String username() { + return "cockroach"; + } + + @Override + public String password() { + return "admin"; + } + + @Override + public Database database() { + return Database.COCKROACHDB; + } + + @Override + public Queries queries() { + return PostgreSQLQueries.INSTANCE; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/DataSourceProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/DataSourceProvider.java new file mode 100644 index 000000000..86932721f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/DataSourceProvider.java @@ -0,0 +1,43 @@ +package com.vladmihalcea.hpjp.util.providers; + +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import com.vladmihalcea.hpjp.util.providers.queries.Queries; +import org.hibernate.dialect.Dialect; + +import java.util.Properties; +import javax.sql.DataSource; + +/** + * @author Vlad Mihalcea + */ +public interface DataSourceProvider { + + enum IdentifierStrategy { + IDENTITY, + SEQUENCE + } + + String hibernateDialect(); + + DataSource dataSource(); + + Class driverClassName(); + + Class dataSourceClassName(); + + Properties dataSourceProperties(); + + String url(); + + String username(); + + String password(); + + Database database(); + + Queries queries(); + + default Class hibernateDialectClass() { + return ReflectionUtils.getClass(hibernateDialect()); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/Database.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/Database.java new file mode 100644 index 000000000..e143020ca --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/Database.java @@ -0,0 +1,186 @@ +package com.vladmihalcea.hpjp.util.providers; + +import com.vladmihalcea.hpjp.util.ReflectionUtils; +import org.hibernate.dialect.Dialect; +import org.testcontainers.containers.*; + +import java.util.Collections; + +/** + * @author Vlad Mihalcea + */ +public enum Database { + //Mandatory databases + HSQLDB { + @Override + public Class dataSourceProviderClass() { + return HSQLDBDataSourceProvider.class; + } + }, + POSTGRESQL { + @Override + public Class dataSourceProviderClass() { + return PostgreSQLDataSourceProvider.class; + } + + @Override + protected JdbcDatabaseContainer newJdbcDatabaseContainer() { + return new PostgreSQLContainer("postgres:15.3"); + } + }, + ORACLE { + @Override + public Class dataSourceProviderClass() { + return OracleDataSourceProvider.class; + } + + @Override + protected JdbcDatabaseContainer newJdbcDatabaseContainer() { + return new OracleContainer("gvenzl/oracle-xe:21.3.0-slim"); + } + + @Override + protected boolean supportsDatabaseName() { + return false; + } + }, + MYSQL { + @Override + public Class dataSourceProviderClass() { + return MySQLDataSourceProvider.class; + } + + @Override + protected JdbcDatabaseContainer newJdbcDatabaseContainer() { + return new MySQLContainer("mysql:8.0"); + } + }, + SQLSERVER { + @Override + public Class dataSourceProviderClass() { + return SQLServerDataSourceProvider.class; + } + + @Override + protected JdbcDatabaseContainer newJdbcDatabaseContainer() { + return new MSSQLServerContainer("mcr.microsoft.com/mssql/server:2019-latest"); + } + + @Override + protected boolean supportsDatabaseName() { + return false; + } + + @Override + protected boolean supportsCredentials() { + return false; + } + }, + MARIADB { + @Override + public Class dataSourceProviderClass() { + return MariaDBDataSourceProvider.class; + } + + @Override + protected JdbcDatabaseContainer newJdbcDatabaseContainer() { + return new MariaDBContainer("mariadb:10.10"); + } + }, + YUGABYTEDB { + @Override + public Class dataSourceProviderClass() { + return YugabyteDBDataSourceProvider.class; + } + + @Override + protected JdbcDatabaseContainer newJdbcDatabaseContainer() { + return new YugabyteDBYSQLContainer("yugabytedb/yugabyte:2.14.4.0-b26"); + } + }, + COCKROACHDB { + @Override + public Class dataSourceProviderClass() { + return CockroachDBDataSourceProvider.class; + } + + @Override + protected JdbcDatabaseContainer newJdbcDatabaseContainer() { + return new CockroachContainer("cockroachdb/cockroach:v22.2.10"); + } + + protected String databaseName() { + return "high_performance_java_persistence"; + } + }, + //These databases require manual setup + YUGABYTEDB_CLUSTER { + @Override + public Class dataSourceProviderClass() { + return YugabyteDBClusterDataSourceProvider.class; + } + }, + ; + + private JdbcDatabaseContainer container; + + public JdbcDatabaseContainer getContainer() { + return container; + } + + public DataSourceProvider dataSourceProvider() { + return ReflectionUtils.newInstance(dataSourceProviderClass().getName()); + } + + public abstract Class dataSourceProviderClass(); + + public void initContainer(String username, String password) { + container = (JdbcDatabaseContainer) newJdbcDatabaseContainer() + .withReuse(true) + .withEnv(Collections.singletonMap("ACCEPT_EULA", "Y")) + .withTmpFs(Collections.singletonMap("/testtmpfs", "rw")); + if(supportsDatabaseName()) { + container.withDatabaseName(databaseName()); + } + if(supportsCredentials()) { + container.withUsername(username).withPassword(password); + } + container.start(); + } + + protected JdbcDatabaseContainer newJdbcDatabaseContainer() { + throw new UnsupportedOperationException( + String.format( + "The [%s] database was not configured to use Testcontainers!", + name() + ) + ); + } + + protected boolean supportsDatabaseName() { + return true; + } + + protected String databaseName() { + return "high-performance-java-persistence"; + } + + protected boolean supportsCredentials() { + return true; + } + + public static Database of(Dialect dialect) { + Class dialectClass = dialect.getClass(); + for(Database database : values()) { + if(database.dataSourceProvider().hibernateDialectClass().isAssignableFrom(dialectClass)) { + return database; + } + } + throw new UnsupportedOperationException( + String.format( + "The provided Dialect [%s] is not supported!", + dialectClass + ) + ); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/FastOracleDialect.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/FastOracleDialect.java new file mode 100644 index 000000000..ac392d81d --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/FastOracleDialect.java @@ -0,0 +1,29 @@ +package com.vladmihalcea.hpjp.util.providers; + +import org.hibernate.dialect.DatabaseVersion; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl; +import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; + +/*** + * @author Vlad Mihalcea + */ +public class FastOracleDialect extends OracleDialect { + + public FastOracleDialect() { + } + + public FastOracleDialect(DatabaseVersion version) { + super(version); + } + + public FastOracleDialect(DialectResolutionInfo info) { + super(info); + } + + @Override + public SequenceInformationExtractor getSequenceInformationExtractor() { + return SequenceInformationExtractorNoOpImpl.INSTANCE; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/HSQLDBDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/HSQLDBDataSourceProvider.java new file mode 100644 index 000000000..8aa905f20 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/HSQLDBDataSourceProvider.java @@ -0,0 +1,74 @@ +package com.vladmihalcea.hpjp.util.providers; + +import com.vladmihalcea.hpjp.util.providers.queries.HSQLDBServerQueries; +import com.vladmihalcea.hpjp.util.providers.queries.Queries; +import org.hibernate.dialect.HSQLDialect; +import org.hsqldb.jdbc.JDBCDataSource; +import org.hsqldb.jdbc.JDBCDriver; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class HSQLDBDataSourceProvider implements DataSourceProvider { + + @Override + public String hibernateDialect() { + return HSQLDialect.class.getName(); + } + + @Override + public DataSource dataSource() { + JDBCDataSource dataSource = new JDBCDataSource(); + dataSource.setUrl(url()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return JDBCDataSource.class; + } + + @Override + public Class driverClassName() { + return JDBCDriver.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("url", url()); + properties.setProperty("user", username()); + properties.setProperty("password", password()); + return properties; + } + + @Override + public String url() { + return "jdbc:hsqldb:mem:test"; + } + + @Override + public String username() { + return "sa"; + } + + @Override + public String password() { + return ""; + } + + @Override + public Database database() { + return Database.HSQLDB; + } + + @Override + public Queries queries() { + return HSQLDBServerQueries.INSTANCE; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/LegacyOracleDialect.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/LegacyOracleDialect.java new file mode 100644 index 000000000..fa18977a9 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/LegacyOracleDialect.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.util.providers; + +import org.hibernate.dialect.DatabaseVersion; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.pagination.LegacyOracleLimitHandler; +import org.hibernate.dialect.pagination.LimitHandler; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl; +import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; + +/*** + * @author Vlad Mihalcea + */ +public class LegacyOracleDialect extends OracleDialect { + + public LegacyOracleDialect() { + } + + public LegacyOracleDialect(DatabaseVersion version) { + super(version); + } + + public LegacyOracleDialect(DialectResolutionInfo info) { + super(info); + } + + @Override + public SequenceInformationExtractor getSequenceInformationExtractor() { + return SequenceInformationExtractorNoOpImpl.INSTANCE; + } + + @Override + public LimitHandler getLimitHandler() { + return new LegacyOracleLimitHandler(DatabaseVersion.make(21)); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/LockType.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/LockType.java new file mode 100644 index 000000000..713c62b16 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/LockType.java @@ -0,0 +1,10 @@ +package com.vladmihalcea.hpjp.util.providers; + +/** + * @author Vlad Mihalcea + */ +public enum LockType { + LOCKS, + MVLOCKS, + MVCC +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/MariaDBDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/MariaDBDataSourceProvider.java new file mode 100644 index 000000000..79fa1574b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/MariaDBDataSourceProvider.java @@ -0,0 +1,168 @@ +package com.vladmihalcea.hpjp.util.providers; + +import org.mariadb.jdbc.Driver; +import com.vladmihalcea.hpjp.util.providers.queries.MySQLQueries; +import com.vladmihalcea.hpjp.util.providers.queries.Queries; +import com.zaxxer.hikari.util.DriverDataSource; +import org.hibernate.dialect.MariaDBDialect; +import org.mariadb.jdbc.MariaDbDataSource; +import org.testcontainers.containers.JdbcDatabaseContainer; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class MariaDBDataSourceProvider extends AbstractContainerDataSourceProvider { + + private boolean rewriteBatchedStatements = true; + + private boolean cachePrepStmts = false; + + private boolean useServerPrepStmts = false; + + private boolean useTimezone = false; + + private boolean useJDBCCompliantTimezoneShift = false; + + private boolean useLegacyDatetimeCode = true; + + public boolean isRewriteBatchedStatements() { + return rewriteBatchedStatements; + } + + public void setRewriteBatchedStatements(boolean rewriteBatchedStatements) { + this.rewriteBatchedStatements = rewriteBatchedStatements; + } + + public boolean isCachePrepStmts() { + return cachePrepStmts; + } + + public void setCachePrepStmts(boolean cachePrepStmts) { + this.cachePrepStmts = cachePrepStmts; + } + + public boolean isUseServerPrepStmts() { + return useServerPrepStmts; + } + + public void setUseServerPrepStmts(boolean useServerPrepStmts) { + this.useServerPrepStmts = useServerPrepStmts; + } + + public boolean isUseTimezone() { + return useTimezone; + } + + public void setUseTimezone(boolean useTimezone) { + this.useTimezone = useTimezone; + } + + public boolean isUseJDBCCompliantTimezoneShift() { + return useJDBCCompliantTimezoneShift; + } + + public void setUseJDBCCompliantTimezoneShift(boolean useJDBCCompliantTimezoneShift) { + this.useJDBCCompliantTimezoneShift = useJDBCCompliantTimezoneShift; + } + + public boolean isUseLegacyDatetimeCode() { + return useLegacyDatetimeCode; + } + + public void setUseLegacyDatetimeCode(boolean useLegacyDatetimeCode) { + this.useLegacyDatetimeCode = useLegacyDatetimeCode; + } + + @Override + public String hibernateDialect() { + return MariaDBDialect.class.getName(); + } + + @Override + protected String defaultJdbcUrl() { + return "jdbc:mariadb://localhost/high_performance_java_persistence " + + "?rewriteBatchedStatements=" + rewriteBatchedStatements + + "&cachePrepStmts=" + cachePrepStmts + + "&useServerPrepStmts=" + useServerPrepStmts; + } + + @Override + protected DataSource newDataSource() { + JdbcDatabaseContainer container = database().getContainer(); + if(container != null) { + Properties properties = new Properties(); + properties.setProperty("rewriteBatchedStatements", String.valueOf(rewriteBatchedStatements)); + properties.setProperty("cachePrepStmts", String.valueOf(cachePrepStmts)); + properties.setProperty("useServerPrepStmts", String.valueOf(useServerPrepStmts)); + return new DriverDataSource( + container.getJdbcUrl(), + container.getDriverClassName(), + properties, + container.getUsername(), + container.getPassword() + ); + } + MariaDbDataSource dataSource = new MariaDbDataSource(); + try { + dataSource.setUrl(defaultJdbcUrl()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return MariaDbDataSource.class; + } + + @Override + public Class driverClassName() { + return Driver.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("url", url()); + return properties; + } + + @Override + public String username() { + return "mariadb"; + } + + @Override + public String password() { + return "admin"; + } + + @Override + public Database database() { + return Database.MARIADB; + } + + @Override + public String toString() { + return "MariaDBDataSourceProvider{" + + "rewriteBatchedStatements=" + rewriteBatchedStatements + + ", cachePrepStmts=" + cachePrepStmts + + ", useServerPrepStmts=" + useServerPrepStmts + + ", useTimezone=" + useTimezone + + ", useJDBCCompliantTimezoneShift=" + useJDBCCompliantTimezoneShift + + ", useLegacyDatetimeCode=" + useLegacyDatetimeCode + + '}'; + } + + @Override + public Queries queries() { + return MySQLQueries.INSTANCE; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/MySQLDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/MySQLDataSourceProvider.java new file mode 100644 index 000000000..000b17fa7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/MySQLDataSourceProvider.java @@ -0,0 +1,191 @@ +package com.vladmihalcea.hpjp.util.providers; + +import com.mysql.cj.jdbc.Driver; +import com.mysql.cj.jdbc.MysqlDataSource; +import com.vladmihalcea.hpjp.util.providers.queries.MySQLQueries; +import com.vladmihalcea.hpjp.util.providers.queries.Queries; +import org.hibernate.dialect.MySQLDialect; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class MySQLDataSourceProvider extends AbstractContainerDataSourceProvider { + + private Boolean rewriteBatchedStatements; + + private Boolean cachePrepStmts; + + private Boolean useServerPrepStmts; + + private Boolean useTimezone; + + private Boolean useJDBCCompliantTimezoneShift; + + private Boolean useLegacyDatetimeCode; + + private Boolean useCursorFetch; + + private Integer prepStmtCacheSqlLimit; + + public boolean isRewriteBatchedStatements() { + return rewriteBatchedStatements; + } + + public MySQLDataSourceProvider setRewriteBatchedStatements(boolean rewriteBatchedStatements) { + this.rewriteBatchedStatements = rewriteBatchedStatements; + return this; + } + + public boolean isCachePrepStmts() { + return cachePrepStmts; + } + + public MySQLDataSourceProvider setCachePrepStmts(boolean cachePrepStmts) { + this.cachePrepStmts = cachePrepStmts; + return this; + } + + public boolean isUseServerPrepStmts() { + return useServerPrepStmts; + } + + public MySQLDataSourceProvider setUseServerPrepStmts(boolean useServerPrepStmts) { + this.useServerPrepStmts = useServerPrepStmts; + return this; + } + + public boolean isUseTimezone() { + return useTimezone; + } + + public MySQLDataSourceProvider setUseTimezone(boolean useTimezone) { + this.useTimezone = useTimezone; + return this; + } + + public boolean isUseJDBCCompliantTimezoneShift() { + return useJDBCCompliantTimezoneShift; + } + + public MySQLDataSourceProvider setUseJDBCCompliantTimezoneShift(boolean useJDBCCompliantTimezoneShift) { + this.useJDBCCompliantTimezoneShift = useJDBCCompliantTimezoneShift; + return this; + } + + public boolean isUseLegacyDatetimeCode() { + return useLegacyDatetimeCode; + } + + public MySQLDataSourceProvider setUseLegacyDatetimeCode(boolean useLegacyDatetimeCode) { + this.useLegacyDatetimeCode = useLegacyDatetimeCode; + return this; + } + + public boolean isUseCursorFetch() { + return useCursorFetch; + } + + public MySQLDataSourceProvider setUseCursorFetch(boolean useCursorFetch) { + this.useCursorFetch = useCursorFetch; + return this; + } + + public Integer getPrepStmtCacheSqlLimit() { + return prepStmtCacheSqlLimit; + } + + public MySQLDataSourceProvider setPrepStmtCacheSqlLimit(Integer prepStmtCacheSqlLimit) { + this.prepStmtCacheSqlLimit = prepStmtCacheSqlLimit; + return this; + } + + @Override + public String hibernateDialect() { + return MySQLDialect.class.getName(); + } + + @Override + protected String defaultJdbcUrl() { + return "jdbc:mysql://localhost/high_performance_java_persistence?useSSL=false"; + } + + @Override + protected DataSource newDataSource() { + try { + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setURL(url()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + + if (rewriteBatchedStatements != null) { + dataSource.setRewriteBatchedStatements(rewriteBatchedStatements); + } + if (useCursorFetch != null) { + dataSource.setUseCursorFetch(useCursorFetch); + } + if (cachePrepStmts != null) { + dataSource.setCachePrepStmts(cachePrepStmts); + } + if (useServerPrepStmts != null) { + dataSource.setUseServerPrepStmts(useServerPrepStmts); + } + if (prepStmtCacheSqlLimit != null) { + dataSource.setPrepStmtCacheSqlLimit(prepStmtCacheSqlLimit); + } + + return dataSource; + } catch (SQLException e) { + throw new IllegalStateException("The DataSource could not be instantiated!"); + } + } + + @Override + public Class dataSourceClassName() { + return MysqlDataSource.class; + } + + @Override + public Class driverClassName() { + return Driver.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("url", url()); + return properties; + } + + @Override + public String username() { + return "mysql"; + } + + @Override + public String password() { + return "admin"; + } + + @Override + public Database database() { + return Database.MYSQL; + } + + @Override + public String toString() { + return "MySQLDataSourceProvider{" + + "cachePrepStmts=" + cachePrepStmts + + ", useServerPrepStmts=" + useServerPrepStmts + + ", rewriteBatchedStatements=" + rewriteBatchedStatements + + '}'; + } + + @Override + public Queries queries() { + return MySQLQueries.INSTANCE; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/OracleDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/OracleDataSourceProvider.java new file mode 100644 index 000000000..c69201ff3 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/OracleDataSourceProvider.java @@ -0,0 +1,86 @@ +package com.vladmihalcea.hpjp.util.providers; + +import com.vladmihalcea.hpjp.util.providers.queries.OracleQueries; +import com.vladmihalcea.hpjp.util.providers.queries.Queries; +import oracle.jdbc.pool.OracleDataSource; +import org.postgresql.Driver; +import org.testcontainers.containers.JdbcDatabaseContainer; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class OracleDataSourceProvider extends AbstractContainerDataSourceProvider { + + @Override + public String hibernateDialect() { + return FastOracleDialect.class.getName(); + } + + @Override + public String defaultJdbcUrl() { + return "jdbc:oracle:thin:@localhost:1521/xe"; + } + + @Override + public DataSource newDataSource() { + try { + OracleDataSource dataSource = new OracleDataSource(); + JdbcDatabaseContainer container = database().getContainer(); + if(container == null) { + dataSource.setDatabaseName("high_performance_java_persistence"); + } else { + dataSource.setDatabaseName(container.getDatabaseName()); + } + dataSource.setURL(url()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + return dataSource; + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @Override + public Class dataSourceClassName() { + return OracleDataSource.class; + } + + @Override + public Class driverClassName() { + return Driver.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("databaseName", "high_performance_java_persistence"); + properties.setProperty("URL", url()); + properties.setProperty("user", username()); + properties.setProperty("password", password()); + return properties; + } + + @Override + public String username() { + return "oracle"; + } + + @Override + public String password() { + return "admin"; + } + + @Override + public Database database() { + return Database.ORACLE; + } + + @Override + public Queries queries() { + return OracleQueries.INSTANCE; + } + +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/PostgreSQLDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/PostgreSQLDataSourceProvider.java new file mode 100644 index 000000000..67aadbdcb --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/PostgreSQLDataSourceProvider.java @@ -0,0 +1,92 @@ +package com.vladmihalcea.hpjp.util.providers; + +import com.vladmihalcea.hpjp.util.providers.queries.PostgreSQLQueries; +import com.vladmihalcea.hpjp.util.providers.queries.Queries; +import org.hibernate.dialect.PostgreSQLDialect; +import org.postgresql.Driver; +import org.postgresql.ds.PGSimpleDataSource; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLDataSourceProvider extends AbstractContainerDataSourceProvider { + + private Boolean reWriteBatchedInserts; + + public boolean getReWriteBatchedInserts() { + return reWriteBatchedInserts; + } + + public PostgreSQLDataSourceProvider setReWriteBatchedInserts(boolean reWriteBatchedInserts) { + this.reWriteBatchedInserts = reWriteBatchedInserts; + return this; + } + + @Override + public String hibernateDialect() { + return PostgreSQLDialect.class.getName(); + } + + @Override + protected String defaultJdbcUrl() { + return "jdbc:postgresql://localhost/high_performance_java_persistence"; + } + + protected DataSource newDataSource() { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setURL(url()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + if (reWriteBatchedInserts != null) { + dataSource.setReWriteBatchedInserts(reWriteBatchedInserts); + } + + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return PGSimpleDataSource.class; + } + + @Override + public Class driverClassName() { + return Driver.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("databaseName", "high_performance_java_persistence"); + properties.setProperty("serverName", "localhost"); + properties.setProperty("user", username()); + properties.setProperty("password", password()); + if (reWriteBatchedInserts != null) { + properties.setProperty("reWriteBatchedInserts", String.valueOf(reWriteBatchedInserts)); + } + return properties; + } + + @Override + public String username() { + return "postgres"; + } + + @Override + public String password() { + return "admin"; + } + + @Override + public Database database() { + return Database.POSTGRESQL; + } + + @Override + public Queries queries() { + return PostgreSQLQueries.INSTANCE; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/SQLServerDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/SQLServerDataSourceProvider.java new file mode 100644 index 000000000..93ac91042 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/SQLServerDataSourceProvider.java @@ -0,0 +1,119 @@ +package com.vladmihalcea.hpjp.util.providers; + +import com.microsoft.sqlserver.jdbc.SQLServerDataSource; +import com.microsoft.sqlserver.jdbc.SQLServerDriver; +import com.vladmihalcea.hpjp.util.providers.queries.Queries; +import com.vladmihalcea.hpjp.util.providers.queries.SQLServerQueries; +import org.hibernate.dialect.SQLServerDialect; +import org.testcontainers.containers.JdbcDatabaseContainer; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerDataSourceProvider extends AbstractContainerDataSourceProvider { + + private boolean sendStringParametersAsUnicode = false; + + private Boolean useBulkCopyForBatchInsert; + + public boolean isSendStringParametersAsUnicode() { + return sendStringParametersAsUnicode; + } + + public SQLServerDataSourceProvider setSendStringParametersAsUnicode(boolean sendStringParametersAsUnicode) { + this.sendStringParametersAsUnicode = sendStringParametersAsUnicode; + return this; + } + + public Boolean getUseBulkCopyForBatchInsert() { + return useBulkCopyForBatchInsert; + } + + public SQLServerDataSourceProvider setUseBulkCopyForBatchInsert(Boolean useBulkCopyForBatchInsert) { + this.useBulkCopyForBatchInsert = useBulkCopyForBatchInsert; + return this; + } + + @Override + public String hibernateDialect() { + return SQLServerDialect.class.getName(); + } + + @Override + public String defaultJdbcUrl() { + return "jdbc:sqlserver://localhost;instance=SQLEXPRESS;databaseName=high_performance_java_persistence;encrypt=true;trustServerCertificate=true"; + } + + @Override + public DataSource newDataSource() { + SQLServerDataSource dataSource = new SQLServerDataSource(); + dataSource.setURL(url()); + JdbcDatabaseContainer container = database().getContainer(); + if(container == null) { + dataSource.setUser(username()); + dataSource.setPassword(password()); + } else { + dataSource.setUser(container.getUsername()); + dataSource.setPassword(container.getPassword()); + } + if (useBulkCopyForBatchInsert != null) { + dataSource.setUseBulkCopyForBatchInsert(useBulkCopyForBatchInsert); + } + dataSource.setSendStringParametersAsUnicode(sendStringParametersAsUnicode); + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return SQLServerDataSource.class; + } + + @Override + public Class driverClassName() { + return SQLServerDriver.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty( "URL", url() ); + return properties; + } + + @Override + public String username() { + return "sa"; + } + + @Override + public String password() { + return "adm1n"; + } + + @Override + public Database database() { + return Database.SQLSERVER; + } + + @Override + public Queries queries() { + return SQLServerQueries.INSTANCE; + } + + protected boolean appendCredentialsToUrl() { + return false; + } + + @Override + public String url() { + String url = super.url(); + if(appendCredentialsToUrl()) { + url += ";user=" + username(); + url += ";password=" + password(); + } + return url; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/YugabyteDBClusterDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/YugabyteDBClusterDataSourceProvider.java new file mode 100644 index 000000000..50ad792f0 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/YugabyteDBClusterDataSourceProvider.java @@ -0,0 +1,80 @@ +package com.vladmihalcea.hpjp.util.providers; + +import com.vladmihalcea.hpjp.util.providers.queries.PostgreSQLQueries; +import com.vladmihalcea.hpjp.util.providers.queries.Queries; +import com.yugabyte.ysql.YBClusterAwareDataSource; +import org.hibernate.dialect.PostgreSQLDialect; +import org.postgresql.Driver; + +import javax.sql.DataSource; + +/** + * @author Vlad Mihalcea + */ +public class YugabyteDBClusterDataSourceProvider extends YugabyteDBDataSourceProvider { + + public static final DataSourceProvider INSTANCE = new YugabyteDBDataSourceProvider(); + + private String host = "172.22.0.2"; + + private int port = 5433; + + private String database = "high_performance_java_persistence"; + + @Override + public String hibernateDialect() { + return PostgreSQLDialect.class.getName(); + } + + @Override + public DataSource dataSource() { + YBClusterAwareDataSource dataSource = new YBClusterAwareDataSource(); + dataSource.setURL(url()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + dataSource.setLoadBalanceHosts(true); + dataSource.setConnectTimeout(10); + dataSource.setSocketTimeout(10); + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return YBClusterAwareDataSource.class; + } + + @Override + public Class driverClassName() { + return Driver.class; + } + + @Override + public String url() { + return String.format( + "jdbc:yugabytedb://%s:%d/%s?load-balance=true", + host, + port, + database + ); + } + + @Override + public String username() { + return "yugabyte"; + } + + @Override + public String password() { + return "admin"; + } + + @Override + public Database database() { + return Database.YUGABYTEDB_CLUSTER; + } + + @Override + public Queries queries() { + return PostgreSQLQueries.INSTANCE; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/YugabyteDBDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/YugabyteDBDataSourceProvider.java new file mode 100644 index 000000000..ad8e69098 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/YugabyteDBDataSourceProvider.java @@ -0,0 +1,84 @@ +package com.vladmihalcea.hpjp.util.providers; + +import com.vladmihalcea.hpjp.util.providers.queries.PostgreSQLQueries; +import com.vladmihalcea.hpjp.util.providers.queries.Queries; +import com.zaxxer.hikari.util.DriverDataSource; +import org.hibernate.dialect.PostgreSQLDialect; +import org.postgresql.Driver; +import org.postgresql.ds.PGSimpleDataSource; +import org.testcontainers.containers.JdbcDatabaseContainer; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class YugabyteDBDataSourceProvider extends AbstractContainerDataSourceProvider { + + @Override + public String hibernateDialect() { + return PostgreSQLDialect.class.getName(); + } + + @Override + protected String defaultJdbcUrl() { + return "jdbc:postgresql://127.0.0.1:5433/high_performance_java_persistence"; + } + + protected DataSource newDataSource() { + JdbcDatabaseContainer container = database().getContainer(); + if(container != null) { + return new DriverDataSource( + container.getJdbcUrl(), + container.getDriverClassName(), + new Properties(), + container.getUsername(), + container.getPassword() + ); + } + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setURL(url()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return PGSimpleDataSource.class; + } + + @Override + public Class driverClassName() { + return Driver.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("user", username()); + properties.setProperty("password", password()); + return properties; + } + + @Override + public String username() { + return "yugabyte"; + } + + @Override + public String password() { + return "admin"; + } + + @Override + public Database database() { + return Database.YUGABYTEDB; + } + + @Override + public Queries queries() { + return PostgreSQLQueries.INSTANCE; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/aiven/AivenPostgreSQLDataSourceProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/aiven/AivenPostgreSQLDataSourceProvider.java new file mode 100644 index 000000000..5641561f4 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/aiven/AivenPostgreSQLDataSourceProvider.java @@ -0,0 +1,94 @@ +package com.vladmihalcea.hpjp.util.providers.aiven; + +import com.vladmihalcea.hpjp.util.providers.AbstractContainerDataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.vladmihalcea.hpjp.util.providers.queries.PostgreSQLQueries; +import com.vladmihalcea.hpjp.util.providers.queries.Queries; +import org.hibernate.dialect.PostgreSQLDialect; +import org.postgresql.Driver; +import org.postgresql.ds.PGSimpleDataSource; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class AivenPostgreSQLDataSourceProvider extends AbstractContainerDataSourceProvider { + + private interface Aiven { + String URL = "AIVEN_URL"; + String USER = "AIVEN_USER"; + String PASS = "AIVEN_PASS"; + } + + private Boolean reWriteBatchedInserts; + + public boolean getReWriteBatchedInserts() { + return reWriteBatchedInserts; + } + + public AivenPostgreSQLDataSourceProvider setReWriteBatchedInserts(boolean reWriteBatchedInserts) { + this.reWriteBatchedInserts = reWriteBatchedInserts; + return this; + } + + @Override + public String hibernateDialect() { + return PostgreSQLDialect.class.getName(); + } + + @Override + protected String defaultJdbcUrl() { + return String.format( + "jdbc:postgresql://%s?ssl=require", + System.getenv().get(Aiven.URL) + ); + } + + protected DataSource newDataSource() { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setURL(url()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + if (reWriteBatchedInserts != null) { + dataSource.setReWriteBatchedInserts(reWriteBatchedInserts); + } + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return PGSimpleDataSource.class; + } + + @Override + public Class driverClassName() { + return Driver.class; + } + + @Override + public Properties dataSourceProperties() { + return null; + } + + @Override + public String username() { + return System.getenv().get(Aiven.USER); + } + + @Override + public String password() { + return System.getenv().get(Aiven.PASS); + } + + @Override + public Database database() { + return Database.POSTGRESQL; + } + + @Override + public Queries queries() { + return PostgreSQLQueries.INSTANCE; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/AutoIncrementBatchEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/AutoIncrementBatchEntityProvider.java similarity index 79% rename from core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/AutoIncrementBatchEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/AutoIncrementBatchEntityProvider.java index 27f27abc5..9cd153b4c 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/AutoIncrementBatchEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/AutoIncrementBatchEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.util.providers.entity; +package com.vladmihalcea.hpjp.util.providers.entity; -import com.vladmihalcea.book.hpjp.util.EntityProvider; +import com.vladmihalcea.hpjp.util.EntityProvider; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea @@ -26,7 +26,7 @@ public static class Post { private String title; @Version - private int version; + private short version; private Post() { } diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/BankEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/BankEntityProvider.java similarity index 79% rename from core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/BankEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/BankEntityProvider.java index 013a391d1..2baea1326 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/BankEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/BankEntityProvider.java @@ -1,9 +1,9 @@ -package com.vladmihalcea.book.hpjp.util.providers.entity; +package com.vladmihalcea.hpjp.util.providers.entity; -import com.vladmihalcea.book.hpjp.util.EntityProvider; +import com.vladmihalcea.hpjp.util.EntityProvider; -import javax.persistence.Entity; -import javax.persistence.Id; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; /** * @author Vlad Mihalcea diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/BlogEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/BlogEntityProvider.java similarity index 90% rename from core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/BlogEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/BlogEntityProvider.java index 90c410612..31ce95053 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/BlogEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/BlogEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.util.providers.entity; +package com.vladmihalcea.hpjp.util.providers.entity; -import com.vladmihalcea.book.hpjp.util.EntityProvider; +import com.vladmihalcea.hpjp.util.EntityProvider; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -12,6 +12,8 @@ */ public class BlogEntityProvider implements EntityProvider { + public static final EntityProvider INSTANCE = new BlogEntityProvider(); + @Override public Class[] entities() { return new Class[]{ @@ -32,7 +34,7 @@ public static class Post { private String title; @Version - private int version; + private short version; public Post() {} @@ -79,7 +81,7 @@ public int getVersion() { return version; } - public void setVersion(int version) { + public void setVersion(short version) { this.version = version; } @@ -125,15 +127,15 @@ public static class PostDetails { private String createdBy; @Version - private int version; + private short version; public PostDetails() { createdOn = new Date(); } @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") @MapsId + @JoinColumn(name = "id") private Post post; public Long getId() { @@ -172,7 +174,7 @@ public int getVersion() { return version; } - public void setVersion(int version) { + public void setVersion(short version) { this.version = version; } } @@ -188,7 +190,7 @@ public static class PostComment { private Post post; @Version - private int version; + private short version; private String review; @@ -226,7 +228,7 @@ public int getVersion() { return version; } - public void setVersion(int version) { + public void setVersion(short version) { this.version = version; } } @@ -239,7 +241,7 @@ public static class Tag { private Long id; @Version - private int version; + private short version; private String name; @@ -255,7 +257,7 @@ public int getVersion() { return version; } - public void setVersion(int version) { + public void setVersion(short version) { this.version = version; } } diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/PostDetailsCommentsEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/PostDetailsCommentsEntityProvider.java similarity index 88% rename from core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/PostDetailsCommentsEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/PostDetailsCommentsEntityProvider.java index 83d3b9639..5271824d2 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/PostDetailsCommentsEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/PostDetailsCommentsEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.util.providers.entity; +package com.vladmihalcea.hpjp.util.providers.entity; -import com.vladmihalcea.book.hpjp.util.EntityProvider; +import com.vladmihalcea.hpjp.util.EntityProvider; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -31,7 +31,7 @@ public static class Post { private String title; @Version - private int version; + private short version; public Post() { } @@ -72,10 +72,6 @@ public int getVersion() { return version; } - public void setVersion(int version) { - this.version = version; - } - public List getComments() { return comments; } @@ -114,14 +110,13 @@ public static class PostDetails { private String createdBy; @Version - private int version; + private short version; public PostDetails() { createdOn = new Date(); } @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") @MapsId private Post post; @@ -160,10 +155,6 @@ public void setCreatedBy(String createdBy) { public int getVersion() { return version; } - - public void setVersion(int version) { - this.version = version; - } } @Entity(name = "PostComment") @@ -177,7 +168,7 @@ public static class PostComment { private Post post; @Version - private int version; + private short version; private String review; @@ -216,9 +207,5 @@ public void setReview(String review) { public int getVersion() { return version; } - - public void setVersion(int version) { - this.version = version; - } } } \ No newline at end of file diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/SequenceBatchEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/SequenceBatchEntityProvider.java similarity index 79% rename from core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/SequenceBatchEntityProvider.java rename to core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/SequenceBatchEntityProvider.java index a70a49d4f..bcf699a1b 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/providers/entity/SequenceBatchEntityProvider.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/SequenceBatchEntityProvider.java @@ -1,8 +1,8 @@ -package com.vladmihalcea.book.hpjp.util.providers.entity; +package com.vladmihalcea.hpjp.util.providers.entity; -import com.vladmihalcea.book.hpjp.util.EntityProvider; +import com.vladmihalcea.hpjp.util.EntityProvider; -import javax.persistence.*; +import jakarta.persistence.*; /** * @author Vlad Mihalcea @@ -21,13 +21,13 @@ public static class Post { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_seq") - @SequenceGenerator(name="post_seq", sequenceName="post_seq") + @SequenceGenerator(name="post_seq", sequenceName="post_seq", allocationSize = 1) private Long id; private String title; @Version - private int version; + private short version; private Post() { } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/TaskEntityProvider.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/TaskEntityProvider.java new file mode 100644 index 000000000..39a1038f6 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/entity/TaskEntityProvider.java @@ -0,0 +1,49 @@ +package com.vladmihalcea.hpjp.util.providers.entity; + +import com.vladmihalcea.hpjp.util.EntityProvider; + +import jakarta.persistence.*; +import java.util.Date; + +/** + * @author Vlad Mihalcea + */ +public class TaskEntityProvider implements EntityProvider { + + public enum StatusType { + TO_D0, + DONE, + FAILED + } + + @Override + public Class[] entities() { + return new Class[]{ + Task.class + }; + } + + @Entity(name = "Task") + @Table(name = "task", indexes = @Index(name = "IDX_task_status", columnList = "status")) + public static class Task { + + @Id + private Long id; + + @Enumerated(EnumType.STRING) + private StatusType status; + + @Embedded + private Change change; + } + + @Embeddable + public static class Change { + + @Column(name = "changed_on") + private Date changedOn; + + @Column(name = "created_by") + private String changedBy; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/HSQLDBServerQueries.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/HSQLDBServerQueries.java new file mode 100644 index 000000000..335b68112 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/HSQLDBServerQueries.java @@ -0,0 +1,14 @@ +package com.vladmihalcea.hpjp.util.providers.queries; + +/** + * @author Vlad Mihalcea + */ +public class HSQLDBServerQueries implements Queries { + + public static final Queries INSTANCE = new HSQLDBServerQueries(); + + @Override + public String transactionId() { + return "VALUES (TRANSACTION_ID())"; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/MySQLQueries.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/MySQLQueries.java new file mode 100644 index 000000000..6730c69ec --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/MySQLQueries.java @@ -0,0 +1,14 @@ +package com.vladmihalcea.hpjp.util.providers.queries; + +/** + * @author Vlad Mihalcea + */ +public class MySQLQueries implements Queries { + + public static final Queries INSTANCE = new MySQLQueries(); + + @Override + public String transactionId() { + return "SELECT tx.trx_id FROM information_schema.innodb_trx tx WHERE tx.trx_mysql_thread_id = connection_id()"; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/OracleQueries.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/OracleQueries.java new file mode 100644 index 000000000..e980444e8 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/OracleQueries.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hpjp.util.providers.queries; + +/** + * @author Vlad Mihalcea + */ +public class OracleQueries implements Queries { + + public static final Queries INSTANCE = new OracleQueries(); + + @Override + public String transactionId() { + return """ + SELECT RAWTOHEX(tx.xid) + FROM v$transaction tx + JOIN v$session s ON tx.addr=s.taddr + WHERE s.sid = sys_context('userenv','sid') + """ ; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/PostgreSQLQueries.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/PostgreSQLQueries.java new file mode 100644 index 000000000..9d8b43350 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/PostgreSQLQueries.java @@ -0,0 +1,58 @@ +package com.vladmihalcea.hpjp.util.providers.queries; + +import com.vladmihalcea.hpjp.jdbc.index.PostgreSQLIndexSelectivityTest; +import org.postgresql.PGStatement; +import org.postgresql.util.PGobject; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLQueries implements Queries { + + public static final Queries INSTANCE = new PostgreSQLQueries(); + + @Override + public String transactionId() { + return "SELECT CAST(pg_current_xact_id_if_assigned() AS text)"; + } + + public static void setPrepareThreshold(Statement statement, int threshold) throws SQLException { + if(statement instanceof PGStatement) { + PGStatement pgStatement = (PGStatement) statement; + pgStatement.setPrepareThreshold(threshold); + } else { + InvocationHandler handler = Proxy.getInvocationHandler(statement); + try { + handler.invoke(statement, PGStatement.class.getMethod("setPrepareThreshold", int.class), new Object[]{threshold}); + } catch (Throwable throwable) { + throw new IllegalArgumentException(throwable); + } + } + } + + public static boolean isUseServerPrepare(Statement statement) { + if(statement instanceof PGStatement) { + PGStatement pgStatement = (PGStatement) statement; + return pgStatement.isUseServerPrepare(); + } else { + InvocationHandler handler = Proxy.getInvocationHandler(statement); + try { + return (boolean) handler.invoke(statement, PGStatement.class.getMethod("isUseServerPrepare"), null); + } catch (Throwable e) { + throw new IllegalArgumentException(e); + } + } + } + + public static PGobject toEnum(Enum enumValue, String enumName) throws SQLException { + PGobject object = new PGobject(); + object.setType(enumName); + object.setValue(enumValue.name()); + return object; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/Queries.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/Queries.java new file mode 100644 index 000000000..3b6b0a04c --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/Queries.java @@ -0,0 +1,9 @@ +package com.vladmihalcea.hpjp.util.providers.queries; + +/** + * @author Vlad Mihalcea + */ +public interface Queries { + + String transactionId(); +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/SQLServerQueries.java b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/SQLServerQueries.java new file mode 100644 index 000000000..e11f1e2c1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/providers/queries/SQLServerQueries.java @@ -0,0 +1,14 @@ +package com.vladmihalcea.hpjp.util.providers.queries; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerQueries implements Queries { + + public static final Queries INSTANCE = new SQLServerQueries(); + + @Override + public String transactionId() { + return "SELECT CONVERT(VARCHAR, CURRENT_TRANSACTION_ID())"; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/resources/CpuTest.java b/core/src/test/java/com/vladmihalcea/hpjp/util/resources/CpuTest.java new file mode 100644 index 000000000..56e8b0c9b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/resources/CpuTest.java @@ -0,0 +1,118 @@ +package com.vladmihalcea.hpjp.util.resources; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Slf4jReporter; +import com.codahale.metrics.Timer; +import com.vladmihalcea.hpjp.spring.transaction.readonly.config.stats.SpringTransactionStatisticsReport; +import com.vladmihalcea.hpjp.util.AbstractTest; +import com.vladmihalcea.hpjp.util.CryptoUtils; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +/** + * @author Vlad Mihalcea + */ +public final class CpuTest { + + public static Logger LOGGER = LoggerFactory.getLogger(SpringTransactionStatisticsReport.class); + + private MetricRegistry metricRegistry = new MetricRegistry(); + + private Slf4jReporter logReporter = Slf4jReporter + .forRegistry(metricRegistry) + .outputTo(LOGGER) + .convertDurationsTo(TimeUnit.MICROSECONDS) + .build(); + + private final Timer encryptTimer = metricRegistry.timer("encryptTimer"); + private final Timer decryptTimer = metricRegistry.timer("decryptTimer"); + + private final ThreadLocalRandom random = ThreadLocalRandom.current(); + + private final long MAX_EXECUTION_TIME_NANOS = TimeUnit.MINUTES.toNanos(5); + + @Test + public void testOneCore() { + if(!AbstractTest.ENABLE_LONG_RUNNING_TESTS) { + return; + } + process(); + } + + @Test + public void testMultipleCores() { + if(!AbstractTest.ENABLE_LONG_RUNNING_TESTS) { + return; + } + + int threadCount = 7; + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + List futures = new ArrayList<>(threadCount); + for (int i = 0; i < threadCount; i++) { + futures.add(executorService.submit(this::process)); + } + for (int i = 0; i < threadCount; i++) { + try { + futures.get(i).get(); + } catch (InterruptedException e) { + Thread.interrupted(); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + try { + executorService.awaitTermination(MAX_EXECUTION_TIME_NANOS, TimeUnit.NANOSECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void process() { + long startNanos = System.nanoTime(); + int messageLength = 100; + long previousElapsedSecond = 0; + + while ((System.nanoTime() - startNanos) < MAX_EXECUTION_TIME_NANOS) { + BigInteger.valueOf(random.nextLong()) + .multiply(BigInteger.valueOf(random.nextLong())); + + StringBuilder stringBuilder = new StringBuilder(); + byte[] bytes = new byte[16]; + for (int i = 0; i < messageLength; i++) { + random.nextBytes(bytes); + stringBuilder.append(new String(bytes)); + } + String message = stringBuilder.toString(); + + long startEncryptNanos = System.nanoTime(); + String encryptedValue = CryptoUtils.encrypt(message); + encryptTimer.update((System.nanoTime() - startEncryptNanos), TimeUnit.NANOSECONDS); + + long startDecryptNanos = System.nanoTime(); + String decryptedValue = CryptoUtils.decrypt(encryptedValue, String.class); + decryptTimer.update((System.nanoTime() - startDecryptNanos), TimeUnit.NANOSECONDS); + + long elapsedSeconds = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startNanos); + + if(elapsedSeconds > 0 && previousElapsedSecond != elapsedSeconds && elapsedSeconds % 5 == 0) { + LOGGER.info("Elapsed {} seconds", elapsedSeconds); + previousElapsedSecond = elapsedSeconds; + logReporter.report(); + } + } + } + + protected void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/flyway/AbstractFlywayConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/flyway/AbstractFlywayConfiguration.java similarity index 75% rename from core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/flyway/AbstractFlywayConfiguration.java rename to core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/flyway/AbstractFlywayConfiguration.java index 41631566b..6fd623ce9 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/flyway/AbstractFlywayConfiguration.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/flyway/AbstractFlywayConfiguration.java @@ -1,5 +1,6 @@ -package com.vladmihalcea.book.hpjp.util.spring.config.flyway; +package com.vladmihalcea.hpjp.util.spring.config.flyway; +import com.vladmihalcea.hpjp.util.providers.Database; import org.flywaydb.core.Flyway; import org.hibernate.jpa.HibernatePersistenceProvider; import org.springframework.beans.factory.annotation.Value; @@ -14,7 +15,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.support.TransactionTemplate; -import javax.persistence.EntityManagerFactory; +import jakarta.persistence.EntityManagerFactory; import javax.sql.DataSource; import java.util.Properties; @@ -25,8 +26,11 @@ @EnableAspectJAutoProxy public abstract class AbstractFlywayConfiguration { - @Value("${hibernate.dialect}") - private String hibernateDialect; + private final Database databaseType; + + protected AbstractFlywayConfiguration(Database databaseType) { + this.databaseType = databaseType; + } @Bean public static PropertySourcesPlaceholderConfigurer properties() { @@ -34,15 +38,25 @@ public static PropertySourcesPlaceholderConfigurer properties() { } @Bean - public abstract DataSource actualDataSource(); + public abstract DataSource dataSource(); + @Bean + public Database database() { + return databaseType; + } - @Bean(initMethod = "migrate") + @Bean public Flyway flyway() { - Flyway flyway = new Flyway(); - flyway.setDataSource(actualDataSource()); - flyway.setBaselineOnMigrate(true); - flyway.setLocations(String.format("classpath:/flyway/db/%1$s/migration", databaseType())); + Flyway flyway = Flyway.configure() + .dataSource(dataSource()) + .baselineOnMigrate(true) + .locations( + String.format( + "classpath:/flyway/scripts/%1$s/migration", + databaseType.name().toLowerCase() + ) + ).load(); + flyway.migrate(); return flyway; } @@ -51,7 +65,7 @@ public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); localContainerEntityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); localContainerEntityManagerFactoryBean.setPersistenceProvider(new HibernatePersistenceProvider()); - localContainerEntityManagerFactoryBean.setDataSource(actualDataSource()); + localContainerEntityManagerFactoryBean.setDataSource(dataSource()); localContainerEntityManagerFactoryBean.setPackagesToScan(packagesToScan()); JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); @@ -74,7 +88,7 @@ public TransactionTemplate transactionTemplate(EntityManagerFactory entityManage protected Properties additionalProperties() { Properties properties = new Properties(); - properties.setProperty("hibernate.dialect", hibernateDialect); + /*properties.setProperty("hibernate.hbm2ddl.auto", "validate");*/ return properties; } @@ -87,6 +101,4 @@ protected String[] packagesToScan() { protected Class configurationClass() { return this.getClass(); } - - protected abstract String databaseType(); } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/flyway/AbstractHSQLDBFlywayConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/flyway/AbstractHSQLDBFlywayConfiguration.java new file mode 100644 index 000000000..933cd2568 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/flyway/AbstractHSQLDBFlywayConfiguration.java @@ -0,0 +1,51 @@ +package com.vladmihalcea.hpjp.util.spring.config.flyway; + +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +@Configuration +@PropertySource({"/META-INF/jdbc-hsqldb.properties"}) +public class AbstractHSQLDBFlywayConfiguration extends AbstractFlywayConfiguration { + + @Value("${jdbc.dataSourceClassName}") + private String dataSourceClassName; + + @Value("${jdbc.url}") + private String jdbcUrl; + + @Value("${jdbc.username}") + private String jdbcUser; + + @Value("${jdbc.password}") + private String jdbcPassword; + + public AbstractHSQLDBFlywayConfiguration() { + super(Database.HSQLDB); + } + + @Override + public DataSource dataSource() { + Properties driverProperties = new Properties(); + driverProperties.setProperty("url", jdbcUrl); + driverProperties.setProperty("user", jdbcUser); + driverProperties.setProperty("password", jdbcPassword); + + Properties properties = new Properties(); + properties.put("dataSourceClassName", dataSourceClassName); + properties.put("dataSourceProperties", driverProperties); + //properties.setProperty("minimumPoolSize", String.valueOf(1)); + properties.setProperty("maximumPoolSize", String.valueOf(3)); + properties.setProperty("connectionTimeout", String.valueOf(5000)); + return new HikariDataSource(new HikariConfig(properties)); + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/flyway/AbstractPostgreSQLFlywayConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/flyway/AbstractPostgreSQLFlywayConfiguration.java similarity index 88% rename from core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/flyway/AbstractPostgreSQLFlywayConfiguration.java rename to core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/flyway/AbstractPostgreSQLFlywayConfiguration.java index 51acd6838..8bebb80c8 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/spring/config/flyway/AbstractPostgreSQLFlywayConfiguration.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/flyway/AbstractPostgreSQLFlywayConfiguration.java @@ -1,5 +1,6 @@ -package com.vladmihalcea.book.hpjp.util.spring.config.flyway; +package com.vladmihalcea.hpjp.util.spring.config.flyway; +import com.vladmihalcea.hpjp.util.providers.Database; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.springframework.beans.factory.annotation.Value; @@ -34,8 +35,12 @@ public class AbstractPostgreSQLFlywayConfiguration extends AbstractFlywayConfigu @Value("${jdbc.port}") private String jdbcPort; + public AbstractPostgreSQLFlywayConfiguration() { + super(Database.POSTGRESQL); + } + @Override - public DataSource actualDataSource() { + public DataSource dataSource() { Properties driverProperties = new Properties(); driverProperties.setProperty("user", jdbcUser); driverProperties.setProperty("password", jdbcPassword); @@ -51,9 +56,4 @@ public DataSource actualDataSource() { properties.setProperty("connectionTimeout", String.valueOf(5000)); return new HikariDataSource(new HikariConfig(properties)); } - - @Override - protected String databaseType() { - return "postgresql"; - } } diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jpa/AbstractHsqldbJPAConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jpa/AbstractHsqldbJPAConfiguration.java new file mode 100644 index 000000000..1a04ac385 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jpa/AbstractHsqldbJPAConfiguration.java @@ -0,0 +1,51 @@ +package com.vladmihalcea.hpjp.util.spring.config.jpa; + +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +@Configuration +@PropertySource({"/META-INF/jdbc-hsqldb.properties"}) +public class AbstractHsqldbJPAConfiguration extends AbstractJPAConfiguration { + + @Value("${jdbc.dataSourceClassName}") + private String dataSourceClassName; + + @Value("${jdbc.url}") + private String jdbcUrl; + + @Value("${jdbc.username}") + private String jdbcUser; + + @Value("${jdbc.password}") + private String jdbcPassword; + + public AbstractHsqldbJPAConfiguration() { + super(Database.HSQLDB); + } + + @Override + public DataSource actualDataSource() { + Properties driverProperties = new Properties(); + driverProperties.setProperty("url", jdbcUrl); + driverProperties.setProperty("user", jdbcUser); + driverProperties.setProperty("password", jdbcPassword); + + Properties properties = new Properties(); + properties.put("dataSourceClassName", dataSourceClassName); + properties.put("dataSourceProperties", driverProperties); + //properties.setProperty("minimumPoolSize", String.valueOf(1)); + properties.setProperty("maximumPoolSize", String.valueOf(3)); + properties.setProperty("connectionTimeout", String.valueOf(5000)); + return new HikariDataSource(new HikariConfig(properties)); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jpa/AbstractJPAConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jpa/AbstractJPAConfiguration.java new file mode 100644 index 000000000..aa516bb8f --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jpa/AbstractJPAConfiguration.java @@ -0,0 +1,115 @@ +package com.vladmihalcea.hpjp.util.spring.config.jpa; + +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import com.vladmihalcea.hpjp.util.providers.Database; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaDialect; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +import jakarta.persistence.EntityManagerFactory; +import javax.sql.DataSource; +import java.util.Locale; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +@EnableTransactionManagement +@EnableAspectJAutoProxy +public abstract class AbstractJPAConfiguration { + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + private final Database databaseType; + + protected AbstractJPAConfiguration(Database databaseType) { + this.databaseType = databaseType; + } + + @Bean + public Database database() { + return databaseType; + } + + @Bean + public static PropertySourcesPlaceholderConfigurer properties() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Bean + public abstract DataSource actualDataSource(); + + private DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + return ProxyDataSourceBuilder + .create(actualDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setPersistenceUnitName(getClass().getSimpleName()); + entityManagerFactoryBean.setPersistenceProvider(new HibernatePersistenceProvider()); + entityManagerFactoryBean.setDataSource(dataSource()); + entityManagerFactoryBean.setPackagesToScan(packagesToScan()); + + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + HibernateJpaDialect jpaDialect = vendorAdapter.getJpaDialect(); + jpaDialect.setPrepareConnection(false); + entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + entityManagerFactoryBean.setJpaProperties(additionalProperties()); + return entityManagerFactoryBean; + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate(EntityManagerFactory entityManagerFactory) { + return new TransactionTemplate(transactionManager(entityManagerFactory)); + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + return properties; + } + + protected String[] packagesToScan() { + return new String[]{ + configurationClass().getPackage().getName() + }; + } + + protected Class configurationClass() { + return this.getClass(); + } + + @Bean + protected String databaseType() { + return databaseType.name().toLowerCase(Locale.ROOT); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jpa/HikariCPJPAConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jpa/HikariCPJPAConfiguration.java new file mode 100644 index 000000000..9812eadfe --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jpa/HikariCPJPAConfiguration.java @@ -0,0 +1,36 @@ +package com.vladmihalcea.hpjp.util.spring.config.jpa; + +import com.vladmihalcea.hpjp.util.providers.DataSourceProvider; +import com.vladmihalcea.hpjp.util.providers.Database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.sql.DataSource; + +/** + * @author Vlad Mihalcea + */ +@Configuration +public class HikariCPJPAConfiguration extends AbstractJPAConfiguration { + + protected HikariCPJPAConfiguration() { + super(Database.MYSQL); + } + + @Bean + public DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + @Bean(destroyMethod = "close") + public DataSource actualDataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setMaximumPoolSize(64); + hikariConfig.setAutoCommit(false); + hikariConfig.setDataSource(dataSourceProvider().dataSource()); + return new HikariDataSource(hikariConfig); + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jpa/PostgreSQLJPAConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jpa/PostgreSQLJPAConfiguration.java new file mode 100644 index 000000000..7ceae77de --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jpa/PostgreSQLJPAConfiguration.java @@ -0,0 +1,50 @@ +package com.vladmihalcea.hpjp.util.spring.config.jpa; + +import com.vladmihalcea.hpjp.util.providers.Database; +import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.sql.DataSource; + +/** + * @author Vlad Mihalcea + */ +@Configuration +@PropertySource({"/META-INF/jdbc-postgresql.properties"}) +public class PostgreSQLJPAConfiguration extends AbstractJPAConfiguration { + + @Value("${jdbc.dataSourceClassName}") + private String dataSourceClassName; + + @Value("${jdbc.username}") + private String jdbcUser; + + @Value("${jdbc.password}") + private String jdbcPassword; + + @Value("${jdbc.database}") + private String jdbcDatabase; + + @Value("${jdbc.host}") + private String jdbcHost; + + @Value("${jdbc.port}") + private String jdbcPort; + + public PostgreSQLJPAConfiguration() { + super(Database.POSTGRESQL); + } + + @Override + public DataSource actualDataSource() { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setDatabaseName(jdbcDatabase); + dataSource.setUser(jdbcUser); + dataSource.setPassword(jdbcPassword); + dataSource.setServerName(jdbcHost); + dataSource.setPortNumber(Integer.valueOf(jdbcPort)); + return dataSource; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jta/AbstractJTATransactionManagerConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jta/AbstractJTATransactionManagerConfiguration.java new file mode 100644 index 000000000..d2394df68 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jta/AbstractJTATransactionManagerConfiguration.java @@ -0,0 +1,104 @@ +package com.vladmihalcea.hpjp.util.spring.config.jta; + +import com.atomikos.icatch.jta.UserTransactionManager; +import com.vladmihalcea.hpjp.util.DataSourceProxyType; +import com.vladmihalcea.hpjp.util.logging.InlineQueryLogEntryCreator; +import jakarta.transaction.SystemException; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.engine.transaction.jta.platform.internal.AtomikosJtaPlatform; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.jta.JtaTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +@EnableTransactionManagement +@EnableAspectJAutoProxy +public abstract class AbstractJTATransactionManagerConfiguration { + + public static final String DATA_SOURCE_PROXY_NAME = DataSourceProxyType.DATA_SOURCE_PROXY.name(); + + @Bean + public static PropertySourcesPlaceholderConfigurer properties() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Bean + public abstract DataSource actualDataSource(); + + @DependsOn("actualDataSource") + public DataSource dataSource() { + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + return ProxyDataSourceBuilder + .create(actualDataSource()) + .name(DATA_SOURCE_PROXY_NAME) + .listener(loggingListener) + .build(); + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + localContainerEntityManagerFactoryBean.setJtaDataSource(dataSource()); + localContainerEntityManagerFactoryBean.setPackagesToScan(packagesToScan()); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + localContainerEntityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); + localContainerEntityManagerFactoryBean.setJpaProperties(additionalProperties()); + return localContainerEntityManagerFactoryBean; + } + + protected String[] packagesToScan() { + return new String[]{ + configurationClass().getPackage().getName() + }; + } + + protected abstract Class configurationClass(); + + @Bean(initMethod = "init", destroyMethod = "close") + public UserTransactionManager userTransactionManager() throws SystemException { + UserTransactionManager userTransactionManager = new UserTransactionManager(); + userTransactionManager.setTransactionTimeout(300); + userTransactionManager.setForceShutdown(true); + return userTransactionManager; + } + + @Bean + public JtaTransactionManager transactionManager() throws SystemException { + JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(); + jtaTransactionManager.setTransactionManager(userTransactionManager()); + jtaTransactionManager.setUserTransaction(userTransactionManager()); + jtaTransactionManager.setAllowCustomIsolationLevels(true); + return jtaTransactionManager; + } + + @Bean + public TransactionTemplate transactionTemplate() throws SystemException { + return new TransactionTemplate(transactionManager()); + } + + protected Properties additionalProperties() { + Properties properties = new Properties(); + properties.put( + AvailableSettings.JTA_PLATFORM, + AtomikosJtaPlatform.class + ); + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + return properties; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jta/HSQLDBJtaTransactionManagerConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jta/HSQLDBJtaTransactionManagerConfiguration.java new file mode 100644 index 000000000..56ef79476 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jta/HSQLDBJtaTransactionManagerConfiguration.java @@ -0,0 +1,38 @@ +package com.vladmihalcea.hpjp.util.spring.config.jta; + +import org.hsqldb.jdbc.pool.JDBCXADataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import javax.sql.DataSource; + +/** + * @author Vlad Mihalcea + */ +@PropertySource({"/META-INF/jta-hsqldb.properties"}) +@Configuration +public abstract class HSQLDBJtaTransactionManagerConfiguration extends AbstractJTATransactionManagerConfiguration { + + @Value("${jdbc.dataSourceClassName}") + private String dataSourceClassName; + + @Value("${jdbc.username}") + private String jdbcUser; + + @Value("${jdbc.password}") + private String jdbcPassword; + + @Value("${jdbc.url}") + private String jdbcUrl; + + public DataSource actualDataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName(JDBCXADataSource.class.getName()); + dataSource.setUrl(jdbcUrl); + dataSource.setUsername(jdbcUser); + dataSource.setPassword(jdbcPassword); + return dataSource; + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jta/PostgreSQLJTATransactionManagerConfiguration.java b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jta/PostgreSQLJTATransactionManagerConfiguration.java new file mode 100644 index 000000000..02f653b05 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/spring/config/jta/PostgreSQLJTATransactionManagerConfiguration.java @@ -0,0 +1,50 @@ +package com.vladmihalcea.hpjp.util.spring.config.jta; + +import com.atomikos.jdbc.AtomikosDataSourceBean; +import org.postgresql.xa.PGXADataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +@PropertySource({"/META-INF/jta-postgresql.properties"}) +@Configuration +public abstract class PostgreSQLJTATransactionManagerConfiguration extends AbstractJTATransactionManagerConfiguration { + + @Value("${jdbc.dataSourceClassName}") + protected String dataSourceClassName; + + @Value("${jdbc.username}") + protected String jdbcUser; + + @Value("${jdbc.password}") + protected String jdbcPassword; + + @Value("${jdbc.database}") + protected String jdbcDatabase; + + @Value("${jdbc.host}") + protected String jdbcHost; + + @Value("${jdbc.port}") + protected String jdbcPort; + + @Bean(initMethod = "init", destroyMethod = "close") + public AtomikosDataSourceBean actualDataSource() { + AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean(); + dataSource.setUniqueResourceName("PostgreSQL"); + PGXADataSource xaDataSource = new PGXADataSource(); + xaDataSource.setUser(jdbcUser); + xaDataSource.setPassword(jdbcPassword); + dataSource.setXaDataSource(xaDataSource); + dataSource.setPoolSize(5); + return dataSource; + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/ConnectionCallable.java b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/ConnectionCallable.java similarity index 80% rename from core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/ConnectionCallable.java rename to core/src/test/java/com/vladmihalcea/hpjp/util/transaction/ConnectionCallable.java index 9d310b0aa..501b801af 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/ConnectionCallable.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/ConnectionCallable.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.util.transaction; +package com.vladmihalcea.hpjp.util.transaction; import java.sql.Connection; import java.sql.SQLException; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/ConnectionVoidCallable.java b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/ConnectionVoidCallable.java new file mode 100644 index 000000000..77a45baf1 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/ConnectionVoidCallable.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.hpjp.util.transaction; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface ConnectionVoidCallable { + void execute(Connection connection) throws SQLException; +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/HibernateStatelessTransactionConsumer.java b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/HibernateStatelessTransactionConsumer.java new file mode 100644 index 000000000..f019ecde5 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/HibernateStatelessTransactionConsumer.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hpjp.util.transaction; + +import org.hibernate.StatelessSession; + +import java.util.function.Consumer; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface HibernateStatelessTransactionConsumer extends Consumer { + default void beforeTransactionCompletion() { + + } + + default void afterTransactionCompletion() { + + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/HibernateStatelessTransactionFunction.java b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/HibernateStatelessTransactionFunction.java new file mode 100644 index 000000000..5d88aff2b --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/HibernateStatelessTransactionFunction.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.hpjp.util.transaction; + +import org.hibernate.StatelessSession; + +import java.util.function.Function; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface HibernateStatelessTransactionFunction extends Function { + default void beforeTransactionCompletion() { + + } + + default void afterTransactionCompletion() { + + } +} diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/HibernateTransactionConsumer.java b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/HibernateTransactionConsumer.java similarity index 84% rename from core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/HibernateTransactionConsumer.java rename to core/src/test/java/com/vladmihalcea/hpjp/util/transaction/HibernateTransactionConsumer.java index d9a510517..ae44ebed6 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/HibernateTransactionConsumer.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/HibernateTransactionConsumer.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.util.transaction; +package com.vladmihalcea.hpjp.util.transaction; import java.util.function.Consumer; diff --git a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/HibernateTransactionFunction.java b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/HibernateTransactionFunction.java similarity index 85% rename from core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/HibernateTransactionFunction.java rename to core/src/test/java/com/vladmihalcea/hpjp/util/transaction/HibernateTransactionFunction.java index cb9b06d61..7d03d240c 100644 --- a/core/src/test/java/com/vladmihalcea/book/hpjp/util/transaction/HibernateTransactionFunction.java +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/HibernateTransactionFunction.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.util.transaction; +package com.vladmihalcea.hpjp.util.transaction; import java.util.function.Function; diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/JPATransactionFunction.java b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/JPATransactionFunction.java new file mode 100644 index 000000000..ecfb4d8dd --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/JPATransactionFunction.java @@ -0,0 +1,18 @@ +package com.vladmihalcea.hpjp.util.transaction; + +import java.util.function.Function; +import jakarta.persistence.EntityManager; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface JPATransactionFunction extends Function { + default void beforeTransactionCompletion() { + + } + + default void afterTransactionCompletion() { + + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/JPATransactionVoidFunction.java b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/JPATransactionVoidFunction.java new file mode 100644 index 000000000..fce51c742 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/JPATransactionVoidFunction.java @@ -0,0 +1,18 @@ +package com.vladmihalcea.hpjp.util.transaction; + +import java.util.function.Consumer; +import jakarta.persistence.EntityManager; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface JPATransactionVoidFunction extends Consumer { + default void beforeTransactionCompletion() { + + } + + default void afterTransactionCompletion() { + + } +} diff --git a/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/VoidCallable.java b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/VoidCallable.java new file mode 100644 index 000000000..c96f7b2d7 --- /dev/null +++ b/core/src/test/java/com/vladmihalcea/hpjp/util/transaction/VoidCallable.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.hpjp.util.transaction; + +import java.util.concurrent.Callable; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface VoidCallable extends Callable { + + void execute(); + + default Void call() throws Exception { + execute(); + return null; + } +} diff --git a/core/src/test/markus.txt b/core/src/test/markus.txt deleted file mode 100644 index 7b82c8581..000000000 --- a/core/src/test/markus.txt +++ /dev/null @@ -1,31 +0,0 @@ -SELECT id, parent_id, root_id, review, created_on, score, rank, total_score -FROM ( - SELECT - id, parent_id, root_id, review, created_on, score, total_score, - dense_rank() OVER (ORDER BY total_score DESC) rank - FROM ( - SELECT - id, parent_id, root_id, review, created_on, score, - SUM(score) OVER (PARTITION BY root_id) total_score - FROM ( - WITH RECURSIVE post_comment_score(id, root_id, post_id, - parent_id, review, created_on, score) AS ( - SELECT - id, id, post_id, parent_id, review, created_on, score - FROM post_comment - WHERE post_id = 1 AND parent_id IS NULL - UNION ALL - SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, - pc.review, pc.created_on, pc.score - FROM post_comment pc - INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id --- WHERE pc.parent_id = pcs.id - WHERE pc.post_id = pcs.post_id - ) - SELECT id, parent_id, root_id, review, created_on, score - FROM post_comment_score - ) score_by_comment - ) score_total -) total_score_group -WHERE rank <= 0 -ORDER BY total_score DESC, id ASC ; \ No newline at end of file diff --git a/core/src/test/resources/META-INF/jdbc-mysql.properties b/core/src/test/resources/META-INF/jdbc-mysql.properties index 4da16dfbd..1ad795689 100644 --- a/core/src/test/resources/META-INF/jdbc-mysql.properties +++ b/core/src/test/resources/META-INF/jdbc-mysql.properties @@ -1,5 +1,4 @@ -hibernate.dialect=org.hibernate.dialect.MySQL57Dialect -jdbc.dataSourceClassName=com.mysql.jdbc.jdbc2.optional.MysqlDataSource +jdbc.dataSourceClassName=com.mysql.cj.jdbc.MysqlDataSource jdbc.url=jdbc:mysql://localhost/high_performance_java_persistence jdbc.database=high_performance_java_persistence jdbc.username=mysql diff --git a/core/src/test/resources/META-INF/jdbc-postgresql-replication.properties b/core/src/test/resources/META-INF/jdbc-postgresql-replication.properties new file mode 100644 index 000000000..728d93c9a --- /dev/null +++ b/core/src/test/resources/META-INF/jdbc-postgresql-replication.properties @@ -0,0 +1,4 @@ +jdbc.url.primary=jdbc:postgresql://localhost:5432/high_performance_java_persistence +jdbc.url.replica=jdbc:postgresql://localhost:5432/high_performance_java_persistence_replica +jdbc.username=postgres +jdbc.password=admin \ No newline at end of file diff --git a/core/src/test/resources/META-INF/jdbc-postgresql.properties b/core/src/test/resources/META-INF/jdbc-postgresql.properties index f8be0fadc..f2af3b722 100644 --- a/core/src/test/resources/META-INF/jdbc-postgresql.properties +++ b/core/src/test/resources/META-INF/jdbc-postgresql.properties @@ -1,5 +1,5 @@ -hibernate.dialect=org.hibernate.dialect.PostgreSQL94Dialect jdbc.dataSourceClassName=org.postgresql.ds.PGSimpleDataSource +jdbc.url=jdbc:postgresql://localhost:5432/high_performance_java_persistence jdbc.host=localhost jdbc.port=5432 jdbc.database=high_performance_java_persistence diff --git a/core/src/test/resources/META-INF/jta-hsqldb.properties b/core/src/test/resources/META-INF/jta-hsqldb.properties index 03cc26d23..6a573bc5b 100644 --- a/core/src/test/resources/META-INF/jta-hsqldb.properties +++ b/core/src/test/resources/META-INF/jta-hsqldb.properties @@ -3,5 +3,4 @@ jdbc.driverClassName=org.hsqldb.jdbc.JDBCDriver jdbc.dataSourceClassName=org.hsqldb.jdbc.pool.JDBCXADataSource jdbc.url=jdbc:hsqldb:mem:test jdbc.username=sa -jdbc.password= -btm.config.journal=null \ No newline at end of file +jdbc.password= \ No newline at end of file diff --git a/core/src/test/resources/META-INF/jta-postgresql.properties b/core/src/test/resources/META-INF/jta-postgresql.properties index 3646a6b8e..cc6dbabbb 100644 --- a/core/src/test/resources/META-INF/jta-postgresql.properties +++ b/core/src/test/resources/META-INF/jta-postgresql.properties @@ -1,8 +1,7 @@ -hibernate.dialect=org.hibernate.dialect.PostgreSQL94Dialect +hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect jdbc.dataSourceClassName=org.postgresql.xa.PGXADataSource jdbc.host=localhost jdbc.port=5432 jdbc.database=high_performance_java_persistence jdbc.username=postgres -jdbc.password=admin -btm.config.journal=null \ No newline at end of file +jdbc.password=admin \ No newline at end of file diff --git a/core/src/test/resources/META-INF/persistence.xml b/core/src/test/resources/META-INF/persistence.xml deleted file mode 100644 index eb6461610..000000000 --- a/core/src/test/resources/META-INF/persistence.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - org.hibernate.jpa.HibernatePersistenceProvider - - false - - - - - - - - - - - - - diff --git a/core/src/test/resources/data/oracle_quotes.sql b/core/src/test/resources/data/oracle_quotes.sql new file mode 100644 index 000000000..a1aaa39ca --- /dev/null +++ b/core/src/test/resources/data/oracle_quotes.sql @@ -0,0 +1,1258 @@ +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-29', 4769.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-28', 4783.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-27', 4781.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-26', 4774.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-22', 4754.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-21', 4746.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-20', 4698.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-19', 4768.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-18', 4740.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-15', 4719.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-14', 4719.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-13', 4707.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-12', 4643.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-11', 4622.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-08', 4604.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-07', 4585.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-06', 4549.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-05', 4567.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-04', 4569.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-12-01', 4594.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-30', 4567.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-29', 4550.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-28', 4554.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-27', 4550.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-24', 4559.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-22', 4556.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-21', 4538.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-20', 4547.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-17', 4514.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-16', 4508.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-15', 4502.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-14', 4495.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-13', 4411.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-10', 4415.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-09', 4347.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-08', 4382.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-07', 4378.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-06', 4365.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-03', 4358.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-02', 4317.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-11-01', 4237.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-31', 4193.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-30', 4166.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-27', 4117.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-26', 4137.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-25', 4186.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-24', 4247.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-23', 4217.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-20', 4224.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-19', 4278); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-18', 4314.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-17', 4373.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-16', 4373.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-13', 4327.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-12', 4349.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-11', 4376.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-10', 4358.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-09', 4335.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-06', 4308.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-05', 4258.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-04', 4263.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-03', 4229.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-10-02', 4288.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-29', 4288.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-28', 4299.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-27', 4274.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-26', 4273.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-25', 4337.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-22', 4320.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-21', 4330); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-20', 4402.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-19', 4443.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-18', 4453.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-15', 4450.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-14', 4505.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-13', 4467.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-12', 4461.9); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-11', 4487.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-08', 4457.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-07', 4451.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-06', 4465.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-05', 4496.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-09-01', 4515.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-31', 4507.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-30', 4514.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-29', 4497.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-28', 4433.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-25', 4405.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-24', 4376.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-23', 4436.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-22', 4387.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-21', 4399.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-18', 4369.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-17', 4370.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-16', 4404.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-15', 4437.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-14', 4489.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-11', 4464.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-10', 4468.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-09', 4467.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-08', 4499.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-07', 4518.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-04', 4478.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-03', 4501.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-02', 4513.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-08-01', 4576.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-31', 4588.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-28', 4582.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-27', 4537.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-26', 4566.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-25', 4567.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-24', 4554.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-21', 4536.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-20', 4534.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-19', 4565.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-18', 4554.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-17', 4522.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-14', 4505.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-13', 4510.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-12', 4472.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-11', 4439.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-10', 4409.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-07', 4398.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-06', 4411.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-05', 4446.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-07-03', 4455.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-30', 4450.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-29', 4396.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-28', 4376.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-27', 4378.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-26', 4328.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-23', 4348.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-22', 4381.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-21', 4365.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-20', 4388.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-16', 4409.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-15', 4425.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-14', 4372.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-13', 4369.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-12', 4338.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-09', 4298.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-08', 4293.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-07', 4267.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-06', 4283.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-05', 4273.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-02', 4282.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-06-01', 4221.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-31', 4179.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-30', 4205.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-26', 4205.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-25', 4151.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-24', 4115.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-23', 4145.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-22', 4192.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-19', 4191.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-18', 4198.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-17', 4158.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-16', 4109.9); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-15', 4136.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-12', 4124.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-11', 4130.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-10', 4137.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-09', 4119.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-08', 4138.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-05', 4136.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-04', 4061.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-03', 4090.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-02', 4119.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-05-01', 4167.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-28', 4169.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-27', 4135.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-26', 4055.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-25', 4071.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-24', 4137.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-21', 4133.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-20', 4129.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-19', 4154.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-18', 4154.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-17', 4151.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-14', 4137.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-13', 4146.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-12', 4091.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-11', 4108.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-10', 4109.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-06', 4105.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-05', 4090.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-04', 4100.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-04-03', 4124.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-31', 4109.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-30', 4050.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-29', 4027.81); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-28', 3971.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-27', 3977.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-24', 3970.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-23', 3948.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-22', 3936.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-21', 4002.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-20', 3951.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-17', 3916.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-16', 3960.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-15', 3891.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-14', 3919.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-13', 3855.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-10', 3861.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-09', 3918.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-08', 3992.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-07', 3986.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-06', 4048.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-03', 4045.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-02', 3981.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-03-01', 3951.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-28', 3970.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-27', 3982.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-24', 3970.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-23', 4012.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-22', 3991.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-21', 3997.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-17', 4079.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-16', 4090.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-15', 4147.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-14', 4136.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-13', 4137.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-10', 4090.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-09', 4081.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-08', 4117.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-07', 4164); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-06', 4111.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-03', 4136.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-02', 4179.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-02-01', 4119.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-31', 4076.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-30', 4017.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-27', 4070.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-26', 4060.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-25', 4016.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-24', 4016.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-23', 4019.81); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-20', 3972.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-19', 3898.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-18', 3928.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-17', 3990.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-13', 3999.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-12', 3983.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-11', 3969.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-10', 3919.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-09', 3892.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-06', 3895.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-05', 3808.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-04', 3852.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2023-01-03', 3824.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-30', 3839.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-29', 3849.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-28', 3783.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-27', 3829.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-23', 3844.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-22', 3822.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-21', 3878.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-20', 3821.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-19', 3817.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-16', 3852.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-15', 3895.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-14', 3995.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-13', 4019.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-12', 3990.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-09', 3934.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-08', 3963.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-07', 3933.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-06', 3941.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-05', 3998.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-02', 4071.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-12-01', 4076.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-30', 4080.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-29', 3957.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-28', 3963.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-25', 4026.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-23', 4027.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-22', 4003.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-21', 3949.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-18', 3965.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-17', 3946.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-16', 3958.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-15', 3991.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-14', 3957.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-11', 3992.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-10', 3956.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-09', 3748.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-08', 3828.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-07', 3806.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-04', 3770.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-03', 3719.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-02', 3759.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-11-01', 3856.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-31', 3871.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-28', 3901.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-27', 3807.3); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-26', 3830.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-25', 3859.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-24', 3797.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-21', 3752.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-20', 3665.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-19', 3695.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-18', 3719.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-17', 3677.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-14', 3583.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-13', 3669.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-12', 3577.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-11', 3588.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-10', 3612.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-07', 3639.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-06', 3744.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-05', 3783.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-04', 3790.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-10-03', 3678.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-30', 3585.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-29', 3640.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-28', 3719.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-27', 3647.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-26', 3655.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-23', 3693.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-22', 3757.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-21', 3789.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-20', 3855.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-19', 3899.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-16', 3873.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-15', 3901.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-14', 3946.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-13', 3932.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-12', 4110.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-09', 4067.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-08', 4006.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-07', 3979.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-06', 3908.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-02', 3924.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-09-01', 3966.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-31', 3955); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-30', 3986.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-29', 4030.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-26', 4057.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-25', 4199.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-24', 4140.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-23', 4128.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-22', 4137.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-19', 4228.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-18', 4283.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-17', 4274.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-16', 4305.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-15', 4297.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-12', 4280.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-11', 4207.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-10', 4210.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-09', 4122.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-08', 4140.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-05', 4145.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-04', 4151.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-03', 4155.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-02', 4091.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-08-01', 4118.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-29', 4130.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-28', 4072.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-27', 4023.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-26', 3921.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-25', 3966.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-22', 3961.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-21', 3998.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-20', 3959.9); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-19', 3936.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-18', 3830.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-15', 3863.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-14', 3790.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-13', 3801.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-12', 3818.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-11', 3854.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-08', 3899.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-07', 3902.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-06', 3845.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-05', 3831.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-07-01', 3825.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-30', 3785.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-29', 3818.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-28', 3821.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-27', 3900.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-24', 3911.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-23', 3795.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-22', 3759.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-21', 3764.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-17', 3674.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-16', 3666.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-15', 3789.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-14', 3735.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-13', 3749.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-10', 3900.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-09', 4017.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-08', 4115.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-07', 4160.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-06', 4121.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-03', 4108.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-02', 4176.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-06-01', 4101.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-31', 4132.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-27', 4158.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-26', 4057.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-25', 3978.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-24', 3941.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-23', 3973.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-20', 3901.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-19', 3900.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-18', 3923.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-17', 4088.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-16', 4008.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-13', 4023.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-12', 3930.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-11', 3935.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-10', 4001.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-09', 3991.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-06', 4123.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-05', 4146.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-04', 4300.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-03', 4175.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-05-02', 4155.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-29', 4131.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-28', 4287.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-27', 4183.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-26', 4175.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-25', 4296.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-22', 4271.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-21', 4393.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-20', 4459.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-19', 4462.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-18', 4391.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-14', 4392.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-13', 4446.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-12', 4397.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-11', 4412.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-08', 4488.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-07', 4500.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-06', 4481.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-05', 4525.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-04', 4582.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-04-01', 4545.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-31', 4530.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-30', 4602.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-29', 4631.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-28', 4575.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-25', 4543.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-24', 4520.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-23', 4456.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-22', 4511.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-21', 4461.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-18', 4463.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-17', 4411.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-16', 4357.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-15', 4262.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-14', 4173.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-11', 4204.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-10', 4259.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-09', 4277.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-08', 4170.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-07', 4201.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-04', 4328.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-03', 4363.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-02', 4386.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-03-01', 4306.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-28', 4373.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-25', 4384.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-24', 4288.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-23', 4225.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-22', 4304.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-18', 4348.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-17', 4380.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-16', 4475.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-15', 4471.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-14', 4401.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-11', 4418.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-10', 4504.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-09', 4587.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-08', 4521.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-07', 4483.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-04', 4500.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-03', 4477.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-02', 4589.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-02-01', 4546.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-31', 4515.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-28', 4431.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-27', 4326.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-26', 4349.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-25', 4356.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-24', 4410.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-21', 4397.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-20', 4482.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-19', 4532.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-18', 4577.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-14', 4662.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-13', 4659.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-12', 4726.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-11', 4713.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-10', 4670.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-07', 4677.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-06', 4696.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-05', 4700.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-04', 4793.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2022-01-03', 4796.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-31', 4766.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-30', 4778.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-29', 4793.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-28', 4786.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-27', 4791.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-23', 4725.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-22', 4696.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-21', 4649.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-20', 4568.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-17', 4620.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-16', 4668.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-15', 4709.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-14', 4634.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-13', 4668.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-10', 4712.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-09', 4667.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-08', 4701.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-07', 4686.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-06', 4591.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-03', 4538.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-02', 4577.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-12-01', 4513.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-30', 4567); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-29', 4655.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-26', 4594.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-24', 4701.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-23', 4690.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-22', 4682.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-19', 4697.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-18', 4704.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-17', 4688.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-16', 4700.9); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-15', 4682.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-12', 4682.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-11', 4649.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-10', 4646.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-09', 4685.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-08', 4701.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-05', 4697.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-04', 4680.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-03', 4660.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-02', 4630.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-11-01', 4613.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-29', 4605.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-28', 4596.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-27', 4551.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-26', 4574.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-25', 4566.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-22', 4544.9); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-21', 4549.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-20', 4536.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-19', 4519.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-18', 4486.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-15', 4471.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-14', 4438.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-13', 4363.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-12', 4350.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-11', 4361.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-08', 4391.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-07', 4399.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-06', 4363.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-05', 4345.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-04', 4300.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-10-01', 4357.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-30', 4307.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-29', 4359.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-28', 4352.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-27', 4443.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-24', 4455.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-23', 4448.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-22', 4395.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-21', 4354.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-20', 4357.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-17', 4432.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-16', 4473.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-15', 4480.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-14', 4443.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-13', 4468.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-10', 4458.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-09', 4493.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-08', 4514.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-07', 4520.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-03', 4535.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-02', 4536.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-09-01', 4524.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-31', 4522.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-30', 4528.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-27', 4509.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-26', 4470); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-25', 4496.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-24', 4486.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-23', 4479.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-20', 4441.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-19', 4405.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-18', 4400.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-17', 4448.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-16', 4479.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-13', 4468); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-12', 4460.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-11', 4447.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-10', 4436.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-09', 4432.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-06', 4436.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-05', 4429.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-04', 4402.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-03', 4423.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-08-02', 4387.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-30', 4395.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-29', 4419.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-28', 4400.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-27', 4401.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-26', 4422.3); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-23', 4411.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-22', 4367.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-21', 4358.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-20', 4323.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-19', 4258.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-16', 4327.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-15', 4360.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-14', 4374.3); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-13', 4369.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-12', 4384.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-09', 4369.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-08', 4320.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-07', 4358.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-06', 4343.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-02', 4352.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-07-01', 4319.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-30', 4297.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-29', 4291.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-28', 4290.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-25', 4280.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-24', 4266.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-23', 4241.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-22', 4246.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-21', 4224.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-18', 4166.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-17', 4221.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-16', 4223.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-15', 4246.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-14', 4255.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-11', 4247.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-10', 4239.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-09', 4219.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-08', 4227.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-07', 4226.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-04', 4229.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-03', 4192.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-02', 4208.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-06-01', 4202.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-28', 4204.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-27', 4200.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-26', 4195.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-25', 4188.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-24', 4197.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-21', 4155.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-20', 4159.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-19', 4115.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-18', 4127.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-17', 4163.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-14', 4173.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-13', 4112.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-12', 4063.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-11', 4152.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-10', 4188.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-07', 4232.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-06', 4201.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-05', 4167.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-04', 4164.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-05-03', 4192.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-30', 4181.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-29', 4211.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-28', 4183.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-27', 4186.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-26', 4187.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-23', 4180.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-22', 4134.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-21', 4173.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-20', 4134.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-19', 4163.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-16', 4185.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-15', 4170.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-14', 4124.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-13', 4141.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-12', 4127.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-09', 4128.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-08', 4097.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-07', 4079.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-06', 4073.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-05', 4077.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-04-01', 4019.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-31', 3972.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-30', 3958.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-29', 3971.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-26', 3974.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-25', 3909.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-24', 3889.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-23', 3910.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-22', 3940.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-19', 3913.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-18', 3915.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-17', 3974.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-16', 3962.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-15', 3968.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-12', 3943.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-11', 3939.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-10', 3898.81); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-09', 3875.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-08', 3821.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-05', 3841.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-04', 3768.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-03', 3819.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-02', 3870.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-03-01', 3901.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-26', 3811.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-25', 3829.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-24', 3925.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-23', 3881.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-22', 3876.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-19', 3906.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-18', 3913.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-17', 3931.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-16', 3932.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-12', 3934.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-11', 3916.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-10', 3909.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-09', 3911.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-08', 3915.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-05', 3886.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-04', 3871.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-03', 3830.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-02', 3826.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-02-01', 3773.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-29', 3714.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-28', 3787.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-27', 3750.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-26', 3849.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-25', 3855.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-22', 3841.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-21', 3853.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-20', 3851.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-19', 3798.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-15', 3768.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-14', 3795.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-13', 3809.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-12', 3801.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-11', 3799.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-08', 3824.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-07', 3803.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-06', 3748.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-05', 3726.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2021-01-04', 3700.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-31', 3756.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-30', 3732.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-29', 3727.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-28', 3735.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-24', 3703.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-23', 3690.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-22', 3687.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-21', 3694.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-18', 3709.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-17', 3722.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-16', 3701.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-15', 3694.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-14', 3647.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-11', 3663.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-10', 3668.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-09', 3672.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-08', 3702.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-07', 3691.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-04', 3699.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-03', 3666.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-02', 3669.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-12-01', 3662.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-30', 3621.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-27', 3638.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-25', 3629.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-24', 3635.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-23', 3577.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-20', 3557.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-19', 3581.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-18', 3567.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-17', 3609.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-16', 3626.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-13', 3585.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-12', 3537.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-11', 3572.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-10', 3545.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-09', 3550.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-06', 3509.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-05', 3510.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-04', 3443.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-03', 3369.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-11-02', 3310.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-30', 3269.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-29', 3310.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-28', 3271.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-27', 3390.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-26', 3400.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-23', 3465.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-22', 3453.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-21', 3435.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-20', 3443.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-19', 3426.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-16', 3483.81); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-15', 3483.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-14', 3488.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-13', 3511.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-12', 3534.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-09', 3477.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-08', 3446.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-07', 3419.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-06', 3360.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-05', 3408.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-02', 3348.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-10-01', 3380.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-30', 3363); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-29', 3335.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-28', 3351.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-25', 3298.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-24', 3246.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-23', 3236.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-22', 3315.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-21', 3281.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-18', 3319.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-17', 3357.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-16', 3385.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-15', 3401.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-14', 3383.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-11', 3340.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-10', 3339.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-09', 3398.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-08', 3331.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-04', 3426.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-03', 3455.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-02', 3580.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-09-01', 3526.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-31', 3500.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-28', 3508.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-27', 3484.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-26', 3478.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-25', 3443.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-24', 3431.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-21', 3397.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-20', 3385.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-19', 3374.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-18', 3389.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-17', 3381.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-14', 3372.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-13', 3373.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-12', 3380.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-11', 3333.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-10', 3360.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-07', 3351.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-06', 3349.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-05', 3327.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-04', 3306.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-08-03', 3294.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-31', 3271.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-30', 3246.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-29', 3258.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-28', 3218.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-27', 3239.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-24', 3215.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-23', 3235.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-22', 3276.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-21', 3257.3); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-20', 3251.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-17', 3224.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-16', 3215.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-15', 3226.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-14', 3197.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-13', 3155.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-10', 3185.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-09', 3152.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-08', 3169.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-07', 3145.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-06', 3179.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-02', 3130.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-07-01', 3115.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-30', 3100.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-29', 3053.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-26', 3009.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-25', 3083.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-24', 3050.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-23', 3131.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-22', 3117.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-19', 3097.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-18', 3115.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-17', 3113.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-16', 3124.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-15', 3066.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-12', 3041.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-11', 3002.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-10', 3190.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-09', 3207.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-08', 3232.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-05', 3193.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-04', 3112.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-03', 3122.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-02', 3080.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-06-01', 3055.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-29', 3044.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-28', 3029.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-27', 3036.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-26', 2991.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-22', 2955.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-21', 2948.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-20', 2971.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-19', 2922.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-18', 2953.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-15', 2863.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-14', 2852.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-13', 2820); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-12', 2870.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-11', 2930.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-08', 2929.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-07', 2881.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-06', 2848.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-05', 2868.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-04', 2842.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-05-01', 2830.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-30', 2912.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-29', 2939.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-28', 2863.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-27', 2878.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-24', 2836.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-23', 2797.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-22', 2799.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-21', 2736.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-20', 2823.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-17', 2874.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-16', 2799.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-15', 2783.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-14', 2846.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-13', 2761.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-09', 2789.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-08', 2749.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-07', 2659.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-06', 2663.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-03', 2488.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-02', 2526.9); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-04-01', 2470.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-31', 2584.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-30', 2626.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-27', 2541.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-26', 2630.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-25', 2475.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-24', 2447.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-23', 2237.4); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-20', 2304.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-19', 2409.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-18', 2398.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-17', 2529.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-16', 2386.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-13', 2711.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-12', 2480.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-11', 2741.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-10', 2882.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-09', 2746.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-06', 2972.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-05', 3023.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-04', 3130.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-03', 3003.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-03-02', 3090.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-28', 2954.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-27', 2978.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-26', 3116.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-25', 3128.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-24', 3225.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-21', 3337.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-20', 3373.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-19', 3386.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-18', 3370.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-14', 3380.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-13', 3373.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-12', 3379.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-11', 3357.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-10', 3352.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-07', 3327.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-06', 3345.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-05', 3334.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-04', 3297.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-02-03', 3248.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-31', 3225.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-30', 3283.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-29', 3273.4); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-28', 3276.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-27', 3243.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-24', 3295.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-23', 3325.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-22', 3321.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-21', 3320.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-17', 3329.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-16', 3316.81); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-15', 3289.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-14', 3283.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-13', 3288.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-10', 3265.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-09', 3274.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-08', 3253.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-07', 3237.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-06', 3246.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-03', 3234.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2020-01-02', 3257.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-31', 3230.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-30', 3221.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-27', 3240.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-26', 3239.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-24', 3223.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-23', 3224.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-20', 3221.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-19', 3205.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-18', 3191.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-17', 3192.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-16', 3191.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-13', 3168.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-12', 3168.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-11', 3141.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-10', 3132.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-09', 3135.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-06', 3145.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-05', 3117.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-04', 3112.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-03', 3093.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-12-02', 3113.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-29', 3140.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-27', 3153.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-26', 3140.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-25', 3133.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-22', 3110.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-21', 3103.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-20', 3108.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-19', 3120.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-18', 3122.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-15', 3120.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-14', 3096.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-13', 3094.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-12', 3091.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-11', 3087.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-08', 3093.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-07', 3085.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-06', 3076.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-05', 3074.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-04', 3078.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-11-01', 3066.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-31', 3037.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-30', 3046.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-29', 3036.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-28', 3039.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-25', 3022.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-24', 3010.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-23', 3004.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-22', 2995.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-21', 3006.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-18', 2986.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-17', 2997.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-16', 2989.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-15', 2995.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-14', 2966.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-11', 2970.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-10', 2938.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-09', 2919.4); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-08', 2893.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-07', 2938.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-04', 2952.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-03', 2910.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-02', 2887.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-10-01', 2940.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-30', 2976.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-27', 2961.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-26', 2977.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-25', 2984.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-24', 2966.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-23', 2991.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-20', 2992.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-19', 3006.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-18', 3006.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-17', 3005.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-16', 2997.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-13', 3007.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-12', 3009.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-11', 3000.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-10', 2979.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-09', 2978.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-06', 2978.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-05', 2976); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-04', 2937.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-09-03', 2906.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-30', 2926.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-29', 2924.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-28', 2887.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-27', 2869.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-26', 2878.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-23', 2847.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-22', 2922.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-21', 2924.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-20', 2900.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-19', 2923.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-16', 2888.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-15', 2847.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-14', 2840.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-13', 2926.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-12', 2883.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-09', 2918.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-08', 2938.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-07', 2883.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-06', 2881.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-05', 2844.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-02', 2932.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-08-01', 2953.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-31', 2980.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-30', 3013.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-29', 3020.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-26', 3025.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-25', 3003.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-24', 3019.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-23', 3005.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-22', 2985.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-19', 2976.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-18', 2995.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-17', 2984.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-16', 3004.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-15', 3014.3); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-12', 3013.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-11', 2999.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-10', 2993.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-09', 2979.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-08', 2975.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-05', 2990.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-03', 2995.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-02', 2973.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-07-01', 2964.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-28', 2941.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-27', 2924.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-26', 2913.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-25', 2917.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-24', 2945.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-21', 2950.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-20', 2954.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-19', 2926.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-18', 2917.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-17', 2889.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-14', 2886.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-13', 2891.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-12', 2879.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-11', 2885.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-10', 2886.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-07', 2873.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-06', 2843.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-05', 2826.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-04', 2803.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-06-03', 2744.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-31', 2752.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-30', 2788.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-29', 2783.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-28', 2802.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-24', 2826.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-23', 2822.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-22', 2856.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-21', 2864.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-20', 2840.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-17', 2859.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-16', 2876.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-15', 2850.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-14', 2834.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-13', 2811.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-10', 2881.4); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-09', 2870.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-08', 2879.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-07', 2884.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-06', 2932.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-03', 2945.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-02', 2917.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-05-01', 2923.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-30', 2945.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-29', 2943.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-26', 2939.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-25', 2926.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-24', 2927.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-23', 2933.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-22', 2907.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-18', 2905.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-17', 2900.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-16', 2907.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-15', 2905.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-12', 2907.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-11', 2888.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-10', 2888.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-09', 2878.20); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-08', 2895.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-05', 2892.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-04', 2879.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-03', 2873.40); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-02', 2867.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-04-01', 2867.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-29', 2834.40); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-28', 2815.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-27', 2805.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-26', 2818.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-25', 2798.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-22', 2800.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-21', 2854.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-20', 2824.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-19', 2832.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-18', 2832.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-15', 2822.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-14', 2808.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-13', 2810.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-12', 2791.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-11', 2783.30); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-08', 2743.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-07', 2748.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-06', 2771.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-05', 2789.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-04', 2792.81); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-03-01', 2803.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-28', 2784.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-27', 2792.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-26', 2793.90); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-25', 2796.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-22', 2792.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-21', 2774.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-20', 2784.70); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-19', 2779.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-15', 2775.60); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-14', 2745.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-13', 2753.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-12', 2744.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-11', 2709.80); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-08', 2707.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-07', 2706.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-06', 2731.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-05', 2737.70); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-04', 2724.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-02-01', 2706.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-31', 2704.10); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-30', 2681.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-29', 2640.00); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-28', 2643.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-25', 2664.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-24', 2642.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-23', 2638.70); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-22', 2632.90); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-18', 2670.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-17', 2635.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-16', 2616.10); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-15', 2610.30); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-14', 2582.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-11', 2596.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-10', 2596.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-09', 2584.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-08', 2574.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-07', 2549.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-04', 2531.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-03', 2447.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', DATE '2019-01-02', 2510.03); \ No newline at end of file diff --git a/core/src/test/resources/data/quotes.sql b/core/src/test/resources/data/quotes.sql new file mode 100644 index 000000000..1d8bf5740 --- /dev/null +++ b/core/src/test/resources/data/quotes.sql @@ -0,0 +1,1258 @@ +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-29', 4769.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-28', 4783.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-27', 4781.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-26', 4774.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-22', 4754.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-21', 4746.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-20', 4698.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-19', 4768.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-18', 4740.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-15', 4719.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-14', 4719.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-13', 4707.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-12', 4643.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-11', 4622.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-08', 4604.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-07', 4585.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-06', 4549.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-05', 4567.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-04', 4569.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-12-01', 4594.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-30', 4567.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-29', 4550.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-28', 4554.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-27', 4550.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-24', 4559.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-22', 4556.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-21', 4538.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-20', 4547.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-17', 4514.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-16', 4508.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-15', 4502.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-14', 4495.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-13', 4411.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-10', 4415.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-09', 4347.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-08', 4382.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-07', 4378.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-06', 4365.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-03', 4358.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-02', 4317.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-11-01', 4237.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-31', 4193.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-30', 4166.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-27', 4117.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-26', 4137.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-25', 4186.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-24', 4247.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-23', 4217.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-20', 4224.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-19', 4278); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-18', 4314.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-17', 4373.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-16', 4373.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-13', 4327.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-12', 4349.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-11', 4376.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-10', 4358.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-09', 4335.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-06', 4308.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-05', 4258.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-04', 4263.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-03', 4229.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-10-02', 4288.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-29', 4288.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-28', 4299.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-27', 4274.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-26', 4273.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-25', 4337.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-22', 4320.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-21', 4330); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-20', 4402.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-19', 4443.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-18', 4453.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-15', 4450.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-14', 4505.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-13', 4467.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-12', 4461.9); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-11', 4487.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-08', 4457.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-07', 4451.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-06', 4465.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-05', 4496.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-09-01', 4515.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-31', 4507.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-30', 4514.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-29', 4497.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-28', 4433.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-25', 4405.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-24', 4376.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-23', 4436.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-22', 4387.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-21', 4399.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-18', 4369.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-17', 4370.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-16', 4404.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-15', 4437.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-14', 4489.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-11', 4464.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-10', 4468.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-09', 4467.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-08', 4499.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-07', 4518.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-04', 4478.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-03', 4501.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-02', 4513.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-08-01', 4576.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-31', 4588.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-28', 4582.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-27', 4537.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-26', 4566.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-25', 4567.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-24', 4554.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-21', 4536.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-20', 4534.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-19', 4565.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-18', 4554.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-17', 4522.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-14', 4505.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-13', 4510.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-12', 4472.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-11', 4439.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-10', 4409.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-07', 4398.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-06', 4411.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-05', 4446.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-07-03', 4455.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-30', 4450.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-29', 4396.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-28', 4376.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-27', 4378.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-26', 4328.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-23', 4348.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-22', 4381.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-21', 4365.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-20', 4388.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-16', 4409.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-15', 4425.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-14', 4372.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-13', 4369.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-12', 4338.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-09', 4298.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-08', 4293.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-07', 4267.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-06', 4283.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-05', 4273.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-02', 4282.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-06-01', 4221.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-31', 4179.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-30', 4205.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-26', 4205.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-25', 4151.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-24', 4115.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-23', 4145.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-22', 4192.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-19', 4191.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-18', 4198.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-17', 4158.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-16', 4109.9); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-15', 4136.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-12', 4124.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-11', 4130.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-10', 4137.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-09', 4119.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-08', 4138.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-05', 4136.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-04', 4061.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-03', 4090.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-02', 4119.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-05-01', 4167.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-28', 4169.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-27', 4135.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-26', 4055.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-25', 4071.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-24', 4137.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-21', 4133.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-20', 4129.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-19', 4154.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-18', 4154.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-17', 4151.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-14', 4137.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-13', 4146.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-12', 4091.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-11', 4108.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-10', 4109.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-06', 4105.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-05', 4090.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-04', 4100.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-04-03', 4124.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-31', 4109.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-30', 4050.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-29', 4027.81); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-28', 3971.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-27', 3977.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-24', 3970.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-23', 3948.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-22', 3936.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-21', 4002.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-20', 3951.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-17', 3916.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-16', 3960.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-15', 3891.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-14', 3919.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-13', 3855.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-10', 3861.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-09', 3918.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-08', 3992.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-07', 3986.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-06', 4048.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-03', 4045.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-02', 3981.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-03-01', 3951.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-28', 3970.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-27', 3982.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-24', 3970.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-23', 4012.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-22', 3991.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-21', 3997.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-17', 4079.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-16', 4090.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-15', 4147.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-14', 4136.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-13', 4137.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-10', 4090.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-09', 4081.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-08', 4117.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-07', 4164); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-06', 4111.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-03', 4136.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-02', 4179.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-02-01', 4119.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-31', 4076.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-30', 4017.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-27', 4070.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-26', 4060.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-25', 4016.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-24', 4016.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-23', 4019.81); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-20', 3972.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-19', 3898.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-18', 3928.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-17', 3990.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-13', 3999.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-12', 3983.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-11', 3969.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-10', 3919.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-09', 3892.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-06', 3895.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-05', 3808.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-04', 3852.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2023-01-03', 3824.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-30', 3839.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-29', 3849.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-28', 3783.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-27', 3829.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-23', 3844.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-22', 3822.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-21', 3878.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-20', 3821.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-19', 3817.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-16', 3852.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-15', 3895.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-14', 3995.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-13', 4019.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-12', 3990.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-09', 3934.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-08', 3963.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-07', 3933.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-06', 3941.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-05', 3998.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-02', 4071.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-12-01', 4076.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-30', 4080.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-29', 3957.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-28', 3963.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-25', 4026.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-23', 4027.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-22', 4003.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-21', 3949.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-18', 3965.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-17', 3946.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-16', 3958.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-15', 3991.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-14', 3957.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-11', 3992.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-10', 3956.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-09', 3748.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-08', 3828.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-07', 3806.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-04', 3770.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-03', 3719.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-02', 3759.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-11-01', 3856.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-31', 3871.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-28', 3901.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-27', 3807.3); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-26', 3830.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-25', 3859.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-24', 3797.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-21', 3752.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-20', 3665.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-19', 3695.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-18', 3719.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-17', 3677.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-14', 3583.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-13', 3669.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-12', 3577.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-11', 3588.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-10', 3612.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-07', 3639.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-06', 3744.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-05', 3783.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-04', 3790.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-10-03', 3678.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-30', 3585.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-29', 3640.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-28', 3719.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-27', 3647.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-26', 3655.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-23', 3693.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-22', 3757.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-21', 3789.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-20', 3855.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-19', 3899.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-16', 3873.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-15', 3901.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-14', 3946.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-13', 3932.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-12', 4110.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-09', 4067.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-08', 4006.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-07', 3979.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-06', 3908.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-02', 3924.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-09-01', 3966.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-31', 3955); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-30', 3986.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-29', 4030.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-26', 4057.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-25', 4199.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-24', 4140.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-23', 4128.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-22', 4137.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-19', 4228.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-18', 4283.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-17', 4274.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-16', 4305.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-15', 4297.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-12', 4280.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-11', 4207.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-10', 4210.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-09', 4122.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-08', 4140.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-05', 4145.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-04', 4151.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-03', 4155.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-02', 4091.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-08-01', 4118.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-29', 4130.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-28', 4072.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-27', 4023.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-26', 3921.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-25', 3966.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-22', 3961.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-21', 3998.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-20', 3959.9); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-19', 3936.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-18', 3830.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-15', 3863.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-14', 3790.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-13', 3801.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-12', 3818.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-11', 3854.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-08', 3899.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-07', 3902.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-06', 3845.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-05', 3831.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-07-01', 3825.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-30', 3785.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-29', 3818.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-28', 3821.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-27', 3900.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-24', 3911.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-23', 3795.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-22', 3759.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-21', 3764.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-17', 3674.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-16', 3666.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-15', 3789.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-14', 3735.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-13', 3749.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-10', 3900.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-09', 4017.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-08', 4115.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-07', 4160.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-06', 4121.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-03', 4108.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-02', 4176.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-06-01', 4101.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-31', 4132.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-27', 4158.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-26', 4057.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-25', 3978.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-24', 3941.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-23', 3973.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-20', 3901.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-19', 3900.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-18', 3923.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-17', 4088.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-16', 4008.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-13', 4023.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-12', 3930.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-11', 3935.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-10', 4001.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-09', 3991.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-06', 4123.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-05', 4146.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-04', 4300.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-03', 4175.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-05-02', 4155.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-29', 4131.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-28', 4287.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-27', 4183.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-26', 4175.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-25', 4296.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-22', 4271.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-21', 4393.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-20', 4459.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-19', 4462.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-18', 4391.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-14', 4392.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-13', 4446.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-12', 4397.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-11', 4412.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-08', 4488.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-07', 4500.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-06', 4481.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-05', 4525.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-04', 4582.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-04-01', 4545.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-31', 4530.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-30', 4602.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-29', 4631.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-28', 4575.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-25', 4543.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-24', 4520.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-23', 4456.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-22', 4511.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-21', 4461.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-18', 4463.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-17', 4411.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-16', 4357.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-15', 4262.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-14', 4173.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-11', 4204.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-10', 4259.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-09', 4277.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-08', 4170.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-07', 4201.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-04', 4328.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-03', 4363.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-02', 4386.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-03-01', 4306.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-28', 4373.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-25', 4384.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-24', 4288.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-23', 4225.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-22', 4304.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-18', 4348.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-17', 4380.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-16', 4475.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-15', 4471.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-14', 4401.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-11', 4418.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-10', 4504.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-09', 4587.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-08', 4521.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-07', 4483.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-04', 4500.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-03', 4477.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-02', 4589.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-02-01', 4546.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-31', 4515.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-28', 4431.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-27', 4326.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-26', 4349.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-25', 4356.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-24', 4410.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-21', 4397.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-20', 4482.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-19', 4532.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-18', 4577.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-14', 4662.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-13', 4659.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-12', 4726.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-11', 4713.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-10', 4670.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-07', 4677.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-06', 4696.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-05', 4700.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-04', 4793.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2022-01-03', 4796.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-31', 4766.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-30', 4778.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-29', 4793.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-28', 4786.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-27', 4791.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-23', 4725.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-22', 4696.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-21', 4649.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-20', 4568.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-17', 4620.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-16', 4668.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-15', 4709.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-14', 4634.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-13', 4668.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-10', 4712.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-09', 4667.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-08', 4701.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-07', 4686.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-06', 4591.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-03', 4538.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-02', 4577.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-12-01', 4513.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-30', 4567); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-29', 4655.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-26', 4594.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-24', 4701.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-23', 4690.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-22', 4682.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-19', 4697.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-18', 4704.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-17', 4688.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-16', 4700.9); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-15', 4682.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-12', 4682.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-11', 4649.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-10', 4646.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-09', 4685.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-08', 4701.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-05', 4697.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-04', 4680.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-03', 4660.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-02', 4630.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-11-01', 4613.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-29', 4605.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-28', 4596.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-27', 4551.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-26', 4574.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-25', 4566.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-22', 4544.9); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-21', 4549.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-20', 4536.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-19', 4519.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-18', 4486.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-15', 4471.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-14', 4438.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-13', 4363.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-12', 4350.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-11', 4361.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-08', 4391.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-07', 4399.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-06', 4363.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-05', 4345.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-04', 4300.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-10-01', 4357.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-30', 4307.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-29', 4359.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-28', 4352.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-27', 4443.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-24', 4455.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-23', 4448.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-22', 4395.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-21', 4354.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-20', 4357.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-17', 4432.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-16', 4473.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-15', 4480.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-14', 4443.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-13', 4468.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-10', 4458.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-09', 4493.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-08', 4514.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-07', 4520.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-03', 4535.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-02', 4536.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-09-01', 4524.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-31', 4522.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-30', 4528.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-27', 4509.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-26', 4470); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-25', 4496.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-24', 4486.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-23', 4479.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-20', 4441.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-19', 4405.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-18', 4400.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-17', 4448.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-16', 4479.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-13', 4468); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-12', 4460.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-11', 4447.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-10', 4436.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-09', 4432.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-06', 4436.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-05', 4429.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-04', 4402.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-03', 4423.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-08-02', 4387.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-30', 4395.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-29', 4419.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-28', 4400.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-27', 4401.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-26', 4422.3); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-23', 4411.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-22', 4367.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-21', 4358.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-20', 4323.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-19', 4258.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-16', 4327.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-15', 4360.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-14', 4374.3); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-13', 4369.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-12', 4384.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-09', 4369.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-08', 4320.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-07', 4358.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-06', 4343.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-02', 4352.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-07-01', 4319.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-30', 4297.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-29', 4291.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-28', 4290.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-25', 4280.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-24', 4266.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-23', 4241.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-22', 4246.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-21', 4224.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-18', 4166.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-17', 4221.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-16', 4223.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-15', 4246.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-14', 4255.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-11', 4247.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-10', 4239.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-09', 4219.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-08', 4227.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-07', 4226.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-04', 4229.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-03', 4192.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-02', 4208.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-06-01', 4202.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-28', 4204.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-27', 4200.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-26', 4195.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-25', 4188.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-24', 4197.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-21', 4155.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-20', 4159.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-19', 4115.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-18', 4127.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-17', 4163.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-14', 4173.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-13', 4112.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-12', 4063.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-11', 4152.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-10', 4188.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-07', 4232.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-06', 4201.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-05', 4167.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-04', 4164.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-05-03', 4192.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-30', 4181.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-29', 4211.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-28', 4183.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-27', 4186.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-26', 4187.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-23', 4180.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-22', 4134.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-21', 4173.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-20', 4134.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-19', 4163.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-16', 4185.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-15', 4170.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-14', 4124.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-13', 4141.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-12', 4127.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-09', 4128.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-08', 4097.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-07', 4079.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-06', 4073.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-05', 4077.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-04-01', 4019.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-31', 3972.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-30', 3958.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-29', 3971.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-26', 3974.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-25', 3909.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-24', 3889.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-23', 3910.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-22', 3940.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-19', 3913.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-18', 3915.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-17', 3974.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-16', 3962.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-15', 3968.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-12', 3943.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-11', 3939.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-10', 3898.81); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-09', 3875.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-08', 3821.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-05', 3841.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-04', 3768.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-03', 3819.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-02', 3870.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-03-01', 3901.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-26', 3811.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-25', 3829.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-24', 3925.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-23', 3881.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-22', 3876.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-19', 3906.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-18', 3913.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-17', 3931.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-16', 3932.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-12', 3934.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-11', 3916.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-10', 3909.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-09', 3911.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-08', 3915.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-05', 3886.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-04', 3871.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-03', 3830.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-02', 3826.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-02-01', 3773.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-29', 3714.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-28', 3787.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-27', 3750.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-26', 3849.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-25', 3855.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-22', 3841.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-21', 3853.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-20', 3851.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-19', 3798.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-15', 3768.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-14', 3795.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-13', 3809.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-12', 3801.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-11', 3799.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-08', 3824.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-07', 3803.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-06', 3748.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-05', 3726.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2021-01-04', 3700.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-31', 3756.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-30', 3732.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-29', 3727.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-28', 3735.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-24', 3703.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-23', 3690.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-22', 3687.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-21', 3694.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-18', 3709.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-17', 3722.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-16', 3701.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-15', 3694.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-14', 3647.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-11', 3663.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-10', 3668.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-09', 3672.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-08', 3702.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-07', 3691.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-04', 3699.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-03', 3666.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-02', 3669.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-12-01', 3662.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-30', 3621.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-27', 3638.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-25', 3629.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-24', 3635.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-23', 3577.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-20', 3557.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-19', 3581.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-18', 3567.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-17', 3609.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-16', 3626.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-13', 3585.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-12', 3537.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-11', 3572.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-10', 3545.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-09', 3550.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-06', 3509.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-05', 3510.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-04', 3443.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-03', 3369.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-11-02', 3310.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-30', 3269.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-29', 3310.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-28', 3271.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-27', 3390.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-26', 3400.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-23', 3465.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-22', 3453.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-21', 3435.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-20', 3443.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-19', 3426.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-16', 3483.81); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-15', 3483.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-14', 3488.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-13', 3511.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-12', 3534.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-09', 3477.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-08', 3446.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-07', 3419.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-06', 3360.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-05', 3408.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-02', 3348.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-10-01', 3380.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-30', 3363); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-29', 3335.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-28', 3351.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-25', 3298.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-24', 3246.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-23', 3236.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-22', 3315.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-21', 3281.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-18', 3319.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-17', 3357.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-16', 3385.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-15', 3401.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-14', 3383.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-11', 3340.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-10', 3339.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-09', 3398.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-08', 3331.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-04', 3426.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-03', 3455.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-02', 3580.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-09-01', 3526.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-31', 3500.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-28', 3508.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-27', 3484.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-26', 3478.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-25', 3443.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-24', 3431.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-21', 3397.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-20', 3385.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-19', 3374.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-18', 3389.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-17', 3381.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-14', 3372.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-13', 3373.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-12', 3380.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-11', 3333.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-10', 3360.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-07', 3351.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-06', 3349.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-05', 3327.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-04', 3306.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-08-03', 3294.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-31', 3271.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-30', 3246.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-29', 3258.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-28', 3218.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-27', 3239.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-24', 3215.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-23', 3235.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-22', 3276.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-21', 3257.3); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-20', 3251.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-17', 3224.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-16', 3215.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-15', 3226.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-14', 3197.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-13', 3155.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-10', 3185.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-09', 3152.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-08', 3169.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-07', 3145.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-06', 3179.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-02', 3130.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-07-01', 3115.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-30', 3100.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-29', 3053.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-26', 3009.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-25', 3083.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-24', 3050.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-23', 3131.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-22', 3117.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-19', 3097.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-18', 3115.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-17', 3113.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-16', 3124.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-15', 3066.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-12', 3041.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-11', 3002.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-10', 3190.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-09', 3207.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-08', 3232.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-05', 3193.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-04', 3112.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-03', 3122.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-02', 3080.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-06-01', 3055.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-29', 3044.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-28', 3029.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-27', 3036.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-26', 2991.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-22', 2955.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-21', 2948.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-20', 2971.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-19', 2922.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-18', 2953.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-15', 2863.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-14', 2852.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-13', 2820); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-12', 2870.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-11', 2930.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-08', 2929.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-07', 2881.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-06', 2848.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-05', 2868.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-04', 2842.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-05-01', 2830.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-30', 2912.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-29', 2939.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-28', 2863.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-27', 2878.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-24', 2836.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-23', 2797.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-22', 2799.31); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-21', 2736.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-20', 2823.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-17', 2874.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-16', 2799.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-15', 2783.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-14', 2846.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-13', 2761.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-09', 2789.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-08', 2749.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-07', 2659.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-06', 2663.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-03', 2488.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-02', 2526.9); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-04-01', 2470.5); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-31', 2584.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-30', 2626.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-27', 2541.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-26', 2630.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-25', 2475.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-24', 2447.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-23', 2237.4); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-20', 2304.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-19', 2409.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-18', 2398.1); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-17', 2529.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-16', 2386.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-13', 2711.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-12', 2480.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-11', 2741.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-10', 2882.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-09', 2746.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-06', 2972.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-05', 3023.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-04', 3130.12); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-03', 3003.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-03-02', 3090.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-28', 2954.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-27', 2978.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-26', 3116.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-25', 3128.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-24', 3225.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-21', 3337.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-20', 3373.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-19', 3386.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-18', 3370.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-14', 3380.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-13', 3373.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-12', 3379.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-11', 3357.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-10', 3352.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-07', 3327.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-06', 3345.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-05', 3334.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-04', 3297.59); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-02-03', 3248.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-31', 3225.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-30', 3283.66); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-29', 3273.4); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-28', 3276.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-27', 3243.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-24', 3295.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-23', 3325.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-22', 3321.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-21', 3320.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-17', 3329.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-16', 3316.81); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-15', 3289.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-14', 3283.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-13', 3288.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-10', 3265.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-09', 3274.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-08', 3253.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-07', 3237.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-06', 3246.28); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-03', 3234.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2020-01-02', 3257.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-31', 3230.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-30', 3221.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-27', 3240.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-26', 3239.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-24', 3223.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-23', 3224.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-20', 3221.22); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-19', 3205.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-18', 3191.14); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-17', 3192.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-16', 3191.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-13', 3168.8); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-12', 3168.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-11', 3141.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-10', 3132.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-09', 3135.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-06', 3145.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-05', 3117.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-04', 3112.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-03', 3093.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-12-02', 3113.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-29', 3140.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-27', 3153.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-26', 3140.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-25', 3133.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-22', 3110.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-21', 3103.54); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-20', 3108.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-19', 3120.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-18', 3122.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-15', 3120.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-14', 3096.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-13', 3094.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-12', 3091.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-11', 3087.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-08', 3093.08); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-07', 3085.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-06', 3076.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-05', 3074.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-04', 3078.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-11-01', 3066.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-31', 3037.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-30', 3046.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-29', 3036.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-28', 3039.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-25', 3022.55); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-24', 3010.29); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-23', 3004.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-22', 2995.99); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-21', 3006.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-18', 2986.2); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-17', 2997.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-16', 2989.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-15', 2995.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-14', 2966.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-11', 2970.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-10', 2938.13); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-09', 2919.4); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-08', 2893.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-07', 2938.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-04', 2952.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-03', 2910.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-02', 2887.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-10-01', 2940.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-30', 2976.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-27', 2961.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-26', 2977.62); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-25', 2984.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-24', 2966.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-23', 2991.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-20', 2992.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-19', 3006.79); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-18', 3006.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-17', 3005.7); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-16', 2997.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-13', 3007.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-12', 3009.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-11', 3000.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-10', 2979.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-09', 2978.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-06', 2978.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-05', 2976); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-04', 2937.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-09-03', 2906.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-30', 2926.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-29', 2924.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-28', 2887.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-27', 2869.16); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-26', 2878.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-23', 2847.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-22', 2922.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-21', 2924.43); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-20', 2900.51); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-19', 2923.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-16', 2888.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-15', 2847.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-14', 2840.6); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-13', 2926.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-12', 2883.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-09', 2918.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-08', 2938.09); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-07', 2883.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-06', 2881.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-05', 2844.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-02', 2932.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-08-01', 2953.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-31', 2980.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-30', 3013.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-29', 3020.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-26', 3025.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-25', 3003.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-24', 3019.56); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-23', 3005.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-22', 2985.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-19', 2976.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-18', 2995.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-17', 2984.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-16', 3004.04); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-15', 3014.3); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-12', 3013.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-11', 2999.91); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-10', 2993.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-09', 2979.63); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-08', 2975.95); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-05', 2990.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-03', 2995.82); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-02', 2973.01); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-07-01', 2964.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-28', 2941.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-27', 2924.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-26', 2913.78); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-25', 2917.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-24', 2945.35); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-21', 2950.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-20', 2954.18); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-19', 2926.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-18', 2917.75); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-17', 2889.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-14', 2886.98); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-13', 2891.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-12', 2879.84); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-11', 2885.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-10', 2886.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-07', 2873.34); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-06', 2843.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-05', 2826.15); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-04', 2803.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-06-03', 2744.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-31', 2752.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-30', 2788.86); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-29', 2783.02); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-28', 2802.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-24', 2826.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-23', 2822.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-22', 2856.27); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-21', 2864.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-20', 2840.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-17', 2859.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-16', 2876.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-15', 2850.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-14', 2834.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-13', 2811.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-10', 2881.4); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-09', 2870.72); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-08', 2879.42); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-07', 2884.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-06', 2932.47); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-03', 2945.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-02', 2917.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-05-01', 2923.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-30', 2945.83); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-29', 2943.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-26', 2939.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-25', 2926.17); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-24', 2927.25); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-23', 2933.68); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-22', 2907.97); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-18', 2905.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-17', 2900.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-16', 2907.06); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-15', 2905.58); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-12', 2907.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-11', 2888.32); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-10', 2888.21); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-09', 2878.20); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-08', 2895.77); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-05', 2892.74); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-04', 2879.39); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-03', 2873.40); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-02', 2867.24); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-04-01', 2867.19); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-29', 2834.40); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-28', 2815.44); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-27', 2805.37); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-26', 2818.46); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-25', 2798.36); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-22', 2800.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-21', 2854.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-20', 2824.23); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-19', 2832.57); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-18', 2832.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-15', 2822.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-14', 2808.48); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-13', 2810.92); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-12', 2791.52); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-11', 2783.30); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-08', 2743.07); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-07', 2748.93); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-06', 2771.45); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-05', 2789.65); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-04', 2792.81); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-03-01', 2803.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-28', 2784.49); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-27', 2792.38); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-26', 2793.90); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-25', 2796.11); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-22', 2792.67); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-21', 2774.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-20', 2784.70); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-19', 2779.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-15', 2775.60); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-14', 2745.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-13', 2753.03); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-12', 2744.73); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-11', 2709.80); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-08', 2707.88); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-07', 2706.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-06', 2731.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-05', 2737.70); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-04', 2724.87); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-02-01', 2706.53); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-31', 2704.10); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-30', 2681.05); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-29', 2640.00); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-28', 2643.85); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-25', 2664.76); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-24', 2642.33); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-23', 2638.70); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-22', 2632.90); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-18', 2670.71); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-17', 2635.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-16', 2616.10); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-15', 2610.30); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-14', 2582.61); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-11', 2596.26); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-10', 2596.64); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-09', 2584.96); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-08', 2574.41); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-07', 2549.69); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-04', 2531.94); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-03', 2447.89); +INSERT INTO quotes (ticker, quote_date, close_price) VALUES ('SPX', '2019-01-02', 2510.03); \ No newline at end of file diff --git a/core/src/test/resources/ehcache.xml b/core/src/test/resources/ehcache.xml index 656dde30b..240148939 100644 --- a/core/src/test/resources/ehcache.xml +++ b/core/src/test/resources/ehcache.xml @@ -1,12 +1,24 @@ - - - - - - + + + + + + + + + + java.lang.Object + java.lang.Object + + 10000 + 1 + 10 + + + + + diff --git a/core/src/test/resources/flyway/db/hsqldb/migration/V1_0__initial_script.sql b/core/src/test/resources/flyway/db/hsqldb/migration/V1_0__initial_script.sql deleted file mode 100644 index f91673ba0..000000000 --- a/core/src/test/resources/flyway/db/hsqldb/migration/V1_0__initial_script.sql +++ /dev/null @@ -1,4 +0,0 @@ -create sequence hibernate_sequence start with 1 increment by 1; - -create table post (id bigint not null, title varchar(255), primary key (id)); -create table tag (id bigint not null, name varchar(255), primary key (id)); \ No newline at end of file diff --git a/core/src/test/resources/flyway/db/hsqldb/migration/V1_3__post_tag.sql b/core/src/test/resources/flyway/db/hsqldb/migration/V1_3__post_tag.sql deleted file mode 100644 index 796df9db9..000000000 --- a/core/src/test/resources/flyway/db/hsqldb/migration/V1_3__post_tag.sql +++ /dev/null @@ -1,4 +0,0 @@ -create table post_tag (post_id bigint not null, tag_id bigint not null); - -alter table post_tag add constraint POST_TAG_TAG_ID_FK foreign key (tag_id) references tag (id); -alter table post_tag add constraint POST_TAG_POST_ID_FK foreign key (post_id) references post (id); \ No newline at end of file diff --git a/core/src/test/resources/flyway/db/postgresql/migration/V1_0__initial_script.sql b/core/src/test/resources/flyway/db/postgresql/migration/V1_0__initial_script.sql deleted file mode 100644 index ea87bc296..000000000 --- a/core/src/test/resources/flyway/db/postgresql/migration/V1_0__initial_script.sql +++ /dev/null @@ -1,3 +0,0 @@ -create sequence hibernate_sequence start 1 increment 1; -create table post (id int8 not null, title varchar(255), primary key (id)); -create table tag (id int8 not null, name varchar(255), primary key (id)); \ No newline at end of file diff --git a/core/src/test/resources/flyway/db/postgresql/migration/V1_1__post_details.sql b/core/src/test/resources/flyway/db/postgresql/migration/V1_1__post_details.sql deleted file mode 100644 index 9a269d490..000000000 --- a/core/src/test/resources/flyway/db/postgresql/migration/V1_1__post_details.sql +++ /dev/null @@ -1,3 +0,0 @@ -create table post_details (id int8 not null, created_by varchar(255), created_on timestamp, primary key (id)); - -alter table post_details add constraint POST_DETAILS_POST_ID_FK foreign key (id) references post; \ No newline at end of file diff --git a/core/src/test/resources/flyway/db/postgresql/migration/V1_2__post_comment.sql b/core/src/test/resources/flyway/db/postgresql/migration/V1_2__post_comment.sql deleted file mode 100644 index ff5fbf46b..000000000 --- a/core/src/test/resources/flyway/db/postgresql/migration/V1_2__post_comment.sql +++ /dev/null @@ -1,3 +0,0 @@ -create table post_comment (id int8 not null, review varchar(255), post_id int8, primary key (id)); - -alter table post_comment add constraint POST_COMMENT_POST_ID_FK foreign key (post_id) references post; \ No newline at end of file diff --git a/core/src/test/resources/flyway/db/postgresql/migration/V1_3__post_tag.sql b/core/src/test/resources/flyway/db/postgresql/migration/V1_3__post_tag.sql deleted file mode 100644 index 3d02e6d90..000000000 --- a/core/src/test/resources/flyway/db/postgresql/migration/V1_3__post_tag.sql +++ /dev/null @@ -1,4 +0,0 @@ -create table post_tag (post_id int8 not null, tag_id int8 not null); - -alter table post_tag add constraint POST_TAG_TAG_ID_FK foreign key (tag_id) references tag; -alter table post_tag add constraint POST_TAG_POST_ID_FK foreign key (post_id) references post; \ No newline at end of file diff --git a/core/src/test/resources/flyway/db/postgresql/staging/V1_4__users.sql b/core/src/test/resources/flyway/db/postgresql/staging/V1_4__users.sql deleted file mode 100644 index 2612c2b1b..000000000 --- a/core/src/test/resources/flyway/db/postgresql/staging/V1_4__users.sql +++ /dev/null @@ -1 +0,0 @@ -create table users (id bigint not null, name varchar(255), primary key (id)); \ No newline at end of file diff --git a/core/src/test/resources/flyway/db/hsqldb/drop/drop.sql b/core/src/test/resources/flyway/scripts/hsqldb/drop/drop.sql similarity index 100% rename from core/src/test/resources/flyway/db/hsqldb/drop/drop.sql rename to core/src/test/resources/flyway/scripts/hsqldb/drop/drop.sql diff --git a/core/src/test/resources/flyway/scripts/hsqldb/migration/V1_0__post_tag.sql b/core/src/test/resources/flyway/scripts/hsqldb/migration/V1_0__post_tag.sql new file mode 100644 index 000000000..5758a9c89 --- /dev/null +++ b/core/src/test/resources/flyway/scripts/hsqldb/migration/V1_0__post_tag.sql @@ -0,0 +1,9 @@ +create sequence hibernate_sequence start with 1 increment by 1; + +create table post (id bigint not null, title varchar(255), primary key (id)); +create table tag (id bigint not null, name varchar(255), primary key (id)); + +create table post_tag (post_id bigint not null, tag_id bigint not null); + +alter table post_tag add constraint POST_TAG_TAG_ID_FK foreign key (tag_id) references tag (id); +alter table post_tag add constraint POST_TAG_POST_ID_FK foreign key (post_id) references post (id); \ No newline at end of file diff --git a/core/src/test/resources/flyway/db/hsqldb/migration/V1_1__post_details.sql b/core/src/test/resources/flyway/scripts/hsqldb/migration/V1_1__post_details.sql similarity index 100% rename from core/src/test/resources/flyway/db/hsqldb/migration/V1_1__post_details.sql rename to core/src/test/resources/flyway/scripts/hsqldb/migration/V1_1__post_details.sql diff --git a/core/src/test/resources/flyway/db/hsqldb/migration/V1_2__post_comment.sql b/core/src/test/resources/flyway/scripts/hsqldb/migration/V1_2__post_comment.sql similarity index 100% rename from core/src/test/resources/flyway/db/hsqldb/migration/V1_2__post_comment.sql rename to core/src/test/resources/flyway/scripts/hsqldb/migration/V1_2__post_comment.sql diff --git a/core/src/test/resources/flyway/db/hsqldb/staging/V1_4__users.sql b/core/src/test/resources/flyway/scripts/hsqldb/staging/V1_3__users.sql similarity index 100% rename from core/src/test/resources/flyway/db/hsqldb/staging/V1_4__users.sql rename to core/src/test/resources/flyway/scripts/hsqldb/staging/V1_3__users.sql diff --git a/core/src/test/resources/flyway/db/postgresql/drop/drop.sql b/core/src/test/resources/flyway/scripts/postgresql/drop/drop.sql similarity index 100% rename from core/src/test/resources/flyway/db/postgresql/drop/drop.sql rename to core/src/test/resources/flyway/scripts/postgresql/drop/drop.sql diff --git a/core/src/test/resources/flyway/scripts/postgresql/migration/V1_0__post_tag.sql b/core/src/test/resources/flyway/scripts/postgresql/migration/V1_0__post_tag.sql new file mode 100644 index 000000000..fad12ddc6 --- /dev/null +++ b/core/src/test/resources/flyway/scripts/postgresql/migration/V1_0__post_tag.sql @@ -0,0 +1,31 @@ +CREATE SEQUENCE post_seq +START 1 INCREMENT 1; + +CREATE TABLE post ( + id int8 NOT NULL, + title varchar(255), + PRIMARY KEY (id) +); + +CREATE SEQUENCE tag_seq + START 1 INCREMENT 1; + +CREATE TABLE tag ( + id int8 NOT NULL, + name varchar(255), + PRIMARY KEY (id) +); + +CREATE TABLE post_tag ( + post_id int8 NOT NULL, + tag_id int8 NOT NULL, + PRIMARY KEY (post_id, tag_id) +); + +ALTER TABLE post_tag +ADD CONSTRAINT POST_TAG_TAG_ID_FK +FOREIGN KEY (tag_id) REFERENCES tag; + +ALTER TABLE post_tag +ADD CONSTRAINT POST_TAG_POST_ID_FK +FOREIGN KEY (post_id) REFERENCES post; \ No newline at end of file diff --git a/core/src/test/resources/flyway/scripts/postgresql/migration/V1_1__post_details.sql b/core/src/test/resources/flyway/scripts/postgresql/migration/V1_1__post_details.sql new file mode 100644 index 000000000..b6b2f785d --- /dev/null +++ b/core/src/test/resources/flyway/scripts/postgresql/migration/V1_1__post_details.sql @@ -0,0 +1,10 @@ +CREATE TABLE post_details ( + id int8 NOT NULL, + created_by varchar(255), + created_on TIMESTAMP, + PRIMARY KEY (id) +); + +ALTER TABLE post_details +ADD CONSTRAINT POST_DETAILS_POST_ID_FK +FOREIGN KEY (id) REFERENCES post; \ No newline at end of file diff --git a/core/src/test/resources/flyway/scripts/postgresql/migration/V1_2__post_comment.sql b/core/src/test/resources/flyway/scripts/postgresql/migration/V1_2__post_comment.sql new file mode 100644 index 000000000..968a63c09 --- /dev/null +++ b/core/src/test/resources/flyway/scripts/postgresql/migration/V1_2__post_comment.sql @@ -0,0 +1,12 @@ +CREATE SEQUENCE post_comment_seq + START 1 INCREMENT 1; + +CREATE TABLE post_comment ( + id int8 NOT NULL, + review varchar(255), + post_id int8, PRIMARY KEY (id) +); + +ALTER TABLE post_comment +ADD CONSTRAINT POST_COMMENT_POST_ID_FK +FOREIGN KEY (post_id) REFERENCES post; \ No newline at end of file diff --git a/core/src/test/resources/flyway/scripts/postgresql/staging/V1_3__users.sql b/core/src/test/resources/flyway/scripts/postgresql/staging/V1_3__users.sql new file mode 100644 index 000000000..fa1b54a53 --- /dev/null +++ b/core/src/test/resources/flyway/scripts/postgresql/staging/V1_3__users.sql @@ -0,0 +1,8 @@ +CREATE SEQUENCE user_seq + START 1 INCREMENT 1; + +CREATE TABLE users ( + id bigint NOT NULL, + name varchar(255), + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/core/src/test/resources/logback-test.xml b/core/src/test/resources/logback-test.xml index dd21212dd..e5cbda1ae 100644 --- a/core/src/test/resources/logback-test.xml +++ b/core/src/test/resources/logback-test.xml @@ -6,16 +6,16 @@ target/test.log false - %-5p [%t]: %c{1} - %m%n + %d %-5p [%t]: %c{1} - %m%n UTF-8 - DEBUG + TRACE - %-5p [%t]: %c{1} - %m%n + %d %-5p [%t]:%X{txId} %c{1} - %m%n UTF-8 @@ -24,16 +24,20 @@ - - - - + + + + + + + - + diff --git a/core/src/test/resources/mappings/identifier/global/mysql-orm.xml b/core/src/test/resources/mappings/identifier/global/mysql-orm.xml index 1ed3fcf90..ec66720e7 100644 --- a/core/src/test/resources/mappings/identifier/global/mysql-orm.xml +++ b/core/src/test/resources/mappings/identifier/global/mysql-orm.xml @@ -2,10 +2,9 @@ - com.vladmihalcea.book.hpjp.hibernate.identifier.global + xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd" + version="2.2"> + com.vladmihalcea.hpjp.hibernate.identifier.global diff --git a/core/src/test/resources/spring/applicationContext-tx.xml b/core/src/test/resources/spring/applicationContext-tx.xml deleted file mode 100644 index 55eb45afa..000000000 --- a/core/src/test/resources/spring/applicationContext-tx.xml +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - ${jdbc.username} - ${jdbc.password} - ${jdbc.url} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/jooq/jooq-core/pom.xml b/jooq/jooq-core/pom.xml index 46ddee758..691349420 100644 --- a/jooq/jooq-core/pom.xml +++ b/jooq/jooq-core/pom.xml @@ -3,20 +3,78 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0%20http://maven.apache.org/xsd/maven-4.0.0.xsd"> - jooq - com.vladmihalcea.book + com.vladmihalcea + high-performance-java-persistence-jooq 1.0-SNAPSHOT 4.0.0 - jooq-core + high-performance-java-persistence-jooq-core + + net.ttddyy + datasource-proxy + ${datasource-proxy.version} + + + + org.postgresql + postgresql + ${postgresql.version} + + + + com.mysql + mysql-connector-j + ${mysql.version} + + + + com.microsoft.sqlserver + mssql-jdbc + ${mssql.version} + + + + com.oracle.database.jdbc + ojdbc8 + ${oracle.version} + + + + com.zaxxer + HikariCP + ${hikari.version} + + + org.slf4j + slf4j-api + + + + + + junit + junit + ${junit.version} + + org.jooq jooq ${jooq.version} + + javax.xml.bind + jaxb-api + ${jaxb-api.version} + + + javax.activation + activation + ${activation.version} + diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/hpjp/jooq/AbstractJOOQIntegrationTest.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/hpjp/jooq/AbstractJOOQIntegrationTest.java new file mode 100644 index 000000000..7c1ae0b4d --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/hpjp/jooq/AbstractJOOQIntegrationTest.java @@ -0,0 +1,138 @@ +package com.vladmihalcea.hpjp.jooq; + +import com.vladmihalcea.util.AbstractTest; +import org.hibernate.Session; +import org.hibernate.Transaction; +import org.hibernate.tool.schema.internal.script.MultiLineSqlScriptExtractor; +import org.jooq.DSLContext; +import org.jooq.SQLDialect; +import org.jooq.conf.Settings; +import org.jooq.impl.DSL; +import org.junit.Assert; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.fail; + + +/** + * @author Vlad Mihalcea + */ +public abstract class AbstractJOOQIntegrationTest extends AbstractTest { + + @Override + protected Class[] entities() { + return new Class[] { + }; + } + + protected abstract String ddlFolder(); + + protected abstract String ddlScript(); + + protected abstract SQLDialect sqlDialect(); + + @Override + protected void beforeInit() { + try { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader( + Thread.currentThread().getContextClassLoader().getResource(String.format("%s/%s", ddlFolder(), ddlScript())).openStream() + ) + ) + ) { + List sqlStatements = MultiLineSqlScriptExtractor.INSTANCE.extractCommands(reader, dialect()); + try(Connection connection = dataSource().getConnection(); + Statement statement = connection.createStatement()) { + for (String sqlStatement : sqlStatements) { + try { + statement.execute(sqlStatement); + } catch (SQLException ignore) { + LOGGER.error("Script [{}] failed when executing statement [{}]", ddlScript(), sqlStatement); + } + } + } + } + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + } + + @Override + protected Properties properties() { + Properties properties = super.properties(); + properties.put("hibernate.hbm2ddl.auto", "validate"); + return properties; + } + + protected T doInJOOQ(DSLContextCallable callable, Settings settings) { + Session session = null; + Transaction txn = null; + try { + session = sessionFactory().openSession(); + txn = session.beginTransaction(); + T result = session.doReturningWork(connection -> { + DSLContext sql = settings != null ? + DSL.using(connection, sqlDialect(), settings) : + DSL.using(connection, sqlDialect()); + return callable.execute(sql); + }); + txn.commit(); + return result; + } catch (Throwable e) { + if ( txn != null ) txn.rollback(); + throw e; + } finally { + if (session != null) { + session.close(); + } + } + } + + protected void doInJOOQ(DSLContextVoidCallable callable, Settings settings) { + Session session = null; + Transaction txn = null; + try { + session = sessionFactory().openSession(); + txn = session.beginTransaction(); + session.doWork(connection -> { + DSLContext sql = settings != null ? + DSL.using(connection, sqlDialect(), settings) : + DSL.using(connection, sqlDialect()); + callable.execute(sql); + }); + txn.commit(); + } catch (Throwable e) { + if ( txn != null ) txn.rollback(); + throw e; + } finally { + if (session != null) { + session.close(); + } + } + } + + protected T doInJOOQ(DSLContextCallable callable) { + return doInJOOQ(callable, null); + } + + protected void doInJOOQ(DSLContextVoidCallable callable) { + doInJOOQ(callable, null); + } + + @FunctionalInterface + protected interface DSLContextCallable { + T execute(DSLContext sql) throws SQLException; + } + + @FunctionalInterface + protected interface DSLContextVoidCallable { + void execute(DSLContext sql) throws SQLException; + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/AbstractOracleIntegrationTest.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/AbstractOracleIntegrationTest.java new file mode 100644 index 000000000..d656bb54a --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/AbstractOracleIntegrationTest.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.util; + +import com.vladmihalcea.util.providers.Database; + +/** + * AbstractOracleXEIntegrationTest - Abstract Orcale XE IntegrationTest + * + * @author Vlad Mihalcea + */ +public abstract class AbstractOracleIntegrationTest extends AbstractTest { + + @Override + protected Database database() { + return Database.ORACLE; + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/AbstractPostgreSQLIntegrationTest.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/AbstractPostgreSQLIntegrationTest.java new file mode 100644 index 000000000..cf28dc04c --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/AbstractPostgreSQLIntegrationTest.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.util; + +import com.vladmihalcea.util.providers.Database; + +/** + * AbstractPostgreSQLIntegrationTest - Abstract PostgreSQL IntegrationTest + * + * @author Vlad Mihalcea + */ +public abstract class AbstractPostgreSQLIntegrationTest extends AbstractTest { + + @Override + protected Database database() { + return Database.POSTGRESQL; + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/AbstractSQLServerIntegrationTest.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/AbstractSQLServerIntegrationTest.java new file mode 100644 index 000000000..ec23be632 --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/AbstractSQLServerIntegrationTest.java @@ -0,0 +1,16 @@ +package com.vladmihalcea.util; + +import com.vladmihalcea.util.providers.Database; + +/** + * AbstractSQLServerIntegrationTest - Abstract SQL Server IntegrationTest + * + * @author Vlad Mihalcea + */ +public abstract class AbstractSQLServerIntegrationTest extends AbstractTest { + + @Override + protected Database database() { + return Database.SQLSERVER; + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/AbstractTest.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/AbstractTest.java new file mode 100644 index 000000000..2db41df37 --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/AbstractTest.java @@ -0,0 +1,670 @@ +package com.vladmihalcea.util; + +import com.vladmihalcea.util.providers.DataSourceProvider; +import com.vladmihalcea.util.providers.Database; +import com.vladmihalcea.util.transaction.*; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.EntityTransaction; +import jakarta.persistence.spi.PersistenceUnitInfo; +import org.hibernate.*; +import org.hibernate.boot.MetadataBuilder; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.SessionFactoryBuilder; +import org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl; +import org.hibernate.boot.registry.BootstrapServiceRegistry; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; +import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor; +import org.hibernate.jpa.boot.spi.IntegratorProvider; +import org.hibernate.jpa.boot.spi.TypeContributorList; +import org.hibernate.usertype.UserType; +import org.junit.After; +import org.junit.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.io.Closeable; +import java.io.IOException; +import java.sql.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Collectors; + +public abstract class AbstractTest { + + public static final boolean ENABLE_LONG_RUNNING_TESTS = false; + + static { + Thread.currentThread().setName("Alice"); + } + + protected final ExecutorService executorService = Executors.newSingleThreadExecutor(r -> { + Thread bob = new Thread(r); + bob.setName("Bob"); + return bob; + }); + + protected final Logger LOGGER = LoggerFactory.getLogger(getClass()); + + private DataSource dataSource; + + private EntityManagerFactory emf; + + private SessionFactory sf; + + private List closeables = new ArrayList<>(); + + @Before + public void init() { + beforeInit(); + if (nativeHibernateSessionFactoryBootstrap()) { + sf = newSessionFactory(); + } else { + emf = newEntityManagerFactory(); + } + afterInit(); + } + + protected void beforeInit() { + + } + + protected void afterInit() { + + } + + @After + public void destroy() { + if (nativeHibernateSessionFactoryBootstrap()) { + if (sf != null) { + sf.close(); + } + } else { + if (emf != null) { + emf.close(); + } + } + for (Closeable closeable : closeables) { + try { + closeable.close(); + } catch (IOException e) { + LOGGER.error("Failure", e); + } + } + closeables.clear(); + afterDestroy(); + } + + protected void afterDestroy() { + + } + + public EntityManagerFactory entityManagerFactory() { + return nativeHibernateSessionFactoryBootstrap() ? sf : emf; + } + + public SessionFactory sessionFactory() { + if (nativeHibernateSessionFactoryBootstrap()) { + return sf; + } + EntityManagerFactory entityManagerFactory = entityManagerFactory(); + if (entityManagerFactory == null) { + return null; + } + return entityManagerFactory.unwrap(SessionFactory.class); + } + + protected boolean nativeHibernateSessionFactoryBootstrap() { + return false; + } + + protected Class[] entities() { + return new Class[]{}; + } + + protected List entityClassNames() { + return Arrays.asList(entities()).stream().map(Class::getName).collect(Collectors.toList()); + } + + protected String[] packages() { + return null; + } + + protected String[] resources() { + return null; + } + + protected Interceptor interceptor() { + return null; + } + + private SessionFactory newSessionFactory() { + final BootstrapServiceRegistryBuilder bsrb = new BootstrapServiceRegistryBuilder() + .enableAutoClose(); + + Integrator integrator = integrator(); + if (integrator != null) { + bsrb.applyIntegrator(integrator); + } + + final BootstrapServiceRegistry bsr = bsrb.build(); + + final StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder(bsr) + .applySettings(properties()) + .build(); + + final MetadataSources metadataSources = new MetadataSources(serviceRegistry); + + for (Class annotatedClass : entities()) { + metadataSources.addAnnotatedClass(annotatedClass); + } + + String[] packages = packages(); + if (packages != null) { + for (String annotatedPackage : packages) { + metadataSources.addPackage(annotatedPackage); + } + } + + String[] resources = resources(); + if (resources != null) { + for (String resource : resources) { + metadataSources.addResource(resource); + } + } + + final MetadataBuilder metadataBuilder = metadataSources.getMetadataBuilder() + .applyImplicitNamingStrategy(ImplicitNamingStrategyLegacyJpaImpl.INSTANCE); + + final List> additionalTypes = additionalTypes(); + if (additionalTypes != null) { + additionalTypes.forEach(type -> { + metadataBuilder.applyTypes((typeContributions, sr) -> typeContributions.contributeType(type)); + }); + } + + additionalMetadata(metadataBuilder); + + MetadataImplementor metadata = (MetadataImplementor) metadataBuilder.build(); + + final SessionFactoryBuilder sfb = metadata.getSessionFactoryBuilder(); + Interceptor interceptor = interceptor(); + if (interceptor != null) { + sfb.applyInterceptor(interceptor); + } + + return sfb.build(); + } + + private SessionFactory newLegacySessionFactory() { + Properties properties = properties(); + Configuration configuration = new Configuration().addProperties(properties); + for (Class entityClass : entities()) { + configuration.addAnnotatedClass(entityClass); + } + String[] packages = packages(); + if (packages != null) { + for (String scannedPackage : packages) { + configuration.addPackage(scannedPackage); + } + } + String[] resources = resources(); + if (resources != null) { + for (String resource : resources) { + configuration.addResource(resource); + } + } + Interceptor interceptor = interceptor(); + if (interceptor != null) { + configuration.setInterceptor(interceptor); + } + + final List> additionalTypes = additionalTypes(); + if (additionalTypes != null) { + configuration.registerTypeContributor( + (typeContributions, serviceRegistry) -> + additionalTypes.forEach(typeContributions::contributeType) + ); + } + return configuration.buildSessionFactory( + new StandardServiceRegistryBuilder() + .applySettings(properties) + .build() + ); + } + + protected EntityManagerFactory newEntityManagerFactory() { + PersistenceUnitInfo persistenceUnitInfo = persistenceUnitInfo(getClass().getSimpleName()); + Map configuration = properties(); + Interceptor interceptor = interceptor(); + if (interceptor != null) { + configuration.put(AvailableSettings.INTERCEPTOR, interceptor); + } + Integrator integrator = integrator(); + if (integrator != null) { + configuration.put("hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList(integrator)); + } + + List> additionalTypes = additionalTypes(); + if (additionalTypes != null) { + configuration.put("hibernate.type_contributors", + (TypeContributorList) () -> Collections.singletonList( + (typeContributions, serviceRegistry) -> { + additionalTypes.forEach(typeContributions::contributeType); + } + )); + } + + EntityManagerFactoryBuilderImpl entityManagerFactoryBuilder = new EntityManagerFactoryBuilderImpl( + new PersistenceUnitInfoDescriptor(persistenceUnitInfo), configuration + ); + return entityManagerFactoryBuilder.build(); + } + + protected Integrator integrator() { + return null; + } + + protected PersistenceUnitInfoImpl persistenceUnitInfo(String name) { + PersistenceUnitInfoImpl persistenceUnitInfo = new PersistenceUnitInfoImpl( + name, entityClassNames(), properties() + ); + String[] resources = resources(); + if (resources != null) { + persistenceUnitInfo.getMappingFileNames().addAll(Arrays.asList(resources)); + } + return persistenceUnitInfo; + } + + protected Properties properties() { + Properties properties = new Properties(); + //log settings + properties.put("hibernate.hbm2ddl.auto", "none"); + //data source settings + DataSource dataSource = dataSource(); + if (dataSource != null) { + properties.put("hibernate.connection.datasource", dataSource); + } + additionalProperties(properties); + return properties; + } + + protected Dialect dialect() { + SessionFactory sessionFactory = sessionFactory(); + return sessionFactory != null ? + sessionFactory.unwrap(SessionFactoryImplementor.class).getJdbcServices().getDialect() : + ReflectionUtils.newInstance(dataSourceProvider().hibernateDialect()); + } + + protected Map propertiesMap() { + return (Map) properties(); + } + + protected void additionalProperties(Properties properties) { + + } + + protected DataSourceProxyType dataSourceProxyType() { + return DataSourceProxyType.DATA_SOURCE_PROXY; + } + + protected DataSource dataSource() { + if (dataSource == null) { + dataSource = newDataSource(); + } + return dataSource; + } + + protected DataSource newDataSource() { + DataSource dataSource = + proxyDataSource() + ? dataSourceProxyType().dataSource(dataSourceProvider().dataSource()) + : dataSourceProvider().dataSource(); + if (connectionPooling()) { + HikariDataSource poolingDataSource = connectionPoolDataSource(dataSource); + closeables.add(poolingDataSource::close); + return poolingDataSource; + } else { + return dataSource; + } + } + + protected boolean proxyDataSource() { + return true; + } + + protected HikariDataSource connectionPoolDataSource(DataSource dataSource) { + return new HikariDataSource(hikariConfig(dataSource)); + } + + protected HikariConfig hikariConfig(DataSource dataSource) { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setMaximumPoolSize(connectionPoolSize()); + hikariConfig.setDataSource(dataSource); + return hikariConfig; + } + + protected boolean connectionPooling() { + return false; + } + + protected int connectionPoolSize() { + int cpuCores = Runtime.getRuntime().availableProcessors(); + return cpuCores * 4; + } + + protected DataSourceProvider dataSourceProvider() { + return database().dataSourceProvider(); + } + + protected Database database() { + return Database.POSTGRESQL; + } + + protected List> additionalTypes() { + return null; + } + + protected void additionalMetadata(MetadataBuilder metadataBuilder) { + + } + + protected T doInJPA(JPATransactionFunction function) { + T result = null; + EntityManager entityManager = null; + EntityTransaction txn = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + function.beforeTransactionCompletion(); + txn = entityManager.getTransaction(); + txn.begin(); + result = function.apply(entityManager); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + function.afterTransactionCompletion(); + if (entityManager != null) { + entityManager.close(); + } + } + return result; + } + + protected void doInJPA(JPATransactionVoidFunction function) { + EntityManager entityManager = null; + EntityTransaction txn = null; + try { + entityManager = entityManagerFactory().createEntityManager(); + function.beforeTransactionCompletion(); + txn = entityManager.getTransaction(); + txn.begin(); + function.accept(entityManager); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + function.afterTransactionCompletion(); + if (entityManager != null) { + entityManager.close(); + } + } + } + + protected void doInJDBC(ConnectionVoidCallable callable) { + Session session = null; + Transaction txn = null; + try { + session = sessionFactory().openSession(); + session.setDefaultReadOnly(true); + session.setHibernateFlushMode(FlushMode.MANUAL); + txn = session.beginTransaction(); + session.doWork(callable::execute); + if (!txn.getRollbackOnly()) { + txn.commit(); + } else { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + } catch (Throwable t) { + if (txn != null && txn.isActive()) { + try { + txn.rollback(); + } catch (Exception e) { + LOGGER.error("Rollback failure", e); + } + } + throw t; + } finally { + if (session != null) { + session.close(); + } + } + } + + protected Future executeAsync(Runnable callable) { + return executorService.submit(callable); + } + + protected void awaitOnLatch(CountDownLatch latch) { + try { + latch.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + + protected void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + protected V sleep(int millis, Callable callable) { + V result = null; + try { + if (callable != null) { + result = callable.call(); + } + Thread.sleep(millis); + } catch (Exception e) { + throw new IllegalStateException(e); + } + return result; + } + + protected void awaitTermination(long timeout, TimeUnit unit) { + try { + executorService.awaitTermination(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + protected void executeStatement(String sql) { + try (Connection connection = dataSource().getConnection(); + Statement statement = connection.createStatement()) { + statement.executeUpdate(sql); + } catch (SQLException e) { + LOGGER.error("Statement failed", e); + } + } + + protected void executeStatement(String... sqls) { + try (Connection connection = dataSource().getConnection(); + Statement statement = connection.createStatement()) { + for(String sql : sqls) { + statement.executeUpdate(sql); + } + } catch (SQLException e) { + LOGGER.error("Statement failed", e); + } + } + + protected void executeStatement(Connection connection, String sql) { + try { + try (Statement statement = connection.createStatement()) { + statement.execute(sql); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + protected void executeStatement(Connection connection, String sql, int timeout) { + try { + try (Statement statement = connection.createStatement()) { + statement.setQueryTimeout(timeout); + statement.execute(sql); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + protected void executeStatement(Connection connection, String... sqls) { + try { + try (Statement statement = connection.createStatement()) { + for (String sql : sqls) { + statement.execute(sql); + } + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + protected void executeStatement(EntityManager entityManager, String... sqls) { + Session session = entityManager.unwrap(Session.class); + for (String sql : sqls) { + try { + session.doWork(connection -> { + executeStatement(connection, sql); + }); + } catch (Exception e) { + LOGGER.error( + String.format("Error executing statement: %s", sql), e + ); + } + } + } + + protected int update(Connection connection, String sql, Object[] params) { + try { + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setQueryTimeout(1); + for (int i = 0; i < params.length; i++) { + statement.setObject(i + 1, params[i]); + } + return statement.executeUpdate(); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + protected int count(Connection connection, String sql) { + try { + try (Statement statement = connection.createStatement()) { + statement.setQueryTimeout(1); + ResultSet resultSet = statement.executeQuery(sql); + if (!resultSet.next()) { + throw new IllegalArgumentException("There was no row to be selected!"); + } + return ((Number) resultSet.getObject(1)).intValue(); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + /** + * Set JDBC Connection or Statement timeout + * + * @param connection JDBC Connection time out + */ + public void setJdbcTimeout(Connection connection) { + setJdbcTimeout(connection, 1000); + } + + /** + * Set JDBC Connection or Statement timeout + * + * @param connection JDBC Connection time out + * @param timoutMillis millis to wait + */ + public void setJdbcTimeout(Connection connection, long timoutMillis) { + try (Statement st = connection.createStatement()) { + DataSourceProvider dataSourceProvider = dataSourceProvider(); + + switch (dataSourceProvider.database()) { + case POSTGRESQL: + st.execute(String.format("SET statement_timeout TO %d", timoutMillis)); + break; + case MYSQL: + st.execute(String.format("SET SESSION innodb_lock_wait_timeout = %d", TimeUnit.MILLISECONDS.toSeconds(timoutMillis))); + connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), (int) timoutMillis); + break; + case SQLSERVER: + st.execute(String.format("SET LOCK_TIMEOUT %d", timoutMillis)); + connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), (int) timoutMillis); + break; + default: + try { + connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), (int) timoutMillis); + } catch (Throwable ignore) { + ignore.fillInStackTrace(); + } + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/CollectionUtils.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/CollectionUtils.java new file mode 100644 index 000000000..4552e5569 --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/CollectionUtils.java @@ -0,0 +1,42 @@ +package com.vladmihalcea.util; + +import java.util.List; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * CollectionUtils - Collection utilities holder. + * + * @author Vlad Mihalcea + */ +public final class CollectionUtils { + + /** + * Prevent any instantiation. + */ + private CollectionUtils() { + throw new UnsupportedOperationException("The " + getClass() + " is not instantiable!"); + } + + /** + * Split an element collection into batches. + * + * @param elements elements to split in batches + * @param class type + * @return the Stream of batches + */ + public static Stream> spitInBatches(List elements, int batchSize) { + int elementCount = elements.size(); + if (elementCount <= 0) { + return Stream.empty(); + } + int batchCount = (elementCount - 1) / batchSize; + return IntStream.range(0, batchCount + 1) + .mapToObj( + batchNumber -> elements.subList( + batchNumber * batchSize, + batchNumber == batchCount ? elementCount : (batchNumber + 1) * batchSize + ) + ); + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/DataSourceProxyType.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/DataSourceProxyType.java new file mode 100644 index 000000000..156416b0a --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/DataSourceProxyType.java @@ -0,0 +1,32 @@ +package com.vladmihalcea.util; + +import com.vladmihalcea.util.logging.InlineQueryLogEntryCreator; +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.listener.DataSourceQueryCountListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; + +import javax.sql.DataSource; + +/** + * @author Vlad Mihalcea + */ +public enum DataSourceProxyType { + DATA_SOURCE_PROXY { + @Override + public DataSource dataSource(DataSource dataSource) { + ChainListener listener = new ChainListener(); + SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); + //loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); + listener.addListener(loggingListener); + listener.addListener(new DataSourceQueryCountListener()); + return ProxyDataSourceBuilder + .create(dataSource) + .name(name()) + .listener(listener) + .build(); + } + }; + + public abstract DataSource dataSource(DataSource dataSource); +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/PersistenceUnitInfoImpl.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/PersistenceUnitInfoImpl.java new file mode 100644 index 000000000..632e68e6f --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/PersistenceUnitInfoImpl.java @@ -0,0 +1,144 @@ +package com.vladmihalcea.util; + +import jakarta.persistence.SharedCacheMode; +import jakarta.persistence.ValidationMode; +import jakarta.persistence.spi.ClassTransformer; +import jakarta.persistence.spi.PersistenceUnitInfo; +import jakarta.persistence.spi.PersistenceUnitTransactionType; +import org.hibernate.jpa.HibernatePersistenceProvider; + +import javax.sql.DataSource; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class PersistenceUnitInfoImpl implements PersistenceUnitInfo { + + public static final String JPA_VERSION = "3.1"; + + private final String persistenceUnitName; + + private PersistenceUnitTransactionType transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL; + + private final List managedClassNames; + + private final List mappingFileNames = new ArrayList<>(); + + private final Properties properties; + + private DataSource jtaDataSource; + + private DataSource nonJtaDataSource; + + public PersistenceUnitInfoImpl( + String persistenceUnitName, + List managedClassNames, + Properties properties) { + this.persistenceUnitName = persistenceUnitName; + this.managedClassNames = managedClassNames; + this.properties = properties; + } + + @Override + public String getPersistenceUnitName() { + return persistenceUnitName; + } + + @Override + public String getPersistenceProviderClassName() { + return HibernatePersistenceProvider.class.getName(); + } + + @Override + public PersistenceUnitTransactionType getTransactionType() { + return transactionType; + } + + @Override + public DataSource getJtaDataSource() { + return jtaDataSource; + } + + public PersistenceUnitInfoImpl setJtaDataSource(DataSource jtaDataSource) { + this.jtaDataSource = jtaDataSource; + this.nonJtaDataSource = null; + transactionType = PersistenceUnitTransactionType.JTA; + return this; + } + + @Override + public DataSource getNonJtaDataSource() { + return nonJtaDataSource; + } + + public PersistenceUnitInfoImpl setNonJtaDataSource(DataSource nonJtaDataSource) { + this.nonJtaDataSource = nonJtaDataSource; + this.jtaDataSource = null; + transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL; + return this; + } + + @Override + public List getMappingFileNames() { + return mappingFileNames; + } + + @Override + public List getJarFileUrls() { + return Collections.emptyList(); + } + + @Override + public URL getPersistenceUnitRootUrl() { + return null; + } + + @Override + public List getManagedClassNames() { + return managedClassNames; + } + + @Override + public boolean excludeUnlistedClasses() { + return false; + } + + @Override + public SharedCacheMode getSharedCacheMode() { + return SharedCacheMode.UNSPECIFIED; + } + + @Override + public ValidationMode getValidationMode() { + return ValidationMode.AUTO; + } + + public Properties getProperties() { + return properties; + } + + @Override + public String getPersistenceXMLSchemaVersion() { + return JPA_VERSION; + } + + @Override + public ClassLoader getClassLoader() { + return Thread.currentThread().getContextClassLoader(); + } + + @Override + public void addTransformer(ClassTransformer transformer) { + + } + + @Override + public ClassLoader getNewTempClassLoader() { + return null; + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/ReflectionUtils.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/ReflectionUtils.java new file mode 100644 index 000000000..cec964b04 --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/ReflectionUtils.java @@ -0,0 +1,744 @@ +package com.vladmihalcea.util; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +/** + * ReflectionUtils - Reflection utilities holder. + * + * @author Vlad Mihalcea + */ +public final class ReflectionUtils { + + private static final String GETTER_PREFIX = "get"; + + private static final String SETTER_PREFIX = "set"; + + /** + * Prevent any instantiation. + */ + private ReflectionUtils() { + throw new UnsupportedOperationException("The " + getClass() + " is not instantiable!"); + } + + /** + * Instantiate a new {@link Object} of the provided type. + * + * @param className The fully-qualified Java class name of the {@link Object} to instantiate + * @param class type + * @return new Java {@link Object} of the provided type + */ + public static T newInstance(String className) { + Class clazz = getClass(className); + return newInstance(clazz); + } + + /** + * Instantiate a new {@link Object} of the provided type. + * + * @param clazz The Java {@link Class} of the {@link Object} to instantiate + * @param class type + * @return new Java {@link Object} of the provided type + */ + @SuppressWarnings("unchecked") + public static T newInstance(Class clazz) { + try { + return (T) clazz.newInstance(); + } catch (InstantiationException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Instantiate a new {@link Object} of the provided type. + * + * @param clazz The Java {@link Class} of the {@link Object} to instantiate + * @param args The arguments that need to be passed to the constructor + * @param argsTypes The argument types that need to be passed to the constructor + * @param class type + * @return new Java {@link Object} of the provided type + */ + @SuppressWarnings("unchecked") + public static T newInstance(Class clazz, Object[] args, Class[] argsTypes) { + try { + Constructor constructor = clazz.getDeclaredConstructor(argsTypes); + constructor.setAccessible(true); + return constructor.newInstance(args); + } catch (InstantiationException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (NoSuchMethodException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Get the {@link Field} with the given name belonging to the provided Java {@link Class}. + * + * @param targetClass the provided Java {@link Class} the field belongs to + * @param fieldName the {@link Field} name + * @return the {@link Field} matching the given name + */ + public static Field getField(Class targetClass, String fieldName) { + Field field = null; + + try { + field = targetClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + try { + field = targetClass.getField(fieldName); + } catch (NoSuchFieldException ignore) { + } + + if (!targetClass.getSuperclass().equals(Object.class)) { + return getField(targetClass.getSuperclass(), fieldName); + } else { + throw handleException(e); + } + } finally { + if (field != null) { + field.setAccessible(true); + } + } + + return field; + } + + /** + * Get the {@link Field} with the given name belonging to the provided Java {@link Class} or {@code null} + * if no {@link Field} was found. + * + * @param targetClass the provided Java {@link Class} the field belongs to + * @param fieldName the {@link Field} name + * @return the {@link Field} matching the given name or {@code null} + */ + public static Field getFieldOrNull(Class targetClass, String fieldName) { + try { + return getField(targetClass, fieldName); + } catch (IllegalArgumentException e) { + return null; + } + } + + /** + * Get the value of the field matching the given name and belonging to target {@link Object}. + * + * @param target target {@link Object} whose field we are retrieving the value from + * @param fieldName field name + * @param field type + * @return field value + */ + public static T getFieldValue(Object target, String fieldName) { + try { + Field field = getField(target.getClass(), fieldName); + @SuppressWarnings("unchecked") + T returnValue = (T) field.get(target); + return returnValue; + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Get the value of the field matching the given name and belonging to target {@link Object} or {@code null} + * if no {@link Field} was found.. + * + * @param target target {@link Object} whose field we are retrieving the value from + * @param fieldName field name + * @param field type + * @return field value matching the given name or {@code null} + */ + public static T getFieldValueOrNull(Object target, String fieldName) { + try { + Field field = getField(target.getClass(), fieldName); + @SuppressWarnings("unchecked") + T returnValue = (T) field.get(target); + return returnValue; + } catch (IllegalAccessException e) { + return null; + } + } + + /** + * Set the value of the field matching the given name and belonging to target {@link Object}. + * + * @param target target object + * @param fieldName field name + * @param value field value + */ + public static void setFieldValue(Object target, String fieldName, Object value) { + try { + Field field = getField(target.getClass(), fieldName); + field.set(target, value); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Object}. + * + * @param target target {@link Object} + * @param methodName method name + * @param parameterTypes method parameter types + * @return return {@link Method} matching the provided signature + */ + public static Method getMethod(Object target, String methodName, Class... parameterTypes) { + return getMethod(target.getClass(), methodName, parameterTypes); + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Object} or {@code null} if no {@link Method} was found. + * + * @param target target {@link Object} + * @param methodName method name + * @param parameterTypes method parameter types + * @return return {@link Method} matching the provided signature or {@code null} + */ + public static Method getMethodOrNull(Object target, String methodName, Class... parameterTypes) { + try { + return getMethod(target.getClass(), methodName, parameterTypes); + } catch (RuntimeException e) { + return null; + } + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Class}. + * + * @param targetClass target {@link Class} + * @param methodName method name + * @param parameterTypes method parameter types + * @return the {@link Method} matching the provided signature + */ + @SuppressWarnings("unchecked") + public static Method getMethod(Class targetClass, String methodName, Class... parameterTypes) { + try { + return targetClass.getDeclaredMethod(methodName, parameterTypes); + } catch (NoSuchMethodException e) { + try { + return targetClass.getMethod(methodName, parameterTypes); + } catch (NoSuchMethodException ignore) { + } + + if (!targetClass.getSuperclass().equals(Object.class)) { + return getMethod(targetClass.getSuperclass(), methodName, parameterTypes); + } else { + throw handleException(e); + } + } + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Object} or {@code null} if no {@link Method} was found. + * + * @param targetClass target {@link Class} + * @param methodName method name + * @param parameterTypes method parameter types + * @return return {@link Method} matching the provided signature or {@code null} + */ + public static Method getMethodOrNull(Class targetClass, String methodName, Class... parameterTypes) { + try { + return getMethod(targetClass, methodName, parameterTypes); + } catch (RuntimeException e) { + return null; + } + } + + /** + * Get the {@link Method} with the given signature (name and parameter types) belonging to + * the provided Java {@link Class}, excluding inherited ones, or {@code null} if no {@link Method} was found. + * + * @param targetClass target {@link Class} + * @param methodName method name + * @param parameterTypes method parameter types + * @return return {@link Method} matching the provided signature or {@code null} + */ + public static Method getDeclaredMethodOrNull(Class targetClass, String methodName, Class... parameterTypes) { + try { + return targetClass.getDeclaredMethod(methodName, parameterTypes); + } catch (NoSuchMethodException e) { + return null; + } + } + + /** + * Check if the provided Java {@link Class} contains a method matching + * the given signature (name and parameter types). + * + * @param targetClass target {@link Class} + * @param methodName method name + * @param parameterTypes method parameter types + * @return the provided Java {@link Class} contains a method with the given signature + */ + public static boolean hasMethod(Class targetClass, String methodName, Class... parameterTypes) { + try { + targetClass.getMethod(methodName, parameterTypes); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + /** + * Get the property setter {@link Method} with the given signature (name and parameter types) + * belonging to the provided Java {@link Object}. + * + * @param target target {@link Object} + * @param propertyName property name + * @param parameterType setter property type + * @return the setter {@link Method} matching the provided signature + */ + public static Method getSetter(Object target, String propertyName, Class parameterType) { + String setterMethodName = SETTER_PREFIX + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + Method setter = getMethod(target, setterMethodName, parameterType); + setter.setAccessible(true); + return setter; + } + + /** + * Get the property setter {@link Method} with the given signature (name and parameter types) + * belonging to the provided Java {@link Object} or {@code null} if no setter + * was found matching the provided name. + * + * @param target target {@link Object} + * @param propertyName property name + * @param parameterType setter property type + * @return the setter {@link Method} matching the provided signature or {@code null} + */ + public static Method getSetterOrNull(Object target, String propertyName, Class parameterType) { + try { + return getSetter(target, propertyName, parameterType); + } catch (Exception e) { + return null; + } + } + + /** + * Get the property getter {@link Method} with the given name belonging to + * the provided Java {@link Object}. + * + * @param target target {@link Object} + * @param propertyName property name + * @return the getter {@link Method} matching the provided name + */ + public static Method getGetter(Object target, String propertyName) { + String getterMethodName = GETTER_PREFIX + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + Method getter = getMethod(target, getterMethodName); + getter.setAccessible(true); + return getter; + } + + /** + * Invoke the provided {@link Method} on the given Java {@link Object}. + * + * @param target target {@link Object} whose method we are invoking + * @param method method to invoke + * @param parameters parameters passed to the method call + * @param return value object type + * @return the value return by the {@link Method} invocation + */ + public static T invokeMethod(Object target, Method method, Object... parameters) { + try { + method.setAccessible(true); + @SuppressWarnings("unchecked") + T returnValue = (T) method.invoke(target, parameters); + return returnValue; + } catch (InvocationTargetException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Invoke the method with the provided signature (name and parameter types) + * on the given Java {@link Object}. + * + * @param target target {@link Object} whose method we are invoking + * @param methodName method name to invoke + * @param parameters parameters passed to the method call + * @param return value object type + * @return the value return by the method invocation + */ + public static T invokeMethod(Object target, String methodName, Object... parameters) { + try { + Class[] parameterClasses = new Class[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + parameterClasses[i] = parameters[i].getClass(); + } + + Method method = getMethod(target, methodName, parameterClasses); + method.setAccessible(true); + @SuppressWarnings("unchecked") + T returnValue = (T) method.invoke(target, parameters); + return returnValue; + } catch (InvocationTargetException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Invoke the property getter with the provided name on the given Java {@link Object}. + * + * @param target target {@link Object} whose property getter we are invoking + * @param propertyName property name whose getter we are invoking + * @param return value object type + * @return the value return by the getter invocation + */ + public static T invokeGetter(Object target, String propertyName) { + Method setter = getGetter(target, propertyName); + try { + return (T) setter.invoke(target); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Invoke the property setter with the provided signature (name and parameter types) + * on the given Java {@link Object}. + * + * @param target target {@link Object} whose property setter we are invoking + * @param propertyName property name whose setter we are invoking + * @param parameter parameter passed to the setter call + */ + public static void invokeSetter(Object target, String propertyName, Object parameter) { + Method setter = getSetter(target, propertyName, parameter.getClass()); + try { + setter.invoke(target, parameter); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Invoke the {@link boolean} property setter with the provided name + * on the given Java {@link Object}. + * + * @param target target {@link Object} whose property setter we are invoking + * @param propertyName property name whose setter we are invoking + * @param parameter {@link boolean} parameter passed to the setter call + */ + public static void invokeSetter(Object target, String propertyName, boolean parameter) { + Method setter = getSetter(target, propertyName, boolean.class); + try { + setter.invoke(target, parameter); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Invoke the {@link int} property setter with the provided name + * on the given Java {@link Object}. + * + * @param target target {@link Object} whose property setter we are invoking + * @param propertyName property name whose setter we are invoking + * @param parameter {@link int} parameter passed to the setter call + */ + public static void invokeSetter(Object target, String propertyName, int parameter) { + Method setter = getSetter(target, propertyName, int.class); + try { + setter.invoke(target, parameter); + } catch (IllegalAccessException e) { + throw handleException(e); + } catch (InvocationTargetException e) { + throw handleException(e); + } + } + + /** + * Invoke the {@code static} {@link Method} with the provided parameters. + * + * @param method target {@code static} {@link Method} to invoke + * @param parameters parameters passed to the method call + * @param return value object type + * @return the value return by the method invocation + */ + public static T invokeStaticMethod(Method method, Object... parameters) { + try { + method.setAccessible(true); + @SuppressWarnings("unchecked") + T returnValue = (T) method.invoke(null, parameters); + return returnValue; + } catch (InvocationTargetException e) { + throw handleException(e); + } catch (IllegalAccessException e) { + throw handleException(e); + } + } + + /** + * Get the Java {@link Class} with the given fully-qualified name. + * + * @param className the Java {@link Class} name to be retrieved + * @param {@link Class} type + * @return the Java {@link Class} object + */ + @SuppressWarnings("unchecked") + public static Class getClass(String className) { + try { + return (Class) Class.forName(className, false, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw handleException(e); + } + } + + /** + * Get the {@link URI} resource with the given fully-qualified name. + * + * @param name the {@link URI} resource to be retrieved + * @return the Java {@link Class} object + */ + public static URL getResource(String name) { + return Thread.currentThread().getContextClassLoader().getResource(name); + } + + /** + * Get the Java {@link Class} with the given fully-qualified name or or {@code null} + * if no {@link Class} was found matching the provided name. + * + * @param className the Java {@link Class} name to be retrieved + * @param {@link Class} type + * @return the Java {@link Class} object or {@code null} + */ + @SuppressWarnings("unchecked") + public static Class getClassOrNull(String className) { + try { + return (Class) getClass(className); + } catch (Exception e) { + return null; + } + } + + /** + * Get the Java Wrapper {@link Class} associated to the given primitive type. + * + * @param clazz primitive class + * @return the Java Wrapper {@link Class} + */ + public static Class getWrapperClass(Class clazz) { + if (!clazz.isPrimitive()) + return clazz; + + if (clazz == Integer.TYPE) + return Integer.class; + if (clazz == Long.TYPE) + return Long.class; + if (clazz == Boolean.TYPE) + return Boolean.class; + if (clazz == Byte.TYPE) + return Byte.class; + if (clazz == Character.TYPE) + return Character.class; + if (clazz == Float.TYPE) + return Float.class; + if (clazz == Double.TYPE) + return Double.class; + if (clazz == Short.TYPE) + return Short.class; + if (clazz == Void.TYPE) + return Void.class; + + return clazz; + } + + /** + * Get the first super class matching the provided package name. + * + * @param clazz Java class + * @param packageName package name + * @param class generic type + * @return the first super class matching the provided package name or {@code null}. + */ + public static Class getFirstSuperClassFromPackage(Class clazz, String packageName) { + if (clazz.getPackage().getName().equals(packageName)) { + return clazz; + } else { + Class superClass = clazz.getSuperclass(); + return (superClass == null || superClass.equals(Object.class)) ? + null : + (Class) getFirstSuperClassFromPackage(superClass, packageName); + } + } + + /** + * Get the generic types of a given Class. + * + * @param parameterizedType parameterized Type + * @return generic types for the given Class. + */ + public static Set getGenericTypes(ParameterizedType parameterizedType) { + Set genericTypes = new LinkedHashSet<>(); + for(Type genericType : parameterizedType.getActualTypeArguments()) { + if (genericType instanceof Class) { + genericTypes.add((Class) genericType); + } + } + return genericTypes; + } + + /** + * Get class package name. + * + * @param className Class name. + * @return class package name + */ + public static String getClassPackageName(String className) { + try { + Class clazz = getClassOrNull(className); + if(clazz == null) { + return null; + } + Package classPackage = clazz.getPackage(); + return classPackage != null ? classPackage.getName() : null; + } catch (Exception e) { + return null; + } + } + + /** + * Get the {@link Member} with the given name belonging to the provided Java {@link Class} or {@code null} + * if no {@link Member} was found. + * + * @param targetClass the provided Java {@link Class} the field or method belongs to + * @param memberName the {@link Field} or {@link Method} name + * @return the {@link Field} or {@link Method} matching the given name or {@code null} + */ + public static Member getMemberOrNull(Class targetClass, String memberName) { + Field field = getFieldOrNull(targetClass, memberName); + return (field != null) ? field : getMethodOrNull(targetClass, memberName); + } + + /** + * Get the generic {@link Type} of the {@link Member} with the given name belonging to the provided Java {@link Class} or {@code null} + * if no {@link Member} was found. + * + * @param targetClass the provided Java {@link Class} the field or method belongs to + * @param memberName the {@link Field} or {@link Method} name + * @return the generic {@link Type} of the {@link Field} or {@link Method} matching the given name or {@code null} + */ + public static Type getMemberGenericTypeOrNull(Class targetClass, String memberName) { + Field field = getFieldOrNull(targetClass, memberName); + return (field != null) ? field.getGenericType() : getMethodOrNull(targetClass, memberName).getGenericReturnType(); + } + + /** + * Get classes by their package name + * @param packageName package name + * @return classes + */ + public static List getClassesByPackage(String packageName) { + List classes = new ArrayList<>(); + + try { + final String packagePath = packageName.replace('.', File.separatorChar); + final String javaClassExtension = ".class"; + try (Stream allPaths = Files.walk(Paths.get(getResource(packagePath).toURI()))) { + allPaths.filter(Files::isRegularFile).forEach(file -> { + final String path = file.toString().replace(File.separatorChar, '.'); + final String name = path.substring( + path.indexOf(packageName), + path.length() - javaClassExtension.length() + ); + classes.add(ReflectionUtils.getClass(name)); + }); + } + } catch (URISyntaxException | IOException e) { + throw new IllegalArgumentException(e); + } + + return classes; + } + + /** + * Handle the {@link NoSuchFieldException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link NoSuchFieldException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(NoSuchFieldException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link NoSuchMethodException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link NoSuchMethodException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(NoSuchMethodException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link IllegalAccessException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link IllegalAccessException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(IllegalAccessException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link InvocationTargetException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link InvocationTargetException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(InvocationTargetException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link ClassNotFoundException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link ClassNotFoundException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(ClassNotFoundException e) { + return new IllegalArgumentException(e); + } + + /** + * Handle the {@link InstantiationException} by rethrowing it as an {@link IllegalArgumentException}. + * + * @param e the original {@link InstantiationException} + * @return the {@link IllegalArgumentException} wrapping exception + */ + private static IllegalArgumentException handleException(InstantiationException e) { + return new IllegalArgumentException(e); + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/exception/ExceptionUtil.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/exception/ExceptionUtil.java new file mode 100644 index 000000000..a61df9f1e --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/exception/ExceptionUtil.java @@ -0,0 +1,153 @@ +package com.vladmihalcea.util.exception; + +import jakarta.persistence.LockTimeoutException; +import org.hibernate.PessimisticLockException; +import org.hibernate.exception.LockAcquisitionException; + +import java.sql.SQLTimeoutException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author Vlad Mihalcea + */ +public interface ExceptionUtil { + + List> LOCK_TIMEOUT_EXCEPTIONS = Arrays.asList( + LockAcquisitionException.class, + LockTimeoutException.class, + PessimisticLockException.class, + jakarta.persistence.PessimisticLockException.class, + SQLTimeoutException.class + ); + + /** + * Get the root cause of a particular {@code Throwable} + * + * @param t exception + * @return exception root cause + */ + static T rootCause(Throwable t) { + Throwable cause = t.getCause(); + if (cause != null && cause != t) { + return rootCause(cause); + } + return (T) t; + } + + /** + * Is the given throwable caused by a database lock timeout? + * + * @param e exception + * @return is caused by a database lock timeout + */ + static boolean isLockTimeout(Throwable e) { + AtomicReference causeHolder = new AtomicReference<>(e); + do { + final Throwable cause = causeHolder.get(); + final String failureMessage = cause.getMessage().toLowerCase(); + if (LOCK_TIMEOUT_EXCEPTIONS.stream().anyMatch(c -> c.isInstance(cause)) || + failureMessage.contains("timeout") || + failureMessage.contains("timed out") || + failureMessage.contains("time out") || + failureMessage.contains("closed connection") || + failureMessage.contains("connection is closed") || + failureMessage.contains("link failure") || + failureMessage.contains("expired or aborted by a conflict") + ) { + return true; + } else { + if (cause.getCause() == null || cause.getCause() == cause) { + break; + } else { + causeHolder.set(cause.getCause()); + } + } + } + while (true); + return false; + } + + /** + * Is the given throwable caused by the following exception type? + * + * @param e exception + * @param exceptionType exception type + * @return is caused by the given exception type + */ + static boolean isCausedBy(Throwable e, Class exceptionType) { + AtomicReference causeHolder = new AtomicReference<>(e); + do { + final Throwable cause = causeHolder.get(); + if (exceptionType.isInstance(cause)) { + return true; + } else { + if (cause.getCause() == null || cause.getCause() == cause) { + break; + } else { + causeHolder.set(cause.getCause()); + } + } + } + while (true); + return false; + } + + /** + * Is the given throwable caused by a database MVCC anomaly detection? + * + * @param e exception + * @return is caused by a database lock MVCC anomaly detection + */ + static boolean isMVCCAnomalyDetection(Throwable e) { + AtomicReference causeHolder = new AtomicReference<>(e); + do { + final Throwable cause = causeHolder.get(); + String lowerCaseMessage = cause.getMessage().toLowerCase(); + if ( + cause.getMessage().contains("ORA-08177: can't serialize access for this transaction") //Oracle + || lowerCaseMessage.contains("could not serialize access due to concurrent update") //PSQLException + || lowerCaseMessage.contains("ould not serialize access due to read/write dependencies among transactions") //PSQLException + || lowerCaseMessage.contains("snapshot isolation transaction aborted due to update conflict") //SQLServerException + ) { + return true; + } else { + if (cause.getCause() == null || cause.getCause() == cause) { + break; + } else { + causeHolder.set(cause.getCause()); + } + } + } + while (true); + return false; + } + + /** + * Was the given exception caused by a SQL connection close + * + * @param e exception + * @return is caused by a SQL connection close + */ + static boolean isConnectionClose(Exception e) { + Throwable cause = e; + do { + if (cause.getMessage().toLowerCase().contains("connection is close") + || cause.getMessage().toLowerCase().contains("closed connection") + || cause.getMessage().toLowerCase().contains("link failure") + || cause.getMessage().toLowerCase().contains("closed") + ) { + return true; + } else { + if (cause.getCause() == null || cause.getCause() == cause) { + break; + } else { + cause = cause.getCause(); + } + } + } + while (true); + return false; + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/logging/InlineQueryLogEntryCreator.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/logging/InlineQueryLogEntryCreator.java new file mode 100644 index 000000000..5f3efd22b --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/logging/InlineQueryLogEntryCreator.java @@ -0,0 +1,86 @@ +package com.vladmihalcea.util.logging; + +import net.ttddyy.dsproxy.ExecutionInfo; +import net.ttddyy.dsproxy.QueryInfo; +import net.ttddyy.dsproxy.listener.logging.DefaultQueryLogEntryCreator; + +import java.util.*; + +/** + * @author Vlad Mihalcea + */ +public class InlineQueryLogEntryCreator extends DefaultQueryLogEntryCreator { + + @Override + protected void writeParamsEntry(StringBuilder sb, ExecutionInfo execInfo, List queryInfoList) { + sb.append("Params:["); + for (QueryInfo queryInfo : queryInfoList) { + boolean firstArg = true; + for (Map paramMap : queryInfo.getQueryArgsList()) { + + if (!firstArg) { + sb.append(", "); + } else { + firstArg = false; + } + + SortedMap sortedParamMap = new TreeMap<>(new CustomStringAsIntegerComparator()); + sortedParamMap.putAll(paramMap); + + sb.append("("); + boolean firstParam = true; + for (Map.Entry paramEntry : sortedParamMap.entrySet()) { + if (!firstParam) { + sb.append(", "); + } else { + firstParam = false; + } + Object parameter = paramEntry.getValue(); + if (parameter != null && parameter.getClass().isArray()) { + sb.append(arrayToString(parameter)); + } else { + sb.append(parameter); + } + } + sb.append(")"); + } + } + sb.append("]"); + } + + private String arrayToString(Object object) { + if (object.getClass().isArray()) { + if (object instanceof byte[]) { + return Arrays.toString((byte[]) object); + } + if (object instanceof short[]) { + return Arrays.toString((short[]) object); + } + if (object instanceof char[]) { + return Arrays.toString((char[]) object); + } + if (object instanceof int[]) { + return Arrays.toString((int[]) object); + } + if (object instanceof long[]) { + return Arrays.toString((long[]) object); + } + if (object instanceof float[]) { + return Arrays.toString((float[]) object); + } + if (object instanceof double[]) { + return Arrays.toString((double[]) object); + } + if (object instanceof boolean[]) { + return Arrays.toString((boolean[]) object); + } + if (object instanceof Object[]) { + return Arrays.toString((Object[]) object); + } + } + throw new UnsupportedOperationException("Array type not supported: " + object.getClass()); + } + + private static class CustomStringAsIntegerComparator extends StringAsIntegerComparator { + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/AbstractContainerDataSourceProvider.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/AbstractContainerDataSourceProvider.java new file mode 100644 index 000000000..3c94fe321 --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/AbstractContainerDataSourceProvider.java @@ -0,0 +1,30 @@ +package com.vladmihalcea.util.providers; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * @author Vlad Mihalcea + */ +public abstract class AbstractContainerDataSourceProvider implements DataSourceProvider { + + @Override + public DataSource dataSource() { + DataSource dataSource = newDataSource(); + try (Connection connection = dataSource.getConnection()) { + return dataSource; + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + @Override + public String url() { + return defaultJdbcUrl(); + } + + protected abstract String defaultJdbcUrl(); + + protected abstract DataSource newDataSource(); +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/DataSourceProvider.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/DataSourceProvider.java new file mode 100644 index 000000000..646c82c56 --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/DataSourceProvider.java @@ -0,0 +1,33 @@ +package com.vladmihalcea.util.providers; + +import io.hypersistence.utils.common.ReflectionUtils; +import org.hibernate.dialect.Dialect; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public interface DataSourceProvider { + + String hibernateDialect(); + + DataSource dataSource(); + + Class dataSourceClassName(); + + Properties dataSourceProperties(); + + String url(); + + String username(); + + String password(); + + Database database(); + + default Class hibernateDialectClass() { + return ReflectionUtils.getClass(hibernateDialect()); + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/Database.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/Database.java new file mode 100644 index 000000000..a3c0f3d3c --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/Database.java @@ -0,0 +1,84 @@ +package com.vladmihalcea.util.providers; + +import io.hypersistence.utils.common.ReflectionUtils; +import org.hibernate.dialect.Dialect; + +/** + * @author Vlad Mihalcea + */ +public enum Database { + //Mandatory databases + POSTGRESQL { + @Override + public Class dataSourceProviderClass() { + return PostgreSQLDataSourceProvider.class; + } + }, + ORACLE { + @Override + public Class dataSourceProviderClass() { + return OracleDataSourceProvider.class; + } + + @Override + protected boolean supportsDatabaseName() { + return false; + } + }, + MYSQL { + @Override + public Class dataSourceProviderClass() { + return MySQLDataSourceProvider.class; + } + }, + SQLSERVER { + @Override + public Class dataSourceProviderClass() { + return SQLServerDataSourceProvider.class; + } + + @Override + protected boolean supportsDatabaseName() { + return false; + } + + @Override + protected boolean supportsCredentials() { + return false; + } + }, + ; + + public DataSourceProvider dataSourceProvider() { + return ReflectionUtils.newInstance(dataSourceProviderClass().getName()); + } + + public abstract Class dataSourceProviderClass(); + + protected boolean supportsDatabaseName() { + return true; + } + + protected String databaseName() { + return "high-performance-java-persistence"; + } + + protected boolean supportsCredentials() { + return true; + } + + public static Database of(Dialect dialect) { + Class dialectClass = dialect.getClass(); + for (Database database : values()) { + if (database.dataSourceProvider().hibernateDialectClass().isAssignableFrom(dialectClass)) { + return database; + } + } + throw new UnsupportedOperationException( + String.format( + "The provided Dialect [%s] is not supported!", + dialectClass + ) + ); + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/MySQLDataSourceProvider.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/MySQLDataSourceProvider.java new file mode 100644 index 000000000..10668c9f7 --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/MySQLDataSourceProvider.java @@ -0,0 +1,180 @@ +package com.vladmihalcea.util.providers; + +import com.mysql.cj.jdbc.Driver; +import com.mysql.cj.jdbc.MysqlDataSource; +import org.hibernate.dialect.MySQLDialect; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class MySQLDataSourceProvider extends AbstractContainerDataSourceProvider { + + private Boolean rewriteBatchedStatements; + + private Boolean cachePrepStmts; + + private Boolean useServerPrepStmts; + + private Boolean useTimezone; + + private Boolean useJDBCCompliantTimezoneShift; + + private Boolean useLegacyDatetimeCode; + + private Boolean useCursorFetch; + + private Integer prepStmtCacheSqlLimit; + + public boolean isRewriteBatchedStatements() { + return rewriteBatchedStatements; + } + + public MySQLDataSourceProvider setRewriteBatchedStatements(boolean rewriteBatchedStatements) { + this.rewriteBatchedStatements = rewriteBatchedStatements; + return this; + } + + public boolean isCachePrepStmts() { + return cachePrepStmts; + } + + public MySQLDataSourceProvider setCachePrepStmts(boolean cachePrepStmts) { + this.cachePrepStmts = cachePrepStmts; + return this; + } + + public boolean isUseServerPrepStmts() { + return useServerPrepStmts; + } + + public MySQLDataSourceProvider setUseServerPrepStmts(boolean useServerPrepStmts) { + this.useServerPrepStmts = useServerPrepStmts; + return this; + } + + public boolean isUseTimezone() { + return useTimezone; + } + + public MySQLDataSourceProvider setUseTimezone(boolean useTimezone) { + this.useTimezone = useTimezone; + return this; + } + + public boolean isUseJDBCCompliantTimezoneShift() { + return useJDBCCompliantTimezoneShift; + } + + public MySQLDataSourceProvider setUseJDBCCompliantTimezoneShift(boolean useJDBCCompliantTimezoneShift) { + this.useJDBCCompliantTimezoneShift = useJDBCCompliantTimezoneShift; + return this; + } + + public boolean isUseLegacyDatetimeCode() { + return useLegacyDatetimeCode; + } + + public MySQLDataSourceProvider setUseLegacyDatetimeCode(boolean useLegacyDatetimeCode) { + this.useLegacyDatetimeCode = useLegacyDatetimeCode; + return this; + } + + public boolean isUseCursorFetch() { + return useCursorFetch; + } + + public MySQLDataSourceProvider setUseCursorFetch(boolean useCursorFetch) { + this.useCursorFetch = useCursorFetch; + return this; + } + + public Integer getPrepStmtCacheSqlLimit() { + return prepStmtCacheSqlLimit; + } + + public MySQLDataSourceProvider setPrepStmtCacheSqlLimit(Integer prepStmtCacheSqlLimit) { + this.prepStmtCacheSqlLimit = prepStmtCacheSqlLimit; + return this; + } + + @Override + public String hibernateDialect() { + return MySQLDialect.class.getName(); + } + + @Override + protected String defaultJdbcUrl() { + return "jdbc:mysql://localhost/high_performance_java_persistence?useSSL=false"; + } + + @Override + protected DataSource newDataSource() { + try { + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setURL(url()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + + if (rewriteBatchedStatements != null) { + dataSource.setRewriteBatchedStatements(rewriteBatchedStatements); + } + if (useCursorFetch != null) { + dataSource.setUseCursorFetch(useCursorFetch); + } + if (cachePrepStmts != null) { + dataSource.setCachePrepStmts(cachePrepStmts); + } + if (useServerPrepStmts != null) { + dataSource.setUseServerPrepStmts(useServerPrepStmts); + } + if (prepStmtCacheSqlLimit != null) { + dataSource.setPrepStmtCacheSqlLimit(prepStmtCacheSqlLimit); + } + + return dataSource; + } catch (SQLException e) { + throw new IllegalStateException("The DataSource could not be instantiated!"); + } + } + + @Override + public Class dataSourceClassName() { + return MysqlDataSource.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("url", url()); + return properties; + } + + @Override + public String username() { + return "mysql"; + } + + @Override + public String password() { + return "admin"; + } + + @Override + public Database database() { + return Database.MYSQL; + } + + @Override + public String toString() { + return "MySQLDataSourceProvider{" + + "cachePrepStmts=" + cachePrepStmts + + ", useServerPrepStmts=" + useServerPrepStmts + + ", rewriteBatchedStatements=" + rewriteBatchedStatements + + '}'; + } + +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/OracleDataSourceProvider.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/OracleDataSourceProvider.java new file mode 100644 index 000000000..87620597d --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/OracleDataSourceProvider.java @@ -0,0 +1,67 @@ +package com.vladmihalcea.util.providers; + +import oracle.jdbc.pool.OracleDataSource; +import org.hibernate.dialect.OracleDialect; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class OracleDataSourceProvider extends AbstractContainerDataSourceProvider { + + @Override + public String hibernateDialect() { + return OracleDialect.class.getName(); + } + + @Override + public String defaultJdbcUrl() { + return "jdbc:oracle:thin:@localhost:1521/xe"; + } + + @Override + public DataSource newDataSource() { + try { + OracleDataSource dataSource = new OracleDataSource(); + dataSource.setDatabaseName("high_performance_java_persistence"); + dataSource.setURL(url()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + return dataSource; + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @Override + public Class dataSourceClassName() { + return OracleDataSource.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("databaseName", "high_performance_java_persistence"); + properties.setProperty("URL", url()); + properties.setProperty("user", username()); + properties.setProperty("password", password()); + return properties; + } + + @Override + public String username() { + return "oracle"; + } + + @Override + public String password() { + return "admin"; + } + + @Override + public Database database() { + return Database.ORACLE; + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/PostgreSQLDataSourceProvider.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/PostgreSQLDataSourceProvider.java new file mode 100644 index 000000000..b3455557d --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/PostgreSQLDataSourceProvider.java @@ -0,0 +1,80 @@ +package com.vladmihalcea.util.providers; + +import org.hibernate.dialect.PostgreSQLDialect; +import org.postgresql.Driver; +import org.postgresql.ds.PGSimpleDataSource; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class PostgreSQLDataSourceProvider extends AbstractContainerDataSourceProvider { + + private Boolean reWriteBatchedInserts; + + public boolean getReWriteBatchedInserts() { + return reWriteBatchedInserts; + } + + public PostgreSQLDataSourceProvider setReWriteBatchedInserts(boolean reWriteBatchedInserts) { + this.reWriteBatchedInserts = reWriteBatchedInserts; + return this; + } + + @Override + public String hibernateDialect() { + return PostgreSQLDialect.class.getName(); + } + + @Override + protected String defaultJdbcUrl() { + return "jdbc:postgresql://localhost/high_performance_java_persistence"; + } + + protected DataSource newDataSource() { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setURL(url()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + if (reWriteBatchedInserts != null) { + dataSource.setReWriteBatchedInserts(reWriteBatchedInserts); + } + + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return PGSimpleDataSource.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("databaseName", "high_performance_java_persistence"); + properties.setProperty("serverName", "localhost"); + properties.setProperty("user", username()); + properties.setProperty("password", password()); + if (reWriteBatchedInserts != null) { + properties.setProperty("reWriteBatchedInserts", String.valueOf(reWriteBatchedInserts)); + } + return properties; + } + + @Override + public String username() { + return "postgres"; + } + + @Override + public String password() { + return "admin"; + } + + @Override + public Database database() { + return Database.POSTGRESQL; + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/SQLServerDataSourceProvider.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/SQLServerDataSourceProvider.java new file mode 100644 index 000000000..8e0ce2e76 --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/providers/SQLServerDataSourceProvider.java @@ -0,0 +1,86 @@ +package com.vladmihalcea.util.providers; + +import com.microsoft.sqlserver.jdbc.SQLServerDataSource; +import com.microsoft.sqlserver.jdbc.SQLServerDriver; +import org.hibernate.dialect.SQLServerDialect; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * @author Vlad Mihalcea + */ +public class SQLServerDataSourceProvider extends AbstractContainerDataSourceProvider { + + private boolean sendStringParametersAsUnicode = false; + + private Boolean useBulkCopyForBatchInsert; + + public boolean isSendStringParametersAsUnicode() { + return sendStringParametersAsUnicode; + } + + public SQLServerDataSourceProvider setSendStringParametersAsUnicode(boolean sendStringParametersAsUnicode) { + this.sendStringParametersAsUnicode = sendStringParametersAsUnicode; + return this; + } + + public Boolean getUseBulkCopyForBatchInsert() { + return useBulkCopyForBatchInsert; + } + + public SQLServerDataSourceProvider setUseBulkCopyForBatchInsert(Boolean useBulkCopyForBatchInsert) { + this.useBulkCopyForBatchInsert = useBulkCopyForBatchInsert; + return this; + } + + @Override + public String hibernateDialect() { + return SQLServerDialect.class.getName(); + } + + @Override + public String defaultJdbcUrl() { + return "jdbc:sqlserver://localhost;instance=SQLEXPRESS;databaseName=high_performance_java_persistence;encrypt=true;trustServerCertificate=true"; + } + + @Override + public DataSource newDataSource() { + SQLServerDataSource dataSource = new SQLServerDataSource(); + dataSource.setURL(url()); + dataSource.setUser(username()); + dataSource.setPassword(password()); + if (useBulkCopyForBatchInsert != null) { + dataSource.setUseBulkCopyForBatchInsert(useBulkCopyForBatchInsert); + } + dataSource.setSendStringParametersAsUnicode(sendStringParametersAsUnicode); + return dataSource; + } + + @Override + public Class dataSourceClassName() { + return SQLServerDataSource.class; + } + + @Override + public Properties dataSourceProperties() { + Properties properties = new Properties(); + properties.setProperty("URL", url()); + return properties; + } + + @Override + public String username() { + return "sa"; + } + + @Override + public String password() { + return "adm1n"; + } + + @Override + public Database database() { + return Database.SQLSERVER; + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/transaction/ConnectionVoidCallable.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/transaction/ConnectionVoidCallable.java new file mode 100644 index 000000000..83aaff5d0 --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/transaction/ConnectionVoidCallable.java @@ -0,0 +1,12 @@ +package com.vladmihalcea.util.transaction; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface ConnectionVoidCallable { + void execute(Connection connection) throws SQLException; +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/transaction/JPATransactionFunction.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/transaction/JPATransactionFunction.java new file mode 100644 index 000000000..91a953ec6 --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/transaction/JPATransactionFunction.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.util.transaction; + +import jakarta.persistence.EntityManager; + +import java.util.function.Function; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface JPATransactionFunction extends Function { + default void beforeTransactionCompletion() { + + } + + default void afterTransactionCompletion() { + + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/transaction/JPATransactionVoidFunction.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/transaction/JPATransactionVoidFunction.java new file mode 100644 index 000000000..e9834f036 --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/transaction/JPATransactionVoidFunction.java @@ -0,0 +1,19 @@ +package com.vladmihalcea.util.transaction; + +import jakarta.persistence.EntityManager; + +import java.util.function.Consumer; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface JPATransactionVoidFunction extends Consumer { + default void beforeTransactionCompletion() { + + } + + default void afterTransactionCompletion() { + + } +} diff --git a/jooq/jooq-core/src/main/java/com/vladmihalcea/util/transaction/VoidCallable.java b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/transaction/VoidCallable.java new file mode 100644 index 000000000..a69c9f131 --- /dev/null +++ b/jooq/jooq-core/src/main/java/com/vladmihalcea/util/transaction/VoidCallable.java @@ -0,0 +1,17 @@ +package com.vladmihalcea.util.transaction; + +import java.util.concurrent.Callable; + +/** + * @author Vlad Mihalcea + */ +@FunctionalInterface +public interface VoidCallable extends Callable { + + void execute(); + + default Void call() throws Exception { + execute(); + return null; + } +} diff --git a/jooq/jooq-core/src/test/java/com/vladmihalcea/book/hpjp/jooq/AbstractJOOQIntegrationTest.java b/jooq/jooq-core/src/test/java/com/vladmihalcea/book/hpjp/jooq/AbstractJOOQIntegrationTest.java deleted file mode 100644 index ff5aca2b2..000000000 --- a/jooq/jooq-core/src/test/java/com/vladmihalcea/book/hpjp/jooq/AbstractJOOQIntegrationTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq; - -import com.vladmihalcea.book.hpjp.util.AbstractTest; -import org.hibernate.Session; -import org.hibernate.Transaction; -import org.jooq.DSLContext; -import org.jooq.SQLDialect; -import org.jooq.conf.Settings; -import org.jooq.impl.DSL; - -import java.sql.SQLException; -import java.util.Properties; - - -/** - * @author Vlad Mihalcea - */ -public abstract class AbstractJOOQIntegrationTest extends AbstractTest { - - @Override - protected Class[] entities() { - return new Class[] { - }; - } - - protected abstract String ddlFolder(); - - protected abstract String ddlScript(); - - protected abstract SQLDialect sqlDialect(); - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.setProperty("hibernate.hbm2ddl.import_files", String.format("%s/%s", ddlFolder(), ddlScript())); - return properties; - } - - protected T doInJOOQ(DSLContextCallable callable, Settings settings) { - Session session = null; - Transaction txn = null; - try { - session = sessionFactory().openSession(); - txn = session.beginTransaction(); - T result = session.doReturningWork(connection -> { - DSLContext sql = settings != null ? - DSL.using(connection, sqlDialect(), settings) : - DSL.using(connection, sqlDialect()); - return callable.execute(sql); - }); - txn.commit(); - return result; - } catch (Throwable e) { - if ( txn != null ) txn.rollback(); - throw e; - } finally { - if (session != null) { - session.close(); - } - } - } - - protected void doInJOOQ(DSLContextVoidCallable callable, Settings settings) { - Session session = null; - Transaction txn = null; - try { - session = sessionFactory().openSession(); - txn = session.beginTransaction(); - session.doWork(connection -> { - DSLContext sql = settings != null ? - DSL.using(connection, sqlDialect(), settings) : - DSL.using(connection, sqlDialect()); - callable.execute(sql); - }); - txn.commit(); - } catch (Throwable e) { - if ( txn != null ) txn.rollback(); - throw e; - } finally { - if (session != null) { - session.close(); - } - } - } - - protected T doInJOOQ(DSLContextCallable callable) { - return doInJOOQ(callable, null); - } - - protected void doInJOOQ(DSLContextVoidCallable callable) { - doInJOOQ(callable, null); - } - - @FunctionalInterface - protected interface DSLContextCallable { - T execute(DSLContext sql) throws SQLException; - } - - @FunctionalInterface - protected interface DSLContextVoidCallable { - void execute(DSLContext sql) throws SQLException; - } -} diff --git a/jooq/jooq-mssql/pom.xml b/jooq/jooq-mssql/pom.xml deleted file mode 100644 index 1def0e7f1..000000000 --- a/jooq/jooq-mssql/pom.xml +++ /dev/null @@ -1,138 +0,0 @@ - - - - jooq - com.vladmihalcea.book - 1.0-SNAPSHOT - - 4.0.0 - - jooq-mssql - - - - org.jooq.pro - jooq - ${jooq.version} - - - - com.vladmihalcea.book - jooq-core - 1.0-SNAPSHOT - test-jar - test - - - - - - - org.codehaus.mojo - sql-maven-plugin - - - com.microsoft.sqlserver - mssql-jdbc - ${mssql.version} - - - - com.microsoft.sqlserver.jdbc.SQLServerDriver - jdbc:sqlserver://localhost;instance=SQLEXPRESS;databaseName=high_performance_java_persistence - sa - adm1n - true - continue - oracle-db-test - - - - create-test-compile-data - generate-test-sources - true - - execute - - - ascending - - ${basedir}/ - - src/test/resources/oracle/initial_schema.sql - - - true - - - - - - - org.jooq.pro - jooq-codegen-maven - ${jooq.version} - - - generate-test-sources - - generate - - - - - - com.microsoft.sqlserver - mssql-jdbc - ${mssql.version} - - - - - - com.microsoft.sqlserver.jdbc.SQLServerDriver - jdbc:sqlserver://localhost;instance=SQLEXPRESS;databaseName=high_performance_java_persistence - sa - adm1n - - - org.jooq.util.JavaGenerator - - org.jooq.util.sqlserver.SQLServerDatabase - high_performance_java_persistence\..* - - dbo - - - - com.vladmihalcea.book.hpjp.jooq.mssql.schema.crud - ${project.build.directory}/generated-sources/java - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - add-source - process-test-sources - - add-test-source - - - - ${project.build.directory}/generated-sources/java - - - - - - - - - \ No newline at end of file diff --git a/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/AbstractJOOQSQLServerSQLIntegrationTest.java b/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/AbstractJOOQSQLServerSQLIntegrationTest.java deleted file mode 100644 index f3724db0b..000000000 --- a/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/AbstractJOOQSQLServerSQLIntegrationTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.mssql.crud; - -import com.vladmihalcea.book.hpjp.jooq.AbstractJOOQIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.SQLServerDataSourceProvider; -import org.jooq.SQLDialect; - -/** - * @author Vlad Mihalcea - */ -public abstract class AbstractJOOQSQLServerSQLIntegrationTest extends AbstractJOOQIntegrationTest { - - @Override - protected String ddlFolder() { - return "mssql"; - } - - @Override - protected SQLDialect sqlDialect() { - return SQLDialect.SQLSERVER2014; - } - - protected DataSourceProvider dataSourceProvider() { - return new SQLServerDataSourceProvider(); - } -} diff --git a/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/BatchTest.java b/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/BatchTest.java deleted file mode 100644 index ac5f7094c..000000000 --- a/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/BatchTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.mssql.crud; - -import org.jooq.BatchBindStep; -import org.jooq.Record; -import org.jooq.Result; -import org.junit.Test; - -import java.math.BigInteger; - -import static com.vladmihalcea.book.hpjp.jooq.mssql.schema.crud.high_performance_java_persistence.dbo.Tables.POST; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class BatchTest extends AbstractJOOQSQLServerSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testBatching() { - doInJOOQ(sql -> { - sql.delete(POST).execute(); - BatchBindStep batch = sql.batch(sql - .insertInto(POST, POST.ID, POST.TITLE) - .values((Long) null, null) - ); - for (int i = 0; i < 3; i++) { - batch.bind(i, String.format("Post no. %d", i)); - } - int[] insertCounts = batch.execute(); - assertEquals(3, insertCounts.length); - Result posts = sql.select().from(POST).fetch(); - assertEquals(3, posts.size()); - }); - } -} diff --git a/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/CrudTest.java b/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/CrudTest.java deleted file mode 100644 index b26434c64..000000000 --- a/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/CrudTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.mssql.crud; - -import org.junit.Test; - -import static org.jooq.impl.DSL.field; -import static org.jooq.impl.DSL.table; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class CrudTest extends AbstractJOOQSQLServerSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testCrud() { - doInJOOQ(sql -> { - sql - .deleteFrom(table("post")) - .execute(); - - assertEquals(1, sql - .insertInto(table("post")).columns(field("id"), field("title")) - .values(1, "High-Performance Java Persistence") - .execute()); - - assertEquals("High-Performance Java Persistence", sql - .select(field("title")) - .from(table("post")) - .where(field("id").eq(1)) - .fetch().getValue(0, "title")); - - sql - .update(table("post")) - .set(field("title"), "High-Performance Java Persistence Book") - .where(field("id").eq(1)) - .execute(); - - assertEquals("High-Performance Java Persistence Book", sql - .select(field("title")) - .from(table("post")) - .where(field("id").eq(1)) - .fetch().getValue(0, "title")); - }); - } -} diff --git a/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/SQLInjectionTest.java b/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/SQLInjectionTest.java deleted file mode 100644 index 10f472dfd..000000000 --- a/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/SQLInjectionTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.mssql.crud; - -import java.sql.Statement; - -import org.junit.Test; - -import org.jooq.DSLContext; -import org.jooq.conf.Settings; -import org.jooq.conf.StatementType; -import org.jooq.impl.DSL; - -import static org.jooq.impl.DSL.field; -import static org.jooq.impl.DSL.table; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class SQLInjectionTest extends AbstractJOOQSQLServerSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testLiteral() { - doInJOOQ(sql -> { - sql - .deleteFrom(table("post")) - .execute(); - - assertEquals(1, sql - .insertInto(table("post")).columns(field("id"), field("title")) - .values(1L, "High-Performance Java Persistence") - .execute()); - }); - - doInJDBC(connection -> { - DSLContext sql = DSL.using( - connection, - sqlDialect(), - new Settings().withStatementType( StatementType.STATIC_STATEMENT) - ); - - //String sqlInjected = ((char)0xbf5c) + " or 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ) --'"; - String sqlInjected = ((char)0x815c) + " or 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ) --'"; - - sql - .select(field("title")) - .from(table("post")) - .where(field("title").eq( sqlInjected )) - .fetch(); - }); - } -} diff --git a/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/UpsertTest.java b/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/UpsertTest.java deleted file mode 100644 index aa7d3ad7b..000000000 --- a/jooq/jooq-mssql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mssql/crud/UpsertTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.mssql.crud; - -import org.jooq.DSLContext; -import org.junit.Test; - -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.concurrent.TimeUnit; - -import static com.vladmihalcea.book.hpjp.jooq.mssql.schema.crud.high_performance_java_persistence.dbo.Tables.POST; -import static com.vladmihalcea.book.hpjp.jooq.mssql.schema.crud.high_performance_java_persistence.dbo.Tables.POST_DETAILS; - -/** - * @author Vlad Mihalcea - */ -public class UpsertTest extends AbstractJOOQSQLServerSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testUpsert() { - doInJOOQ(sql -> { - sql.delete(POST_DETAILS).execute(); - sql.delete(POST).execute(); - sql - .insertInto(POST).columns(POST.ID, POST.TITLE) - .values(1L, "High-Performance Java Persistence") - .execute(); - - executeAsync(() -> { - upsertPostDetails(sql, 1L, "Alice", - Timestamp.from(LocalDateTime.now().toInstant(ZoneOffset.UTC))); - }); - executeAsync(() -> { - upsertPostDetails(sql, 1L, "Bob", - Timestamp.from(LocalDateTime.now().toInstant(ZoneOffset.UTC))); - }); - - awaitTermination(1, TimeUnit.SECONDS); - }); - } - - private void upsertPostDetails(DSLContext sql, Long id, String owner, Timestamp timestamp) { - sql - .insertInto(POST_DETAILS) - .columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON) - .values(id, owner, timestamp) - .onDuplicateKeyUpdate() - .set(POST_DETAILS.UPDATED_BY, owner) - .set(POST_DETAILS.UPDATED_ON, timestamp) - .execute(); - } -} diff --git a/jooq/jooq-mssql/src/test/resources/oracle/initial_schema.sql b/jooq/jooq-mssql/src/test/resources/oracle/initial_schema.sql deleted file mode 100644 index 70487851b..000000000 --- a/jooq/jooq-mssql/src/test/resources/oracle/initial_schema.sql +++ /dev/null @@ -1,19 +0,0 @@ -alter table post_comment drop constraint FKna4y825fdc5hw8aow65ijexm0; -alter table post_details drop constraint FKkl5eik513p1xiudk2kxb0v92u; -alter table post_tag drop constraint FKac1wdchd2pnur3fl225obmlg0; -alter table post_tag drop constraint FKc2auetuvsec0k566l0eyvr9cs; -drop table post; -drop table post_comment; -drop table post_details; -drop table post_tag; -drop table tag; - -create table post (id bigint not null, title varchar(255), primary key (id)); -create table post_comment (id bigint not null, review varchar(255), post_id bigint, primary key (id)); -create table post_details (id bigint not null, created_by varchar(255), created_on datetime2, updated_by varchar(255), updated_on datetime2, primary key (id)); -create table post_tag (post_id bigint not null, tag_id bigint not null); -create table tag (id bigint not null, name varchar(255), primary key (id)); -alter table post_comment add constraint FKna4y825fdc5hw8aow65ijexm0 foreign key (post_id) references post; -alter table post_details add constraint FKkl5eik513p1xiudk2kxb0v92u foreign key (id) references post; -alter table post_tag add constraint FKac1wdchd2pnur3fl225obmlg0 foreign key (tag_id) references tag; -alter table post_tag add constraint FKc2auetuvsec0k566l0eyvr9cs foreign key (post_id) references post; diff --git a/jooq/jooq-mysql/pom.xml b/jooq/jooq-mysql/pom.xml index 38218c278..161a17923 100644 --- a/jooq/jooq-mysql/pom.xml +++ b/jooq/jooq-mysql/pom.xml @@ -3,13 +3,13 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0%20http://maven.apache.org/xsd/maven-4.0.0.xsd"> - jooq - com.vladmihalcea.book + high-performance-java-persistence-jooq + com.vladmihalcea 1.0-SNAPSHOT 4.0.0 - jooq-mysql + high-performance-java-persistence-jooq-mysql @@ -19,11 +19,9 @@ - com.vladmihalcea.book - jooq-core - 1.0-SNAPSHOT - test-jar - test + com.vladmihalcea + high-performance-java-persistence-jooq-core + ${project.parent.version} @@ -34,8 +32,8 @@ sql-maven-plugin - mysql - mysql-connector-java + com.mysql + mysql-connector-j ${mysql.version} @@ -83,8 +81,8 @@ - mysql - mysql-connector-java + com.mysql + mysql-connector-j ${mysql.version} @@ -97,16 +95,15 @@ admin - org.jooq.util.JavaGenerator - org.jooq.util.mysql.MySQLDatabase + org.jooq.meta.mysql.MySQLDatabase .* high_performance_java_persistence - com.vladmihalcea.book.hpjp.jooq.mysql.schema.crud + com.vladmihalcea.hpjp.jooq.mysql.schema.crud ${project.build.directory}/generated-sources/java diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/AbstractJOOQMySQLIntegrationTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/AbstractJOOQMySQLIntegrationTest.java deleted file mode 100644 index 851a2dbf2..000000000 --- a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/AbstractJOOQMySQLIntegrationTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.mysql.crud; - -import com.vladmihalcea.book.hpjp.jooq.AbstractJOOQIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.MySQLDataSourceProvider; -import org.jooq.SQLDialect; - -/** - * @author Vlad Mihalcea - */ -public abstract class AbstractJOOQMySQLIntegrationTest extends AbstractJOOQIntegrationTest { - - @Override - protected String ddlFolder() { - return "mysql"; - } - - @Override - protected SQLDialect sqlDialect() { - return SQLDialect.MYSQL; - } - - protected DataSourceProvider dataSourceProvider() { - return new MySQLDataSourceProvider(); - } -} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/BatchTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/BatchTest.java deleted file mode 100644 index 9ab8929b9..000000000 --- a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/BatchTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.mysql.crud; - -import org.jooq.BatchBindStep; -import org.jooq.Record; -import org.jooq.Result; -import org.junit.Ignore; -import org.junit.Test; - -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static com.vladmihalcea.book.hpjp.jooq.mysql.schema.crud.Tables.POST; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class BatchTest extends AbstractJOOQMySQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testBatching() { - doInJOOQ(sql -> { - sql.delete(POST).execute(); - BatchBindStep batch = sql.batch(sql - .insertInto(POST, POST.TITLE) - .values("?") - ); - for (int i = 0; i < 3; i++) { - batch.bind(String.format("Post no. %d", i)); - } - int[] insertCounts = batch.execute(); - assertEquals(3, insertCounts.length); - Result posts = sql.select().from(POST).fetch(); - assertEquals(3, posts.size()); - }); - } - - @Test - public void testBatchingReturning() { - doInJOOQ(sql -> { - sql.delete(POST).execute(); - BatchBindStep batch = sql.batch(sql - .insertInto(POST, POST.TITLE) - .values("?") - ); - for (int i = 0; i < 3; i++) { - batch.bind(String.format("Post no. %d", i)); - } - int[] insertCounts = batch.execute(); - assertEquals(3, insertCounts.length); - Result posts = sql.select().from(POST).fetch(); - assertEquals(3, posts.size()); - }); - } - - @Test @Ignore("values(Collection) is not INSERT INTO ... VALUES ( (..) (..) (..) )") - public void testBatchingWithCollection() { - doInJOOQ(sql -> { - sql.delete(POST).execute(); - - int insertCount = sql - .insertInto(POST, POST.TITLE) - .values(IntStream.range(1, 3).boxed() - .map(i -> String.format("Post no. %d", i)) - .collect(Collectors.toList())) - .execute(); - assertEquals(3, insertCount); - Result posts = sql.select().from(POST).fetch(); - assertEquals(3, posts.size()); - }); - } -} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/CrudTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/CrudTest.java deleted file mode 100644 index dfe193876..000000000 --- a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/CrudTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.mysql.crud; - -import org.junit.Test; - -import static com.vladmihalcea.book.hpjp.jooq.mysql.schema.crud.Tables.POST; -import static org.jooq.impl.DSL.field; -import static org.jooq.impl.DSL.table; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class CrudTest extends AbstractJOOQMySQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testCrud() { - doInJOOQ(sql -> { - sql - .deleteFrom(table("post")) - .execute(); - - assertEquals(1, sql - .insertInto(table("post")).columns(field("id"), field("title")) - .values(1L, "High-Performance Java Persistence") - .execute()); - - assertEquals("High-Performance Java Persistence", sql - .select(field("title")) - .from(table("post")) - .where(field("id").eq(1)) - .fetch().getValue(0, "title")); - - sql - .update(table("post")) - .set(field("title"), "High-Performance Java Persistence Book") - .where(field("id").eq(1)) - .execute(); - - assertEquals("High-Performance Java Persistence Book", sql - .select(field("title")) - .from(table("post")) - .where(field("id").eq(1)) - .fetch().getValue(0, "title") - ); - }); - } - - @Test - public void testCrudJavaSchema() { - doInJOOQ(sql -> { - sql - .deleteFrom(POST) - .execute(); - - assertEquals(1, sql - .insertInto(POST).columns(POST.ID, POST.TITLE) - .values(1L, "High-Performance Java Persistence") - .execute() - ); - - assertEquals("High-Performance Java Persistence", sql - .select(POST.TITLE) - .from(POST) - .where(POST.ID.eq(1L)) - .fetch().getValue(0, POST.TITLE) - ); - - sql - .update(POST) - .set(POST.TITLE, "High-Performance Java Persistence Book") - .where(POST.ID.eq(1L)) - .execute(); - - assertEquals("High-Performance Java Persistence Book", sql - .select(POST.TITLE) - .from(POST) - .where(POST.ID.eq(1L)) - .fetch().getValue(0, POST.TITLE) - ); - }); - } -} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/HibernateFlushTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/HibernateFlushTest.java deleted file mode 100644 index bcbbdd11b..000000000 --- a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/HibernateFlushTest.java +++ /dev/null @@ -1,259 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.mysql.crud; - -import org.hibernate.Criteria; -import org.hibernate.Session; -import org.hibernate.criterion.Restrictions; -import org.junit.Test; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Properties; - -/** - * @author Vlad Mihalcea - */ -public class HibernateFlushTest extends AbstractJOOQMySQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostDetails.class, - PostComment.class, - Tag.class - }; - } - - @Override - protected String ddlScript() { - return null; - } - - @Override - protected Properties properties() { - Properties properties = super.properties(); - properties.remove("hibernate.hbm2ddl.import_files"); - return properties; - } - - @Test - public void test() { - doInJPA(entityManager -> { - Post post = new Post(1L); - post.title = "Postit"; - - PostComment comment1 = new PostComment(); - comment1.id = 1L; - comment1.review = "Good"; - - PostComment comment2 = new PostComment(); - comment2.id = 2L; - comment2.review = "Excellent"; - - post.addComment(comment1); - post.addComment(comment2); - entityManager.persist(post); - - Session session = entityManager.unwrap(Session.class); - Criteria criteria = session.createCriteria(Post.class) - .add(Restrictions.eq("title", "post")); - LOGGER.info("Criteria: {}", criteria); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Post() {} - - public Post(Long id) { - this.id = id; - } - - public Post(String title) { - this.title = title; - } - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true) - private List comments = new ArrayList<>(); - - @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", - orphanRemoval = true, fetch = FetchType.LAZY) - private PostDetails details; - - @ManyToMany - @JoinTable(name = "post_tag", - joinColumns = @JoinColumn(name = "post_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id") - ) - private List tags = new ArrayList<>(); - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getComments() { - return comments; - } - - public PostDetails getDetails() { - return details; - } - - public List getTags() { - return tags; - } - - public void addComment(PostComment comment) { - comments.add(comment); - comment.setPost(this); - } - - public void addDetails(PostDetails details) { - this.details = details; - details.setPost(this); - } - - public void removeDetails() { - this.details.setPost(null); - this.details = null; - } - } - - @Entity(name = "PostDetails") - @Table(name = "post_details") - public static class PostDetails { - - @Id - private Long id; - - @Column(name = "created_on") - private Date createdOn; - - @Column(name = "created_by") - private String createdBy; - - public PostDetails() { - createdOn = new Date(); - } - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "id") - @MapsId - private Post post; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - public static class PostComment { - - @Id - private Long id; - - @ManyToOne - private Post post; - - private String review; - - public PostComment() {} - - public PostComment(String review) { - this.review = review; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - } - - @Entity(name = "Tag") - @Table(name = "tag") - public static class Tag { - - @Id - private Long id; - - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } -} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/KeysetPaginationTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/KeysetPaginationTest.java deleted file mode 100644 index 7b2dc446d..000000000 --- a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/KeysetPaginationTest.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.mysql.crud; - -import org.jooq.Record3; -import org.jooq.SelectSeekStep2; -import org.junit.Test; - -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.List; - -import static com.vladmihalcea.book.hpjp.jooq.mysql.schema.crud.Tables.POST; -import static com.vladmihalcea.book.hpjp.jooq.mysql.schema.crud.Tables.POST_DETAILS; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class KeysetPaginationTest extends AbstractJOOQMySQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testPagination() { - String user = "Vlad Mihalcea"; - - doInJOOQ(sql -> { - sql - .deleteFrom(POST_DETAILS) - .execute(); - - sql - .deleteFrom(POST) - .execute(); - - LocalDateTime now = LocalDateTime.now(); - - for (long i = 1; i < 100; i++) { - sql - .insertInto(POST).columns(POST.ID, POST.TITLE) - .values(i, String.format("High-Performance Java Persistence - Chapter %d", i)) - .execute(); - - sql - .insertInto(POST_DETAILS).columns(POST_DETAILS.ID, POST_DETAILS.CREATED_ON, POST_DETAILS.CREATED_BY) - .values(i, Timestamp.valueOf(now.plusHours(i / 10)), user) - .execute(); - } - }); - - doInJOOQ(sql -> { - - int pageSize = 5; - - List results = nextPage(pageSize, null); - - assertEquals(5, results.size()); - - PostSummary offsetPostSummary = results.get(results.size() - 1); - - results = nextPage(pageSize, offsetPostSummary); - - assertEquals(5, results.size()); - }); - - doInJOOQ(sql -> { - - int pageSize = 5; - - PostSummary offsetPostSummary = null; - - int pageCount = 0; - - while (true) { - List results = nextPage(pageSize, offsetPostSummary); - if(results.isEmpty()) { - break; - } - - offsetPostSummary = results.get(results.size() - 1); - pageCount++; - } - - assertEquals(Long.valueOf(1), offsetPostSummary.getId()); - assertEquals(20, pageCount); - }); - } - - public List nextPage(int pageSize, PostSummary offsetPostSummary) { - return doInJOOQ(sql -> { - SelectSeekStep2, Timestamp, Long> selectStep = sql - .select(POST.ID, POST.TITLE, POST_DETAILS.CREATED_ON) - .from(POST) - .join(POST_DETAILS).using(POST.ID) - .orderBy(POST_DETAILS.CREATED_ON.desc(), POST.ID.desc()); - - return (offsetPostSummary != null) - ? selectStep - .seek(offsetPostSummary.getCreatedOn(), offsetPostSummary.getId()) - .limit(pageSize) - .fetchInto(PostSummary.class) - : selectStep - .limit(pageSize) - .fetchInto(PostSummary.class); - }); - } - - /** - * @author Vlad Mihalcea - */ - public static class PostSummary { - - private final Long id; - - private final String title; - - private final Timestamp createdOn; - - public PostSummary(Long id, String title, Timestamp createdOn) { - this.id = id; - this.title = title; - this.createdOn = createdOn; - } - - public Long getId() { - return id; - } - - public String getTitle() { - return title; - } - - public Timestamp getCreatedOn() { - return createdOn; - } - } -} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/SQLInjectionTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/SQLInjectionTest.java deleted file mode 100644 index e3f026020..000000000 --- a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/SQLInjectionTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.mysql.crud; - -import java.sql.Statement; - -import org.junit.Test; - -import org.jooq.DSLContext; -import org.jooq.conf.Settings; -import org.jooq.conf.StatementType; -import org.jooq.impl.DSL; - -import static org.jooq.impl.DSL.field; -import static org.jooq.impl.DSL.table; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class SQLInjectionTest extends AbstractJOOQMySQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testLiteral() { - doInJOOQ(sql -> { - sql - .deleteFrom(table("post")) - .execute(); - - assertEquals(1, sql - .insertInto(table("post")).columns(field("id"), field("title")) - .values(1L, "High-Performance Java Persistence") - .execute()); - }); - - doInJDBC(connection -> { - try(Statement st = connection.createStatement()) { - st.execute( "alter table post convert to character set sjis collate sjis_bin;" ); - st.execute( "SET NAMES 'sjis';" ); - - /*st.execute( "alter table post convert to character set gbk collate gbk_bin;" ); - st.execute( "SET character_set_client = 'gbk';" ); - st.execute( "SET NAMES 'gbk';" );*/ - - /*st.execute( "alter table post convert to character set cp932 collate cp932_japanese_ci ;" ); - st.execute( "SET character_set_client = 'cp932';" ); - st.execute( "SET NAMES 'cp932';" );*/ - } - - DSLContext sql = DSL.using( - connection, - sqlDialect(), - new Settings().withStatementType( StatementType.STATIC_STATEMENT) - ); - - //String sqlInjected = ((char)0xbf5c) + " or 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ) --'"; - String sqlInjected = ((char)0x815c) + " or 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ) --'"; - - sql - .select(field("title")) - .from(table("post")) - .where(field("title").eq( sqlInjected )) - .fetch(); - }); - } -} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/StreamTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/StreamTest.java deleted file mode 100644 index 20442a59e..000000000 --- a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/StreamTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.mysql.crud; - -import org.junit.Before; -import org.junit.Test; - -import static com.vladmihalcea.book.hpjp.jooq.mysql.schema.crud.Tables.POST; -import static com.vladmihalcea.book.hpjp.jooq.mysql.schema.crud.Tables.POST_COMMENT_DETAILS; - -/** - * @author Vlad Mihalcea - */ -public class StreamTest extends AbstractJOOQMySQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Before - public void init() { - super.init(); - - doInJOOQ(sql -> { - sql - .deleteFrom(POST) - .execute(); - - long id = 0L; - - sql - .insertInto( - POST_COMMENT_DETAILS).columns( - POST_COMMENT_DETAILS.ID, - POST_COMMENT_DETAILS.POST_ID, - POST_COMMENT_DETAILS.USER_ID, - POST_COMMENT_DETAILS.IP, - POST_COMMENT_DETAILS.FINGERPRINT - ) - .values(++id, 1L, 1L, "192.168.0.2", "ABC123") - .values(++id, 1L, 2L, "192.168.0.3", "ABC456") - .values(++id, 1L, 3L, "192.168.0.4", "ABC789") - .values(++id, 2L, 1L, "192.168.0.2", "ABC123") - .values(++id, 2L, 2L, "192.168.0.3", "ABC456") - .values(++id, 2L, 4L, "192.168.0.3", "ABC456") - .values(++id, 2L, 5L, "192.168.0.3", "ABC456") - .execute(); - }); - } - - @Test - public void testStream() { - doInJOOQ(sql -> { - - - }); - } -} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/UpsertTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/UpsertTest.java deleted file mode 100644 index 8e4b8c90d..000000000 --- a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/UpsertTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.mysql.crud; - -import org.jooq.DSLContext; -import org.junit.Test; - -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.concurrent.TimeUnit; - -import static com.vladmihalcea.book.hpjp.jooq.mysql.schema.crud.Tables.POST; -import static com.vladmihalcea.book.hpjp.jooq.mysql.schema.crud.tables.PostDetails.POST_DETAILS; - -/** - * @author Vlad Mihalcea - */ -public class UpsertTest extends AbstractJOOQMySQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testUpsert() { - doInJOOQ(sql -> { - sql.delete(POST_DETAILS).execute(); - sql.delete(POST).execute(); - sql - .insertInto(POST).columns(POST.ID, POST.TITLE) - .values(1L, "High-Performance Java Persistence") - .execute(); - - executeAsync(() -> { - upsertPostDetails(sql, 1L, "Alice", - Timestamp.from(LocalDateTime.now().toInstant(ZoneOffset.UTC))); - }); - executeAsync(() -> { - upsertPostDetails(sql, 1L, "Bob", - Timestamp.from(LocalDateTime.now().toInstant(ZoneOffset.UTC))); - }); - - awaitTermination(1, TimeUnit.SECONDS); - }); - } - - private void upsertPostDetails(DSLContext sql, Long id, String owner, Timestamp timestamp) { - sql - .insertInto(POST_DETAILS) - .columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON) - .values(id, owner, timestamp) - .onDuplicateKeyUpdate() - .set(POST_DETAILS.UPDATED_BY, owner) - .set(POST_DETAILS.UPDATED_ON, timestamp) - .execute(); - } -} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/AbstractJOOQMySQLIntegrationTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/AbstractJOOQMySQLIntegrationTest.java new file mode 100644 index 000000000..a94a66027 --- /dev/null +++ b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/AbstractJOOQMySQLIntegrationTest.java @@ -0,0 +1,26 @@ +package com.vladmihalcea.hpjp.jooq.mysql.crud; + +import com.vladmihalcea.hpjp.jooq.AbstractJOOQIntegrationTest; +import com.vladmihalcea.util.providers.DataSourceProvider; +import com.vladmihalcea.util.providers.MySQLDataSourceProvider; +import org.jooq.SQLDialect; + +/** + * @author Vlad Mihalcea + */ +public abstract class AbstractJOOQMySQLIntegrationTest extends AbstractJOOQIntegrationTest { + + @Override + protected String ddlFolder() { + return "mysql"; + } + + @Override + protected SQLDialect sqlDialect() { + return SQLDialect.MYSQL; + } + + protected DataSourceProvider dataSourceProvider() { + return new MySQLDataSourceProvider(); + } +} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/BatchTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/BatchTest.java new file mode 100644 index 000000000..27d535e15 --- /dev/null +++ b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/BatchTest.java @@ -0,0 +1,77 @@ +package com.vladmihalcea.hpjp.jooq.mysql.crud; + +import org.jooq.BatchBindStep; +import org.jooq.Record; +import org.jooq.Result; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static com.vladmihalcea.hpjp.jooq.mysql.schema.crud.Tables.POST; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BatchTest extends AbstractJOOQMySQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Test + public void testBatching() { + doInJOOQ(sql -> { + sql.delete(POST).execute(); + BatchBindStep batch = sql.batch(sql + .insertInto(POST, POST.TITLE) + .values("?") + ); + for (int i = 0; i < 3; i++) { + batch.bind(String.format("Post no. %d", i)); + } + int[] insertCounts = batch.execute(); + assertEquals(3, insertCounts.length); + Result posts = sql.select().from(POST).fetch(); + assertEquals(3, posts.size()); + }); + } + + @Test + public void testBatchingReturning() { + doInJOOQ(sql -> { + sql.delete(POST).execute(); + BatchBindStep batch = sql.batch(sql + .insertInto(POST, POST.TITLE) + .values("?") + ); + for (int i = 0; i < 3; i++) { + batch.bind(String.format("Post no. %d", i)); + } + int[] insertCounts = batch.execute(); + assertEquals(3, insertCounts.length); + Result posts = sql.select().from(POST).fetch(); + assertEquals(3, posts.size()); + }); + } + + @Test @Ignore("values(Collection) is not INSERT INTO ... VALUES ( (..) (..) (..) )") + public void testBatchingWithCollection() { + doInJOOQ(sql -> { + sql.delete(POST).execute(); + + int insertCount = sql + .insertInto(POST, POST.TITLE) + .values(IntStream.range(1, 3).boxed() + .map(i -> String.format("Post no. %d", i)) + .collect(Collectors.toList())) + .execute(); + assertEquals(3, insertCount); + Result posts = sql.select().from(POST).fetch(); + assertEquals(3, posts.size()); + }); + } +} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/CrudTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/CrudTest.java new file mode 100644 index 000000000..c135aeaee --- /dev/null +++ b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/CrudTest.java @@ -0,0 +1,87 @@ +package com.vladmihalcea.hpjp.jooq.mysql.crud; + +import org.junit.Test; + +import static com.vladmihalcea.hpjp.jooq.mysql.schema.crud.Tables.POST; +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.table; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class CrudTest extends AbstractJOOQMySQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Test + public void testCrud() { + doInJOOQ(sql -> { + sql + .deleteFrom(table("post")) + .execute(); + + assertEquals(1, sql + .insertInto(table("post")).columns(field("id"), field("title")) + .values(1L, "High-Performance Java Persistence") + .execute()); + + assertEquals("High-Performance Java Persistence", sql + .select(field("title")) + .from(table("post")) + .where(field("id").eq(1)) + .fetch().getValue(0, "title")); + + sql + .update(table("post")) + .set(field("title"), "High-Performance Java Persistence Book") + .where(field("id").eq(1)) + .execute(); + + assertEquals("High-Performance Java Persistence Book", sql + .select(field("title")) + .from(table("post")) + .where(field("id").eq(1)) + .fetch().getValue(0, "title") + ); + }); + } + + @Test + public void testCrudJavaSchema() { + doInJOOQ(sql -> { + sql + .deleteFrom(POST) + .execute(); + + assertEquals(1, sql + .insertInto(POST).columns(POST.ID, POST.TITLE) + .values(1L, "High-Performance Java Persistence") + .execute() + ); + + assertEquals("High-Performance Java Persistence", sql + .select(POST.TITLE) + .from(POST) + .where(POST.ID.eq(1L)) + .fetch().getValue(0, POST.TITLE) + ); + + sql + .update(POST) + .set(POST.TITLE, "High-Performance Java Persistence Book") + .where(POST.ID.eq(1L)) + .execute(); + + assertEquals("High-Performance Java Persistence Book", sql + .select(POST.TITLE) + .from(POST) + .where(POST.ID.eq(1L)) + .fetch().getValue(0, POST.TITLE) + ); + }); + } +} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/HibernateFlushTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/HibernateFlushTest.java new file mode 100644 index 000000000..d6bed1fe2 --- /dev/null +++ b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/HibernateFlushTest.java @@ -0,0 +1,138 @@ +package com.vladmihalcea.hpjp.jooq.mysql.crud; + +import jakarta.persistence.*; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class HibernateFlushTest extends AbstractJOOQMySQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + }; + } + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Test + public void test() { + doInJPA(entityManager -> { + Post post = new Post(1L); + post.title = "Postit"; + + PostComment comment1 = new PostComment(); + comment1.id = 1L; + comment1.review = "Good"; + + PostComment comment2 = new PostComment(); + comment2.id = 2L; + comment2.review = "Excellent"; + + post.addComment(comment1); + post.addComment(comment2); + entityManager.persist(post); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Post() {} + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", + orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getComments() { + return comments; + } + + public void addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + @ManyToOne + private Post post; + + private String review; + + public PostComment() {} + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } +} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/InlineBindParametersTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/InlineBindParametersTest.java similarity index 89% rename from jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/InlineBindParametersTest.java rename to jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/InlineBindParametersTest.java index 1ad82694a..dfb7bbe43 100644 --- a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/book/hpjp/jooq/mysql/crud/InlineBindParametersTest.java +++ b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/InlineBindParametersTest.java @@ -1,4 +1,4 @@ -package com.vladmihalcea.book.hpjp.jooq.mysql.crud; +package com.vladmihalcea.hpjp.jooq.mysql.crud; import org.jooq.DSLContext; import org.jooq.conf.Settings; @@ -8,7 +8,7 @@ import java.util.List; -import static com.vladmihalcea.book.hpjp.jooq.mysql.schema.crud.Tables.POST; +import static com.vladmihalcea.hpjp.jooq.mysql.schema.crud.Tables.POST; import static org.jooq.impl.DSL.field; import static org.jooq.impl.DSL.table; import static org.junit.Assert.assertEquals; @@ -20,7 +20,7 @@ public class InlineBindParametersTest extends AbstractJOOQMySQLIntegrationTest { @Override protected String ddlScript() { - return "initial_schema.sql"; + return "clean_schema.sql"; } @Test diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/KeysetPaginationTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/KeysetPaginationTest.java new file mode 100644 index 000000000..a77b3ba87 --- /dev/null +++ b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/KeysetPaginationTest.java @@ -0,0 +1,138 @@ +package com.vladmihalcea.hpjp.jooq.mysql.crud; + +import org.jooq.Record3; +import org.jooq.SelectSeekStep2; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.vladmihalcea.hpjp.jooq.mysql.schema.crud.Tables.POST; +import static com.vladmihalcea.hpjp.jooq.mysql.schema.crud.Tables.POST_DETAILS; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class KeysetPaginationTest extends AbstractJOOQMySQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Test + public void testPagination() { + String user = "Vlad Mihalcea"; + + doInJOOQ(sql -> { + sql + .deleteFrom(POST_DETAILS) + .execute(); + + sql + .deleteFrom(POST) + .execute(); + + LocalDateTime now = LocalDateTime.now(); + + for (long i = 1; i < 100; i++) { + sql + .insertInto(POST).columns(POST.ID, POST.TITLE) + .values(i, String.format("High-Performance Java Persistence - Chapter %d", i)) + .execute(); + + sql + .insertInto(POST_DETAILS).columns(POST_DETAILS.ID, POST_DETAILS.CREATED_ON, POST_DETAILS.CREATED_BY) + .values(i, now.plusHours(i / 10), user) + .execute(); + } + }); + + doInJOOQ(sql -> { + + int pageSize = 5; + + List results = nextPage(pageSize, null); + + assertEquals(5, results.size()); + + PostSummary offsetPostSummary = results.get(results.size() - 1); + + results = nextPage(pageSize, offsetPostSummary); + + assertEquals(5, results.size()); + }); + + doInJOOQ(sql -> { + + int pageSize = 5; + + PostSummary offsetPostSummary = null; + + int pageCount = 0; + + while (true) { + List results = nextPage(pageSize, offsetPostSummary); + if(results.isEmpty()) { + break; + } + + offsetPostSummary = results.get(results.size() - 1); + pageCount++; + } + + assertEquals(Long.valueOf(1), offsetPostSummary.getId()); + assertEquals(20, pageCount); + }); + } + + public List nextPage(int pageSize, PostSummary offsetPostSummary) { + return doInJOOQ(sql -> { + SelectSeekStep2, LocalDateTime, Long> selectStep = sql + .select(POST.ID, POST.TITLE, POST_DETAILS.CREATED_ON) + .from(POST) + .join(POST_DETAILS).using(POST.ID) + .orderBy(POST_DETAILS.CREATED_ON.desc(), POST.ID.desc()); + + return (offsetPostSummary != null) + ? selectStep + .seek(offsetPostSummary.getCreatedOn(), offsetPostSummary.getId()) + .limit(pageSize) + .fetchInto(PostSummary.class) + : selectStep + .limit(pageSize) + .fetchInto(PostSummary.class); + }); + } + + /** + * @author Vlad Mihalcea + */ + public static class PostSummary { + + private final Long id; + + private final String title; + + private final LocalDateTime createdOn; + + public PostSummary(Long id, String title, LocalDateTime createdOn) { + this.id = id; + this.title = title; + this.createdOn = createdOn; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + } +} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/SQLInjectionTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/SQLInjectionTest.java new file mode 100644 index 000000000..9ec78fc18 --- /dev/null +++ b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/SQLInjectionTest.java @@ -0,0 +1,69 @@ +package com.vladmihalcea.hpjp.jooq.mysql.crud; + +import java.sql.Statement; + +import org.junit.Test; + +import org.jooq.DSLContext; +import org.jooq.conf.Settings; +import org.jooq.conf.StatementType; +import org.jooq.impl.DSL; + +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.table; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SQLInjectionTest extends AbstractJOOQMySQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Test + public void testLiteral() { + doInJOOQ(sql -> { + sql + .deleteFrom(table("post")) + .execute(); + + assertEquals(1, sql + .insertInto(table("post")).columns(field("id"), field("title")) + .values(1L, "High-Performance Java Persistence") + .execute()); + }); + + doInJDBC(connection -> { + try(Statement st = connection.createStatement()) { + st.execute( "alter table post convert to character set sjis collate sjis_bin;" ); + st.execute( "SET NAMES 'sjis';" ); + + /*st.execute( "alter table post convert to character set gbk collate gbk_bin;" ); + st.execute( "SET character_set_client = 'gbk';" ); + st.execute( "SET NAMES 'gbk';" );*/ + + /*st.execute( "alter table post convert to character set cp932 collate cp932_japanese_ci ;" ); + st.execute( "SET character_set_client = 'cp932';" ); + st.execute( "SET NAMES 'cp932';" );*/ + } + + DSLContext sql = DSL.using( + connection, + sqlDialect(), + new Settings().withStatementType( StatementType.STATIC_STATEMENT) + ); + + //String sqlInjected = ((char)0xbf5c) + " or 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ) --'"; + String sqlInjected = ((char)0x815c) + " or 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ) --'"; + + sql + .select(field("title")) + .from(table("post")) + .where(field("title").eq( sqlInjected )) + .fetch(); + }); + } +} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/StreamTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/StreamTest.java new file mode 100644 index 000000000..84768920e --- /dev/null +++ b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/StreamTest.java @@ -0,0 +1,57 @@ +package com.vladmihalcea.hpjp.jooq.mysql.crud; + +import org.junit.Before; +import org.junit.Test; + +import static com.vladmihalcea.hpjp.jooq.mysql.schema.crud.Tables.POST; +import static com.vladmihalcea.hpjp.jooq.mysql.schema.crud.Tables.POST_COMMENT_DETAILS; + +/** + * @author Vlad Mihalcea + */ +public class StreamTest extends AbstractJOOQMySQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Before + public void init() { + super.init(); + + doInJOOQ(sql -> { + sql + .deleteFrom(POST) + .execute(); + + long id = 0L; + + sql + .insertInto( + POST_COMMENT_DETAILS).columns( + POST_COMMENT_DETAILS.ID, + POST_COMMENT_DETAILS.POST_ID, + POST_COMMENT_DETAILS.USER_ID, + POST_COMMENT_DETAILS.IP, + POST_COMMENT_DETAILS.FINGERPRINT + ) + .values(++id, 1L, 1L, "192.168.0.2", "ABC123") + .values(++id, 1L, 2L, "192.168.0.3", "ABC456") + .values(++id, 1L, 3L, "192.168.0.4", "ABC789") + .values(++id, 2L, 1L, "192.168.0.2", "ABC123") + .values(++id, 2L, 2L, "192.168.0.3", "ABC456") + .values(++id, 2L, 4L, "192.168.0.3", "ABC456") + .values(++id, 2L, 5L, "192.168.0.3", "ABC456") + .execute(); + }); + } + + @Test + public void testStream() { + doInJOOQ(sql -> { + + + }); + } +} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/UpsertAndGetConcurrencyTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/UpsertAndGetConcurrencyTest.java new file mode 100644 index 000000000..cef8f4f65 --- /dev/null +++ b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/UpsertAndGetConcurrencyTest.java @@ -0,0 +1,99 @@ +package com.vladmihalcea.hpjp.jooq.mysql.crud; + +import com.vladmihalcea.hpjp.jooq.mysql.schema.crud.tables.records.PostDetailsRecord; +import com.vladmihalcea.hpjp.jooq.mysql.schema.crud.tables.records.PostRecord; +import com.vladmihalcea.util.exception.ExceptionUtil; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.vladmihalcea.hpjp.jooq.mysql.schema.crud.Tables.POST; +import static com.vladmihalcea.hpjp.jooq.mysql.schema.crud.tables.PostDetails.POST_DETAILS; +import static org.jooq.impl.DSL.val; +import static org.jooq.impl.DSL.field; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class UpsertAndGetConcurrencyTest extends AbstractJOOQMySQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + private final CountDownLatch aliceLatch = new CountDownLatch(1); + + @Test(timeout = 3000) + public void testUpsert() { + doInJOOQ(sql -> { + sql.delete(POST_DETAILS).execute(); + sql.delete(POST).execute(); + + PostRecord postRecord = sql + .insertInto(POST).columns(POST.TITLE) + .values(val("High-Performance Java Persistence")) + .returning(POST.ID) + .fetchOne(); + + final Long postId = postRecord.getId(); + + sql + .insertInto(POST_DETAILS) + .columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON) + .values(postId, "Alice", LocalDateTime.now()) + .onDuplicateKeyIgnore() + .execute(); + + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + + executeAsync(() -> { + try { + doInJOOQ(_sql -> { + Connection connection = _sql.configuration().connectionProvider().acquire(); + setJdbcTimeout(connection); + + Thread shutdownThread = new Thread(() -> { + sleep(1500); + try { + if (!connection.isClosed()) { + connection.close(); + } + preventedByLocking.set( true ); + aliceLatch.countDown(); + } catch (SQLException ignore) {} + }); + shutdownThread.setDaemon(true); + shutdownThread.start(); + + _sql + .insertInto(POST_DETAILS) + .columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON) + .values(postId, "Bob", LocalDateTime.now()) + .onDuplicateKeyIgnore() + .execute(); + }); + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } + } + + aliceLatch.countDown(); + }); + + awaitOnLatch(aliceLatch); + + PostDetailsRecord postDetailsRecord = sql.selectFrom(POST_DETAILS) + .where(field(POST_DETAILS.ID).eq(postId)) + .fetchOne(); + + assertTrue(preventedByLocking.get()); + }); + } +} diff --git a/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/UpsertTest.java b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/UpsertTest.java new file mode 100644 index 000000000..6e7039ea6 --- /dev/null +++ b/jooq/jooq-mysql/src/test/java/com/vladmihalcea/hpjp/jooq/mysql/crud/UpsertTest.java @@ -0,0 +1,53 @@ +package com.vladmihalcea.hpjp.jooq.mysql.crud; + +import org.jooq.DSLContext; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +import static com.vladmihalcea.hpjp.jooq.mysql.schema.crud.Tables.POST; +import static com.vladmihalcea.hpjp.jooq.mysql.schema.crud.tables.PostDetails.POST_DETAILS; + +/** + * @author Vlad Mihalcea + */ +public class UpsertTest extends AbstractJOOQMySQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Test + public void testUpsert() { + doInJOOQ(sql -> { + sql.delete(POST_DETAILS).execute(); + sql.delete(POST).execute(); + sql + .insertInto(POST).columns(POST.ID, POST.TITLE) + .values(1L, "High-Performance Java Persistence") + .execute(); + + executeAsync(() -> { + upsertPostDetails(sql, 1L, "Alice", LocalDateTime.now()); + }); + executeAsync(() -> { + upsertPostDetails(sql, 1L, "Bob", LocalDateTime.now()); + }); + + awaitTermination(1, TimeUnit.SECONDS); + }); + } + + private void upsertPostDetails(DSLContext sql, Long id, String owner, LocalDateTime timestamp) { + sql + .insertInto(POST_DETAILS) + .columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON) + .values(id, owner, timestamp) + .onDuplicateKeyUpdate() + .set(POST_DETAILS.UPDATED_BY, owner) + .set(POST_DETAILS.UPDATED_ON, timestamp) + .execute(); + } +} diff --git a/jooq/jooq-mysql/src/test/resources/mysql/clean_schema.sql b/jooq/jooq-mysql/src/test/resources/mysql/clean_schema.sql new file mode 100644 index 000000000..9df966f94 --- /dev/null +++ b/jooq/jooq-mysql/src/test/resources/mysql/clean_schema.sql @@ -0,0 +1,13 @@ +DELETE FROM post_tag; +DELETE FROM tag; +DELETE FROM post_details; +DELETE FROM post_comment_details; +DELETE FROM post_comment; +DELETE FROM post; + +ALTER TABLE post_tag AUTO_INCREMENT = 1; +ALTER TABLE tag AUTO_INCREMENT = 1; +ALTER TABLE post_details AUTO_INCREMENT = 1; +ALTER TABLE post_comment_details AUTO_INCREMENT = 1; +ALTER TABLE post_comment AUTO_INCREMENT = 1; +ALTER TABLE post AUTO_INCREMENT = 1; \ No newline at end of file diff --git a/jooq/jooq-mysql/src/test/resources/mysql/initial_schema.sql b/jooq/jooq-mysql/src/test/resources/mysql/initial_schema.sql index cd461b031..637fe0916 100644 --- a/jooq/jooq-mysql/src/test/resources/mysql/initial_schema.sql +++ b/jooq/jooq-mysql/src/test/resources/mysql/initial_schema.sql @@ -12,7 +12,7 @@ create table post_tag (post_id bigint not null, tag_id bigint not null); create table tag (id bigint not null AUTO_INCREMENT, name varchar(255), primary key (id)); create table post_comment_details (id int8 not null, post_id int8 not null, user_id int8 not null, ip varchar(18) not null, fingerprint varchar(256), primary key (id)); -alter table post_comment add constraint FKna4y825fdc5hw8aow65ijexm0 foreign key (post_id) references post (id); -alter table post_details add constraint FKkl5eik513p1xiudk2kxb0v92u foreign key (id) references post (id); -alter table post_tag add constraint FKac1wdchd2pnur3fl225obmlg0 foreign key (tag_id) references tag (id); -alter table post_tag add constraint FKc2auetuvsec0k566l0eyvr9cs foreign key (post_id) references post (id); \ No newline at end of file +alter table post_comment add constraint post_comment_post_id foreign key (post_id) references post (id); +alter table post_details add constraint post_details_post_id foreign key (id) references post (id); +alter table post_tag add constraint post_tag_tag_id foreign key (tag_id) references tag (id); +alter table post_tag add constraint post_tag_post_id foreign key (post_id) references post (id); \ No newline at end of file diff --git a/jooq/jooq-oracle/pom.xml b/jooq/jooq-oracle/pom.xml deleted file mode 100644 index a51369dec..000000000 --- a/jooq/jooq-oracle/pom.xml +++ /dev/null @@ -1,146 +0,0 @@ - - - - jooq - com.vladmihalcea.book - 1.0-SNAPSHOT - - 4.0.0 - - jooq-oracle - - - - org.jooq.pro - jooq - ${jooq.version} - - - - com.vladmihalcea.book - jooq-core - 1.0-SNAPSHOT - test-jar - test - - - - - - - - org.codehaus.mojo - sql-maven-plugin - - - com.oracle - ojdbc7_g - 12.1.0.1 - - - - - - oracle.jdbc.OracleDriver - jdbc:oracle:thin:@localhost:1521/xe - oracle - admin - true - continue - oracle-db-test - - - - create-test-compile-data - generate-test-sources - true - - execute - - - ascending - - ${basedir}/ - - src/test/resources/oracle/initial_schema.sql - - - true - - - - - - - org.jooq.pro - jooq-codegen-maven - ${jooq.version} - - - generate-test-sources - - generate - - - - - - - com.oracle - ojdbc7_g - 12.1.0.1 - - - - - - oracle.jdbc.OracleDriver - jdbc:oracle:thin:@localhost:1521/xe - oracle - admin - - - org.jooq.util.JavaGenerator - - org.jooq.util.oracle.OracleDatabase - .* - - oracle - - - - com.vladmihalcea.book.hpjp.jooq.oracle.schema.crud - ${project.build.directory}/generated-sources/java - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - add-source - process-test-sources - - add-test-source - - - - ${project.build.directory}/generated-sources/java - - - - - - - - - \ No newline at end of file diff --git a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/AbstractJOOQOracleSQLIntegrationTest.java b/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/AbstractJOOQOracleSQLIntegrationTest.java deleted file mode 100644 index 904392fc9..000000000 --- a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/AbstractJOOQOracleSQLIntegrationTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.oracle.crud; - -import com.vladmihalcea.book.hpjp.jooq.AbstractJOOQIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.OracleDataSourceProvider; -import org.jooq.SQLDialect; - -/** - * @author Vlad Mihalcea - */ -public abstract class AbstractJOOQOracleSQLIntegrationTest extends AbstractJOOQIntegrationTest { - - @Override - protected String ddlFolder() { - return "oracle"; - } - - @Override - protected SQLDialect sqlDialect() { - return SQLDialect.ORACLE11G; - } - - protected DataSourceProvider dataSourceProvider() { - return new OracleDataSourceProvider(); - } -} diff --git a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/BatchTest.java b/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/BatchTest.java deleted file mode 100644 index b0ae8a36b..000000000 --- a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/BatchTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.oracle.crud; - -import org.jooq.BatchBindStep; -import org.jooq.Record; -import org.jooq.Result; -import org.junit.Test; - -import java.math.BigInteger; - -import static com.vladmihalcea.book.hpjp.jooq.oracle.schema.crud.Tables.POST; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class BatchTest extends AbstractJOOQOracleSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testBatching() { - doInJOOQ(sql -> { - sql.delete(POST).execute(); - BatchBindStep batch = sql.batch(sql - .insertInto(POST, POST.ID, POST.TITLE) - .values((BigInteger) null, null) - ); - for (int i = 0; i < 3; i++) { - batch.bind(i, String.format("Post no. %d", i)); - } - int[] insertCounts = batch.execute(); - assertEquals(3, insertCounts.length); - Result posts = sql.select().from(POST).fetch(); - assertEquals(3, posts.size()); - }); - } -} diff --git a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/CrudTest.java b/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/CrudTest.java deleted file mode 100644 index a15116cca..000000000 --- a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/CrudTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.oracle.crud; - -import org.junit.Test; - -import static org.jooq.impl.DSL.field; -import static org.jooq.impl.DSL.table; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class CrudTest extends AbstractJOOQOracleSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testCrud() { - doInJOOQ(sql -> { - sql - .deleteFrom(table("post")) - .execute(); - - assertEquals(1, sql - .insertInto(table("post")).columns(field("id"), field("title")) - .values(1, "High-Performance Java Persistence") - .execute()); - - assertEquals("High-Performance Java Persistence", sql - .select(field("title")) - .from(table("post")) - .where(field("id").eq(1)) - .fetch().getValue(0, "title")); - - sql - .update(table("post")) - .set(field("title"), "High-Performance Java Persistence Book") - .where(field("id").eq(1)) - .execute(); - - assertEquals("High-Performance Java Persistence Book", sql - .select(field("title")) - .from(table("post")) - .where(field("id").eq(1)) - .fetch().getValue(0, "title")); - }); - } -} diff --git a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/KeysetPaginationFailTest.java b/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/KeysetPaginationFailTest.java deleted file mode 100644 index e21986673..000000000 --- a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/KeysetPaginationFailTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.oracle.crud; - -import org.jooq.Record3; -import org.jooq.SelectSeekStep2; - -import org.junit.Ignore; -import org.junit.Test; - -import java.math.BigInteger; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.List; - -import static com.vladmihalcea.book.hpjp.jooq.oracle.schema.crud.Tables.POST; -import static com.vladmihalcea.book.hpjp.jooq.oracle.schema.crud.Tables.POST_DETAILS; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class KeysetPaginationFailTest extends AbstractJOOQOracleSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - @Ignore("Failure expected") - public void testPagination() { - String user = "Vlad Mihalcea"; - - doInJOOQ(sql -> { - sql - .deleteFrom(POST_DETAILS) - .execute(); - - sql - .deleteFrom(POST) - .execute(); - - LocalDateTime now = LocalDateTime.now(); - - for (long i = 1; i < 100; i++) { - sql - .insertInto(POST).columns(POST.ID, POST.TITLE) - .values(BigInteger.valueOf(i), String.format("High-Performance Java Persistence - Chapter %d", i)) - .execute(); - - sql - .insertInto(POST_DETAILS).columns(POST_DETAILS.ID, POST_DETAILS.CREATED_ON, POST_DETAILS.CREATED_BY) - .values(BigInteger.valueOf(i), Timestamp.valueOf(now.plusHours(i / 10)), user) - .execute(); - } - }); - - doInJOOQ(sql -> { - - int pageSize = 5; - - List results = nextPage(pageSize, null); - - assertEquals(5, results.size()); - - results = nextPage(pageSize, results.get(results.size() - 1)); - - assertEquals(5, results.size()); - }); - - doInJOOQ(sql -> { - - int pageSize = 5; - - PostSummary offsetPostSummary = null; - - int pageCount = 0; - - while (true) { - List results = nextPage(pageSize, offsetPostSummary); - if(results.isEmpty()) { - break; - } - - offsetPostSummary = results.get(results.size() - 1); - pageCount++; - } - - assertEquals(Long.valueOf(1), offsetPostSummary.getId()); - assertEquals(20, pageCount); - }); - } - - public List nextPage(int pageSize, PostSummary offsetPostSummary) { - return doInJOOQ(sql -> { - SelectSeekStep2, Timestamp, BigInteger> selectStep = sql - .select(POST.ID, POST.TITLE, POST_DETAILS.CREATED_ON) - .from(POST) - .join(POST_DETAILS).using(POST.ID) - .orderBy(POST_DETAILS.CREATED_ON.desc(), POST.ID.desc()); - - return (offsetPostSummary != null) - ? selectStep - .seek(offsetPostSummary.getCreatedOn(), BigInteger.valueOf(offsetPostSummary.getId())) - .limit(pageSize) - .fetchInto(PostSummary.class) - : selectStep - .limit(pageSize) - .fetchInto(PostSummary.class); - }); - } - - /** - * @author Vlad Mihalcea - */ - public static class PostSummary { - - private final Long id; - - private final String title; - - private final Timestamp createdOn; - - public PostSummary(Long id, String title, Timestamp createdOn) { - this.id = id; - this.title = title; - this.createdOn = createdOn; - } - - public Long getId() { - return id; - } - - public String getTitle() { - return title; - } - - public Timestamp getCreatedOn() { - return createdOn; - } - } -} diff --git a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/KeysetPaginationTest.java b/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/KeysetPaginationTest.java deleted file mode 100644 index 1d61b9485..000000000 --- a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/KeysetPaginationTest.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.oracle.crud; - -import org.jooq.Record3; -import org.jooq.SelectSeekStep2; -import org.junit.Test; - -import java.math.BigInteger; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.List; - -import static com.vladmihalcea.book.hpjp.jooq.oracle.schema.crud.Tables.POST; -import static com.vladmihalcea.book.hpjp.jooq.oracle.schema.crud.Tables.POST_DETAILS; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class KeysetPaginationTest extends AbstractJOOQOracleSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testPagination() { - String user = "Vlad Mihalcea"; - - doInJOOQ(sql -> { - sql - .deleteFrom(POST_DETAILS) - .execute(); - - sql - .deleteFrom(POST) - .execute(); - - LocalDateTime now = LocalDateTime.now(); - - for (long i = 1; i < 100; i++) { - sql - .insertInto(POST).columns(POST.ID, POST.TITLE) - .values(BigInteger.valueOf(i), String.format("High-Performance Java Persistence - Chapter %d", i)) - .execute(); - - sql - .insertInto(POST_DETAILS).columns(POST_DETAILS.ID, POST_DETAILS.CREATED_ON, POST_DETAILS.CREATED_BY) - .values(BigInteger.valueOf(i), Timestamp.valueOf(now.plusHours(i / 10)), user) - .execute(); - } - }); - - doInJOOQ(sql -> { - - int pageSize = 5; - - List results = nextPage(pageSize, null); - - assertEquals(5, results.size()); - - results = nextPage(pageSize, results.get(results.size() - 1)); - - assertEquals(5, results.size()); - }); - - doInJOOQ(sql -> { - - int pageSize = 5; - - PostSummary offsetPostSummary = null; - - int pageCount = 0; - - while (true) { - List results = nextPage(pageSize, offsetPostSummary); - if(results.isEmpty()) { - break; - } - - offsetPostSummary = results.get(results.size() - 1); - pageCount++; - } - - assertEquals(Long.valueOf(1), offsetPostSummary.getId()); - assertEquals(20, pageCount); - }); - } - - public List nextPage(int pageSize, PostSummary offsetPostSummary) { - return doInJOOQ(sql -> { - SelectSeekStep2, Timestamp, BigInteger> selectStep = sql - .select(POST.ID, POST.TITLE, POST_DETAILS.CREATED_ON) - .from(POST) - .join(POST_DETAILS).on(POST.ID.eq(POST_DETAILS.ID)) - .orderBy(POST_DETAILS.CREATED_ON.desc(), POST.ID.desc()); - - return (offsetPostSummary != null) - ? selectStep - .seek(offsetPostSummary.getCreatedOn(), BigInteger.valueOf(offsetPostSummary.getId())) - .limit(pageSize) - .fetchInto(PostSummary.class) - : selectStep - .limit(pageSize) - .fetchInto(PostSummary.class); - }); - } - - /** - * @author Vlad Mihalcea - */ - public static class PostSummary { - - private final Long id; - - private final String title; - - private final Timestamp createdOn; - - public PostSummary(Long id, String title, Timestamp createdOn) { - this.id = id; - this.title = title; - this.createdOn = createdOn; - } - - public Long getId() { - return id; - } - - public String getTitle() { - return title; - } - - public Timestamp getCreatedOn() { - return createdOn; - } - } -} diff --git a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/SQLInjectionTest.java b/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/SQLInjectionTest.java deleted file mode 100644 index 38f7e27d2..000000000 --- a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/SQLInjectionTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.oracle.crud; - -import java.sql.Statement; - -import org.junit.Test; - -import org.jooq.DSLContext; -import org.jooq.conf.Settings; -import org.jooq.conf.StatementType; -import org.jooq.impl.DSL; - -import static org.jooq.impl.DSL.field; -import static org.jooq.impl.DSL.table; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class SQLInjectionTest extends AbstractJOOQOracleSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testLiteral() { - doInJOOQ(sql -> { - sql - .deleteFrom(table("post")) - .execute(); - - assertEquals(1, sql - .insertInto(table("post")).columns(field("id"), field("title")) - .values(1L, "High-Performance Java Persistence") - .execute()); - }); - - doInJDBC(connection -> { - DSLContext sql = DSL.using( - connection, - sqlDialect(), - new Settings().withStatementType( StatementType.STATIC_STATEMENT) - ); - - String sqlInjected = ((char)0xbf5c) + " or 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ) --'"; - //String sqlInjected = ((char)0x815c) + " or 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ) --'"; - - sql - .select(field("title")) - .from(table("post")) - .where(field("title").eq( sqlInjected )) - .fetch(); - }); - } -} diff --git a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/UpsertTest.java b/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/UpsertTest.java deleted file mode 100644 index dae985bb5..000000000 --- a/jooq/jooq-oracle/src/test/java/com/vladmihalcea/book/hpjp/jooq/oracle/crud/UpsertTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.oracle.crud; - -import org.jooq.DSLContext; -import org.junit.Test; - -import java.math.BigInteger; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.concurrent.TimeUnit; - -import static com.vladmihalcea.book.hpjp.jooq.oracle.schema.crud.Tables.POST; -import static com.vladmihalcea.book.hpjp.jooq.oracle.schema.crud.tables.PostDetails.POST_DETAILS; - -/** - * @author Vlad Mihalcea - */ -public class UpsertTest extends AbstractJOOQOracleSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testUpsert() { - doInJOOQ(sql -> { - sql.delete(POST_DETAILS).execute(); - sql.delete(POST).execute(); - sql - .insertInto(POST).columns(POST.ID, POST.TITLE) - .values(BigInteger.valueOf(1), "High-Performance Java Persistence") - .execute(); - - executeAsync(() -> { - upsertPostDetails(sql, BigInteger.valueOf(1), "Alice", - Timestamp.from(LocalDateTime.now().toInstant(ZoneOffset.UTC))); - }); - executeAsync(() -> { - upsertPostDetails(sql, BigInteger.valueOf(1), "Bob", - Timestamp.from(LocalDateTime.now().toInstant(ZoneOffset.UTC))); - }); - - awaitTermination(1, TimeUnit.SECONDS); - }); - } - - private void upsertPostDetails( - DSLContext sql, BigInteger id, String owner, Timestamp timestamp) { - sql - .insertInto(POST_DETAILS) - .columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON) - .values(id, owner, timestamp) - .onDuplicateKeyUpdate() - .set(POST_DETAILS.UPDATED_BY, owner) - .set(POST_DETAILS.UPDATED_ON, timestamp) - .execute(); - } -} diff --git a/jooq/jooq-oracle/src/test/resources/oracle/initial_schema.sql b/jooq/jooq-oracle/src/test/resources/oracle/initial_schema.sql deleted file mode 100644 index b888f971a..000000000 --- a/jooq/jooq-oracle/src/test/resources/oracle/initial_schema.sql +++ /dev/null @@ -1,18 +0,0 @@ -drop table post_comment_details cascade constraints; -drop table post_comment cascade constraints; -drop table post_details cascade constraints; -drop table post_tag cascade constraints; -drop table post cascade constraints; -drop table tag cascade constraints; - -create table post (id number(19,0) not null, title varchar2(255 char), primary key (id)); -create table post_comment (id number(19,0) not null, review varchar2(255 char), post_id number(19,0), primary key (id)); -create table post_details (id number(19,0) not null, created_by varchar2(255 char), created_on timestamp, updated_by varchar2(255 char), updated_on timestamp, primary key (id)); -create table post_tag (post_id number(19,0) not null, tag_id number(19,0) not null); -create table tag (id number(19,0) not null, name varchar2(255 char), primary key (id)); -create table post_comment_details (id number(19,0) not null, post_id number(19,0) not null, user_id number(19,0) not null, ip varchar(18) not null, fingerprint varchar(256), primary key (id)); - -alter table post_comment add constraint FKna4y825fdc5hw8aow65ijexm0 foreign key (post_id) references post; -alter table post_details add constraint FKkl5eik513p1xiudk2kxb0v92u foreign key (id) references post; -alter table post_tag add constraint FKac1wdchd2pnur3fl225obmlg0 foreign key (tag_id) references tag; -alter table post_tag add constraint FKc2auetuvsec0k566l0eyvr9cs foreign key (post_id) references post; diff --git a/jooq/jooq-pgsql-score/pom.xml b/jooq/jooq-pgsql-score/pom.xml index 05567a25a..b4aa118c9 100644 --- a/jooq/jooq-pgsql-score/pom.xml +++ b/jooq/jooq-pgsql-score/pom.xml @@ -3,13 +3,13 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0%20http://maven.apache.org/xsd/maven-4.0.0.xsd"> - jooq - com.vladmihalcea.book + com.vladmihalcea + high-performance-java-persistence-jooq 1.0-SNAPSHOT 4.0.0 - jooq-pgsql-score + high-performance-java-persistence-jooq-pgsql-score @@ -19,11 +19,9 @@ - com.vladmihalcea.book - jooq-core - 1.0-SNAPSHOT - test-jar - test + com.vladmihalcea + high-performance-java-persistence-jooq-core + ${project.parent.version} @@ -45,8 +43,8 @@ postgres admin true - continue postgres-db-test + row @@ -57,13 +55,11 @@ execute - row ascending ${basedir}/ src/test/resources/pgsql/initial_schema.sql - src/test/resources/pgsql/stored_procedures.sql true @@ -100,16 +96,15 @@ admin - org.jooq.util.JavaGenerator - org.jooq.util.postgres.PostgresDatabase + org.jooq.meta.postgres.PostgresDatabase .* public - com.vladmihalcea.book.hpjp.jooq.pgsql.schema.score + com.vladmihalcea.hpjp.jooq.pgsql.schema.score ${project.build.directory}/generated-sources/java diff --git a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/AbstractJOOQPostgreSQLIntegrationTest.java b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/AbstractJOOQPostgreSQLIntegrationTest.java deleted file mode 100644 index ff6674d04..000000000 --- a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/AbstractJOOQPostgreSQLIntegrationTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.pgsql.score; - -import com.vladmihalcea.book.hpjp.jooq.AbstractJOOQIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; -import org.jooq.SQLDialect; - -/** - * @author Vlad Mihalcea - */ -public abstract class AbstractJOOQPostgreSQLIntegrationTest extends AbstractJOOQIntegrationTest { - - @Override - protected String ddlFolder() { - return "pgsql"; - } - - @Override - protected SQLDialect sqlDialect() { - return SQLDialect.POSTGRES_9_5; - } - - protected DataSourceProvider dataSourceProvider() { - return new PostgreSQLDataSourceProvider(); - } -} diff --git a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/PostCommentScoreStoredProcedureTest.java b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/PostCommentScoreStoredProcedureTest.java deleted file mode 100644 index 37dbaf1f5..000000000 --- a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/PostCommentScoreStoredProcedureTest.java +++ /dev/null @@ -1,303 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.pgsql.score; - -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScoreResultTransformer; -import com.vladmihalcea.book.hpjp.jooq.pgsql.schema.score.routines.PostCommentScores; -import org.hibernate.query.NativeQuery; -import org.junit.Test; - -import javax.persistence.*; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class PostCommentScoreStoredProcedureTest extends AbstractJOOQPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - }; - } - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Override - public void init() { - super.init(); - initData(); - } - - protected void initData() { - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - - PostComment comment1 = new PostComment(); - comment1.setPost(post); - comment1.setReview("Comment 1"); - comment1.setScore(1); - entityManager.persist(comment1); - - PostComment comment1_1 = new PostComment(); - comment1_1.setParent(comment1); - comment1_1.setPost(post); - comment1_1.setReview("Comment 1_1"); - comment1_1.setScore(2); - entityManager.persist(comment1_1); - - PostComment comment1_2 = new PostComment(); - comment1_2.setParent(comment1); - comment1_2.setPost(post); - comment1_2.setReview("Comment 1_2"); - comment1_2.setScore(2); - entityManager.persist(comment1_2); - - PostComment comment1_2_1 = new PostComment(); - comment1_2_1.setParent(comment1_2); - comment1_2_1.setPost(post); - comment1_2_1.setReview("Comment 1_2_1"); - comment1_2_1.setScore(1); - entityManager.persist(comment1_2_1); - - PostComment comment2 = new PostComment(); - comment2.setPost(post); - comment2.setReview("Comment 2"); - comment2.setScore(1); - entityManager.persist(comment2); - - PostComment comment2_1 = new PostComment(); - comment2_1.setParent(comment2); - comment2_1.setPost(post); - comment2_1.setReview("Comment 2_1"); - comment2_1.setScore(1); - entityManager.persist(comment2_1); - - PostComment comment2_2 = new PostComment(); - comment2_2.setParent(comment2); - comment2_2.setPost(post); - comment2_2.setReview("Comment 2_2"); - comment2_2.setScore(1); - entityManager.persist(comment2_2); - - PostComment comment3 = new PostComment(); - comment3.setPost(post); - comment3.setReview("Comment 3"); - comment3.setScore(1); - entityManager.persist(comment3); - - PostComment comment3_1 = new PostComment(); - comment3_1.setParent(comment3); - comment3_1.setPost(post); - comment3_1.setReview("Comment 3_1"); - comment3_1.setScore(10); - entityManager.persist(comment3_1); - - PostComment comment3_2 = new PostComment(); - comment3_2.setParent(comment3); - comment3_2.setPost(post); - comment3_2.setReview("Comment 3_2"); - comment3_2.setScore(-2); - entityManager.persist(comment3_2); - - PostComment comment4 = new PostComment(); - comment4.setPost(post); - comment4.setReview("Comment 4"); - comment4.setScore(-5); - entityManager.persist(comment4); - - PostComment comment5 = new PostComment(); - comment5.setPost(post); - comment5.setReview("Comment 5"); - entityManager.persist(comment5); - - entityManager.flush(); - - }); - } - - @Test - public void test() { - LOGGER.info("Recursive CTE and Window Functions"); - Long postId = 1L; - int rank = 3; - List postCommentScoresJPA = postCommentScoresCTEJoin(postId, rank); - List postCommentScoresJOOQ = PostCommentScoreRootTransformer.INSTANCE.transform(postCommentScoresJOOQ(postId, rank)); - assertEquals(3, postCommentScoresJPA.size()); - assertEquals(3, postCommentScoresJOOQ.size()); - } - - protected List postCommentScoresCTEJoin(Long postId, int rank) { - return doInJPA(entityManager -> { - List postCommentScores = entityManager.createNativeQuery( - "SELECT id, parent_id, review, created_on, score " + - "FROM ( " + - " SELECT " + - " id, parent_id, review, created_on, score, " + - " dense_rank() OVER (ORDER BY total_score DESC) rank " + - " FROM ( " + - " SELECT " + - " id, parent_id, review, created_on, score, " + - " SUM(score) OVER (PARTITION BY root_id) total_score " + - " FROM (" + - " WITH RECURSIVE post_comment_score(id, root_id, post_id, " + - " parent_id, review, created_on, score) AS (" + - " SELECT " + - " id, id, post_id, parent_id, review, created_on, score" + - " FROM post_comment " + - " WHERE post_id = :postId AND parent_id IS NULL " + - " UNION ALL " + - " SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, " + - " pc.review, pc.created_on, pc.score " + - " FROM post_comment pc " + - " INNER JOIN post_comment_score pcs " + - " ON pc.parent_id = pcs.id " + - " WHERE pc.parent_id = pcs.id " + - " ) " + - " SELECT id, parent_id, root_id, review, created_on, score " + - " FROM post_comment_score " + - " ) score_by_comment " + - " ) score_total " + - " ORDER BY total_score DESC, id ASC " + - ") total_score_group " + - "WHERE rank <= :rank", "PostCommentScore").unwrap(NativeQuery.class) - .setParameter("postId", postId) - .setParameter("rank", rank) - .setResultTransformer(new PostCommentScoreResultTransformer()) - .list(); - return postCommentScores; - }); - } - - protected List postCommentScoresJOOQ(Long postId, int rank) { - return doInJOOQ(sql -> { - PostCommentScores postCommentScores = new PostCommentScores(); - postCommentScores.setPostid(postId); - postCommentScores.setRankid(rank); - postCommentScores.execute(sql.configuration()); - return postCommentScores.getReturnValue().into(PostCommentScore.class); - }); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @SqlResultSetMapping( - name = "PostCommentScore", - classes = @ConstructorResult( - targetClass = PostCommentScore.class, - columns = { - @ColumnResult(name = "id"), - @ColumnResult(name = "parent_id"), - @ColumnResult(name = "review"), - @ColumnResult(name = "created_on"), - @ColumnResult(name = "score") - } - ) - ) - public static class PostComment { - - @Id - @GeneratedValue - private Long id; - - @ManyToOne - @JoinColumn(name = "post_id") - private Post post; - - @ManyToOne - @JoinColumn(name = "parent_id") - private PostComment parent; - - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "created_on") - private Date createdOn = new Date(); - - private String review; - - private int score; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public PostComment getParent() { - return parent; - } - - public void setParent(PostComment parent) { - this.parent = parent; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public int getScore() { - return score; - } - - public void setScore(int score) { - this.score = score; - } - } -} diff --git a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/PostCommentScoreTest.java b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/PostCommentScoreTest.java deleted file mode 100644 index 36fcf591b..000000000 --- a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/PostCommentScoreTest.java +++ /dev/null @@ -1,380 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.pgsql.score; - -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScoreResultTransformer; -import org.hibernate.SQLQuery; -import org.jooq.CommonTableExpression; -import org.jooq.DSLContext; -import org.jooq.Record7; -import org.junit.Test; - -import javax.persistence.*; -import java.sql.Timestamp; -import java.util.Date; -import java.util.List; - -import static com.vladmihalcea.book.hpjp.jooq.pgsql.schema.score.Tables.POST_COMMENT; -import static org.jooq.impl.DSL.*; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class PostCommentScoreTest extends AbstractJOOQPostgreSQLIntegrationTest { - - @Override - protected Class[] entities() { - return new Class[] { - Post.class, - PostComment.class, - }; - } - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Override - public void init() { - super.init(); - initData(); - } - - protected void initData() { - doInJPA(entityManager -> { - Post post = new Post(); - post.setId(1L); - post.setTitle("High-Performance Java Persistence"); - entityManager.persist(post); - - PostComment comment1 = new PostComment(); - comment1.setPost(post); - comment1.setReview("Comment 1"); - comment1.setScore(1); - entityManager.persist(comment1); - - PostComment comment1_1 = new PostComment(); - comment1_1.setParent(comment1); - comment1_1.setPost(post); - comment1_1.setReview("Comment 1_1"); - comment1_1.setScore(2); - entityManager.persist(comment1_1); - - PostComment comment1_2 = new PostComment(); - comment1_2.setParent(comment1); - comment1_2.setPost(post); - comment1_2.setReview("Comment 1_2"); - comment1_2.setScore(2); - entityManager.persist(comment1_2); - - PostComment comment1_2_1 = new PostComment(); - comment1_2_1.setParent(comment1_2); - comment1_2_1.setPost(post); - comment1_2_1.setReview("Comment 1_2_1"); - comment1_2_1.setScore(1); - entityManager.persist(comment1_2_1); - - PostComment comment2 = new PostComment(); - comment2.setPost(post); - comment2.setReview("Comment 2"); - comment2.setScore(1); - entityManager.persist(comment2); - - PostComment comment2_1 = new PostComment(); - comment2_1.setParent(comment2); - comment2_1.setPost(post); - comment2_1.setReview("Comment 2_1"); - comment2_1.setScore(1); - entityManager.persist(comment2_1); - - PostComment comment2_2 = new PostComment(); - comment2_2.setParent(comment2); - comment2_2.setPost(post); - comment2_2.setReview("Comment 2_2"); - comment2_2.setScore(1); - entityManager.persist(comment2_2); - - PostComment comment3 = new PostComment(); - comment3.setPost(post); - comment3.setReview("Comment 3"); - comment3.setScore(1); - entityManager.persist(comment3); - - PostComment comment3_1 = new PostComment(); - comment3_1.setParent(comment3); - comment3_1.setPost(post); - comment3_1.setReview("Comment 3_1"); - comment3_1.setScore(10); - entityManager.persist(comment3_1); - - PostComment comment3_2 = new PostComment(); - comment3_2.setParent(comment3); - comment3_2.setPost(post); - comment3_2.setReview("Comment 3_2"); - comment3_2.setScore(-2); - entityManager.persist(comment3_2); - - PostComment comment4 = new PostComment(); - comment4.setPost(post); - comment4.setReview("Comment 4"); - comment4.setScore(-5); - entityManager.persist(comment4); - - PostComment comment5 = new PostComment(); - comment5.setPost(post); - comment5.setReview("Comment 5"); - entityManager.persist(comment5); - - entityManager.flush(); - - }); - } - - @Test - public void testJPA() { - LOGGER.info("Recursive CTE and Window Functions"); - Long postId = 1L; - int rank = 3; - List postCommentScores = postCommentScoresCTEJoin(postId, rank); - assertEquals(3, postCommentScores.size()); - } - - @Test - public void testJOOQ() { - LOGGER.info("Recursive CTE and Window Functions"); - Long postId = 1L; - int rank = 3; - List postCommentScores = - PostCommentScoreRootTransformer.INSTANCE.transform( - postCommentScores(postId, rank) - ); - assertEquals(3, postCommentScores.size()); - } - - protected List postCommentScoresCTEJoin(Long postId, int rank) { - return doInJPA(entityManager -> { - List postCommentScores = entityManager.createNativeQuery( - "SELECT id, parent_id, review, created_on, score " + - "FROM ( " + - " SELECT " + - " id, parent_id, review, created_on, score, " + - " dense_rank() OVER (ORDER BY total_score DESC) rank " + - " FROM ( " + - " SELECT " + - " id, parent_id, review, created_on, score, " + - " SUM(score) OVER (PARTITION BY root_id) total_score " + - " FROM (" + - " WITH RECURSIVE post_comment_score(id, root_id, post_id, " + - " parent_id, review, created_on, score) AS (" + - " SELECT " + - " id, id, post_id, parent_id, review, created_on, score" + - " FROM post_comment " + - " WHERE post_id = :postId AND parent_id IS NULL " + - " UNION ALL " + - " SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, " + - " pc.review, pc.created_on, pc.score " + - " FROM post_comment pc " + - " INNER JOIN post_comment_score pcs " + - " ON pc.parent_id = pcs.id " + - " WHERE pc.parent_id = pcs.id " + - " ) " + - " SELECT id, parent_id, root_id, review, created_on, score " + - " FROM post_comment_score " + - " ) score_by_comment " + - " ) score_total " + - " ORDER BY total_score DESC, id ASC " + - ") total_score_group " + - "WHERE rank <= :rank", "PostCommentScore").unwrap(SQLQuery.class) - .setParameter("postId", postId) - .setParameter("rank", rank) - .setResultTransformer(new PostCommentScoreResultTransformer()) - .list(); - return postCommentScores; - }); - } - - String PCS = "post_comment_score"; - String TSG = "total_score_group"; - String ST = "score_total"; - String SBC = "score_by_comment"; - - protected List postCommentScores(Long postId, int rank) { - return doInJOOQ(sql -> { - return sql.select( - field(name(TSG, "id"), Long.class), field(name(TSG, "parent_id"), Long.class), - field(name(TSG, "review"), String.class), - field(name(TSG, "created_on"), Timestamp.class), - field(name(TSG, "score"), Integer.class) - ).from( - sql.select( - field(name(ST, "id")), - field(name(ST, "parent_id")), - field(name(ST, "review")), - field(name(ST, "created_on")), - field(name(ST, "score")), - denseRank().over(orderBy(field(name(ST, "total_score")).desc())).as("rank") - ).from( - sql.select( - field(name(SBC, "id")), - field(name(SBC, "parent_id")), - field(name(SBC, "review")), - field(name(SBC, "created_on")), - field(name(SBC, "score")), - sum(field(name(SBC, "score"), Integer.class)) - .over(partitionBy(field(name(SBC, "root_id"))) - ).as("total_score") - ).from( - sql.withRecursive(withRecursiveExpression(sql, postId)) - .select( - field(name(PCS, "id")), - field(name(PCS, "parent_id")), - field(name(PCS, "root_id")), - field(name(PCS, "review")), - field(name(PCS, "created_on")), - field(name(PCS, "score"))) - .from(table(PCS)) - .asTable(SBC) - ) - .asTable(ST)) - .orderBy(field(name(ST, "total_score")).desc(), field(name(ST, "id")).asc() - ).asTable(TSG) - ) - .where(field(name(TSG, "rank"), Integer.class).le(rank)) - .fetchInto(PostCommentScore.class); - }); - } - - private CommonTableExpression> - withRecursiveExpression(DSLContext sql, Long postId) { - return name(PCS).fields("id", "root_id", "post_id", "parent_id", "review", "created_on", "score") - .as(sql.select( - POST_COMMENT.ID, POST_COMMENT.ID, POST_COMMENT.POST_ID, POST_COMMENT.PARENT_ID, POST_COMMENT.REVIEW, - POST_COMMENT.CREATED_ON, POST_COMMENT.SCORE) - .from(POST_COMMENT) - .where(POST_COMMENT.POST_ID.eq(postId).and(POST_COMMENT.PARENT_ID.isNull())) - .unionAll( - sql.select( - POST_COMMENT.ID, field(name("post_comment_score", "root_id"), Long.class), - POST_COMMENT.POST_ID, POST_COMMENT.PARENT_ID, POST_COMMENT.REVIEW, POST_COMMENT.CREATED_ON, - POST_COMMENT.SCORE) - .from(POST_COMMENT) - .innerJoin(table(name(PCS))) - .on(POST_COMMENT.PARENT_ID.eq(field(name(PCS, "id"), Long.class))) - .where(POST_COMMENT.PARENT_ID.eq(field(name(PCS, "id"), Long.class))) - ) - ); - } - - @Entity(name = "Post") - @Table(name = "post") - public static class Post { - - @Id - private Long id; - - private String title; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - } - - @Entity(name = "PostComment") - @Table(name = "post_comment") - @SqlResultSetMapping( - name = "PostCommentScore", - classes = @ConstructorResult( - targetClass = PostCommentScore.class, - columns = { - @ColumnResult(name = "id"), - @ColumnResult(name = "parent_id"), - @ColumnResult(name = "review"), - @ColumnResult(name = "created_on"), - @ColumnResult(name = "score") - } - ) - ) - public static class PostComment { - - @Id - @GeneratedValue - private Long id; - - @ManyToOne - @JoinColumn(name = "post_id") - private Post post; - - @ManyToOne - @JoinColumn(name = "parent_id") - private PostComment parent; - - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "created_on") - private Date createdOn = new Date(); - - private String review; - - private int score; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Post getPost() { - return post; - } - - public void setPost(Post post) { - this.post = post; - } - - public PostComment getParent() { - return parent; - } - - public void setParent(PostComment parent) { - this.parent = parent; - } - - public String getReview() { - return review; - } - - public void setReview(String review) { - this.review = review; - } - - public Date getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(Date createdOn) { - this.createdOn = createdOn; - } - - public int getScore() { - return score; - } - - public void setScore(int score) { - this.score = score; - } - } -} diff --git a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/AbstractJOOQPostgreSQLIntegrationTest.java b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/AbstractJOOQPostgreSQLIntegrationTest.java new file mode 100644 index 000000000..bd8a3eeea --- /dev/null +++ b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/AbstractJOOQPostgreSQLIntegrationTest.java @@ -0,0 +1,26 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.score; + +import com.vladmihalcea.hpjp.jooq.AbstractJOOQIntegrationTest; +import com.vladmihalcea.util.providers.DataSourceProvider; +import com.vladmihalcea.util.providers.PostgreSQLDataSourceProvider; +import org.jooq.SQLDialect; + +/** + * @author Vlad Mihalcea + */ +public abstract class AbstractJOOQPostgreSQLIntegrationTest extends AbstractJOOQIntegrationTest { + + @Override + protected String ddlFolder() { + return "pgsql"; + } + + @Override + protected SQLDialect sqlDialect() { + return SQLDialect.POSTGRES; + } + + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider(); + } +} diff --git a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/PostCommentFingerprintTest.java b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/PostCommentFingerprintTest.java similarity index 93% rename from jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/PostCommentFingerprintTest.java rename to jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/PostCommentFingerprintTest.java index 18c5a3d55..c00df24d0 100644 --- a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/PostCommentFingerprintTest.java +++ b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/PostCommentFingerprintTest.java @@ -1,19 +1,22 @@ -package com.vladmihalcea.book.hpjp.jooq.pgsql.score; +package com.vladmihalcea.hpjp.jooq.pgsql.score; -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScoreResultTransformer; -import org.hibernate.SQLQuery; +import com.vladmihalcea.hpjp.jooq.pgsql.score.dto.PostCommentScore; +import com.vladmihalcea.hpjp.jooq.pgsql.score.transformer.PostCommentScoreResultTransformer; +import com.vladmihalcea.hpjp.jooq.pgsql.score.transformer.PostCommentScoreRootTransformer; + +import org.hibernate.query.NativeQuery; import org.jooq.CommonTableExpression; import org.jooq.DSLContext; import org.jooq.Record7; import org.junit.Test; -import javax.persistence.*; +import jakarta.persistence.*; import java.sql.Timestamp; +import java.time.LocalDateTime; import java.util.Date; import java.util.List; -import static com.vladmihalcea.book.hpjp.jooq.pgsql.schema.score.Tables.POST_COMMENT; +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.score.Tables.POST_COMMENT; import static org.jooq.impl.DSL.*; import static org.junit.Assert.assertEquals; @@ -32,7 +35,7 @@ protected Class[] entities() { @Override protected String ddlScript() { - return "initial_schema.sql"; + return "clean_schema.sql"; } @Override @@ -175,9 +178,7 @@ protected List postCommentScoresCTEJoin(Long postId, int rank) " SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, " + " pc.review, pc.created_on, pc.score " + " FROM post_comment pc " + - " INNER JOIN post_comment_score pcs " + - " ON pc.parent_id = pcs.id " + - " WHERE pc.parent_id = pcs.id " + + " INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id " + " ) " + " SELECT id, parent_id, root_id, review, created_on, score " + " FROM post_comment_score " + @@ -185,9 +186,10 @@ protected List postCommentScoresCTEJoin(Long postId, int rank) " ) score_total " + " ORDER BY total_score DESC, id ASC " + ") total_score_group " + - "WHERE rank <= :rank", "PostCommentScore").unwrap(SQLQuery.class) + "WHERE rank <= :rank", "PostCommentScore") .setParameter("postId", postId) .setParameter("rank", rank) + .unwrap(NativeQuery.class) .setResultTransformer(new PostCommentScoreResultTransformer()) .list(); return postCommentScores; @@ -245,7 +247,7 @@ protected List postCommentScores(Long postId, int rank) { }); } - private CommonTableExpression> + private CommonTableExpression> withRecursiveExpression(DSLContext sql, Long postId) { return name(PCS).fields("id", "root_id", "post_id", "parent_id", "review", "created_on", "score") .as(sql.select( @@ -261,7 +263,6 @@ POST_COMMENT.ID, field(name("post_comment_score", "root_id"), Long.class), .from(POST_COMMENT) .innerJoin(table(name(PCS))) .on(POST_COMMENT.PARENT_ID.eq(field(name(PCS, "id"), Long.class))) - .where(POST_COMMENT.PARENT_ID.eq(field(name(PCS, "id"), Long.class))) ) ); } @@ -310,7 +311,8 @@ public void setTitle(String title) { public static class PostComment { @Id - @GeneratedValue + @GeneratedValue(generator = "hibernate_sequence", strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "hibernate_sequence", allocationSize = 1) private Long id; @ManyToOne diff --git a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/PostCommentScoreStoredProcedureTest.java b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/PostCommentScoreStoredProcedureTest.java new file mode 100644 index 000000000..6dfc8f7f4 --- /dev/null +++ b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/PostCommentScoreStoredProcedureTest.java @@ -0,0 +1,305 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.score; + +import com.vladmihalcea.hpjp.jooq.pgsql.schema.score.routines.GetPostCommentScores; +import com.vladmihalcea.hpjp.jooq.pgsql.score.dto.PostCommentScore; +import com.vladmihalcea.hpjp.jooq.pgsql.score.transformer.PostCommentScoreResultTransformer; +import com.vladmihalcea.hpjp.jooq.pgsql.score.transformer.PostCommentScoreRootTransformer; +import org.hibernate.query.NativeQuery; +import org.junit.Test; + +import jakarta.persistence.*; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentScoreStoredProcedureTest extends AbstractJOOQPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + }; + } + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Override + public void init() { + super.init(); + initData(); + } + + protected void initData() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + PostComment comment1 = new PostComment(); + comment1.setPost(post); + comment1.setReview("Comment 1"); + comment1.setScore(1); + entityManager.persist(comment1); + + PostComment comment1_1 = new PostComment(); + comment1_1.setParent(comment1); + comment1_1.setPost(post); + comment1_1.setReview("Comment 1_1"); + comment1_1.setScore(2); + entityManager.persist(comment1_1); + + PostComment comment1_2 = new PostComment(); + comment1_2.setParent(comment1); + comment1_2.setPost(post); + comment1_2.setReview("Comment 1_2"); + comment1_2.setScore(2); + entityManager.persist(comment1_2); + + PostComment comment1_2_1 = new PostComment(); + comment1_2_1.setParent(comment1_2); + comment1_2_1.setPost(post); + comment1_2_1.setReview("Comment 1_2_1"); + comment1_2_1.setScore(1); + entityManager.persist(comment1_2_1); + + PostComment comment2 = new PostComment(); + comment2.setPost(post); + comment2.setReview("Comment 2"); + comment2.setScore(1); + entityManager.persist(comment2); + + PostComment comment2_1 = new PostComment(); + comment2_1.setParent(comment2); + comment2_1.setPost(post); + comment2_1.setReview("Comment 2_1"); + comment2_1.setScore(1); + entityManager.persist(comment2_1); + + PostComment comment2_2 = new PostComment(); + comment2_2.setParent(comment2); + comment2_2.setPost(post); + comment2_2.setReview("Comment 2_2"); + comment2_2.setScore(1); + entityManager.persist(comment2_2); + + PostComment comment3 = new PostComment(); + comment3.setPost(post); + comment3.setReview("Comment 3"); + comment3.setScore(1); + entityManager.persist(comment3); + + PostComment comment3_1 = new PostComment(); + comment3_1.setParent(comment3); + comment3_1.setPost(post); + comment3_1.setReview("Comment 3_1"); + comment3_1.setScore(10); + entityManager.persist(comment3_1); + + PostComment comment3_2 = new PostComment(); + comment3_2.setParent(comment3); + comment3_2.setPost(post); + comment3_2.setReview("Comment 3_2"); + comment3_2.setScore(-2); + entityManager.persist(comment3_2); + + PostComment comment4 = new PostComment(); + comment4.setPost(post); + comment4.setReview("Comment 4"); + comment4.setScore(-5); + entityManager.persist(comment4); + + PostComment comment5 = new PostComment(); + comment5.setPost(post); + comment5.setReview("Comment 5"); + entityManager.persist(comment5); + + entityManager.flush(); + + }); + } + + @Test + public void test() { + LOGGER.info("Recursive CTE and Window Functions"); + Long postId = 1L; + int rank = 3; + List postCommentScoresJPA = postCommentScoresCTEJoin(postId, rank); + List postCommentScoresJOOQ = PostCommentScoreRootTransformer.INSTANCE.transform(postCommentScoresJOOQ(postId, rank)); + assertEquals(3, postCommentScoresJPA.size()); + assertEquals(3, postCommentScoresJOOQ.size()); + } + + protected List postCommentScoresCTEJoin(Long postId, int rank) { + return doInJPA(entityManager -> { + List postCommentScores = entityManager.createNativeQuery(""" + SELECT id, parent_id, review, created_on, score + FROM ( + SELECT + id, parent_id, review, created_on, score, + dense_rank() OVER (ORDER BY total_score DESC) rank + FROM ( + SELECT + id, parent_id, review, created_on, score, + SUM(score) OVER (PARTITION BY root_id) total_score + FROM ( + WITH RECURSIVE post_comment_score(id, root_id, post_id, + parent_id, review, created_on, score) AS ( + SELECT + id, id, post_id, parent_id, review, created_on, score + FROM post_comment + WHERE post_id = :postId AND parent_id IS NULL + UNION ALL + SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, + pc.review, pc.created_on, pc.score + FROM post_comment pc + INNER JOIN post_comment_score pcs + ON pc.parent_id = pcs.id + ) + SELECT id, parent_id, root_id, review, created_on, score + FROM post_comment_score + ) score_by_comment + ) score_total + ORDER BY total_score DESC, id ASC + ) total_score_group + WHERE rank <= :rank + """, "PostCommentScore").unwrap(NativeQuery.class) + .setParameter("postId", postId) + .setParameter("rank", rank) + .setResultTransformer(new PostCommentScoreResultTransformer()) + .list(); + return postCommentScores; + }); + } + + protected List postCommentScoresJOOQ(Long postId, int rank) { + return doInJOOQ(sql -> { + GetPostCommentScores postCommentScores = new GetPostCommentScores(); + postCommentScores.setPostid(postId); + postCommentScores.setRankid(rank); + postCommentScores.execute(sql.configuration()); + return postCommentScores.getReturnValue().into(PostCommentScore.class); + }); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @SqlResultSetMapping( + name = "PostCommentScore", + classes = @ConstructorResult( + targetClass = PostCommentScore.class, + columns = { + @ColumnResult(name = "id"), + @ColumnResult(name = "parent_id"), + @ColumnResult(name = "review"), + @ColumnResult(name = "created_on"), + @ColumnResult(name = "score") + } + ) + ) + public static class PostComment { + + @Id + @GeneratedValue(generator = "hibernate_sequence", strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "hibernate_sequence", allocationSize = 1) + private Long id; + + @ManyToOne + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne + @JoinColumn(name = "parent_id") + private PostComment parent; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn = new Date(); + + private String review; + + private int score; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public PostComment getParent() { + return parent; + } + + public void setParent(PostComment parent) { + this.parent = parent; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + } +} diff --git a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/PostCommentScoreTest.java b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/PostCommentScoreTest.java new file mode 100644 index 000000000..4be0bc552 --- /dev/null +++ b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/PostCommentScoreTest.java @@ -0,0 +1,384 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.score; + +import com.vladmihalcea.hpjp.jooq.pgsql.score.dto.PostCommentScore; +import com.vladmihalcea.hpjp.jooq.pgsql.score.transformer.PostCommentScoreResultTransformer; +import com.vladmihalcea.hpjp.jooq.pgsql.score.transformer.PostCommentScoreRootTransformer; +import org.hibernate.query.NativeQuery; +import org.jooq.CommonTableExpression; +import org.jooq.DSLContext; +import org.jooq.Record7; +import org.junit.Test; + +import jakarta.persistence.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.score.Tables.POST_COMMENT; +import static org.jooq.impl.DSL.*; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentScoreTest extends AbstractJOOQPostgreSQLIntegrationTest { + + @Override + protected Class[] entities() { + return new Class[] { + Post.class, + PostComment.class, + }; + } + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Override + public void init() { + super.init(); + initData(); + } + + protected void initData() { + doInJPA(entityManager -> { + Post post = new Post(); + post.setId(1L); + post.setTitle("High-Performance Java Persistence"); + entityManager.persist(post); + + PostComment comment1 = new PostComment(); + comment1.setPost(post); + comment1.setReview("Comment 1"); + comment1.setScore(1); + entityManager.persist(comment1); + + PostComment comment1_1 = new PostComment(); + comment1_1.setParent(comment1); + comment1_1.setPost(post); + comment1_1.setReview("Comment 1_1"); + comment1_1.setScore(2); + entityManager.persist(comment1_1); + + PostComment comment1_2 = new PostComment(); + comment1_2.setParent(comment1); + comment1_2.setPost(post); + comment1_2.setReview("Comment 1_2"); + comment1_2.setScore(2); + entityManager.persist(comment1_2); + + PostComment comment1_2_1 = new PostComment(); + comment1_2_1.setParent(comment1_2); + comment1_2_1.setPost(post); + comment1_2_1.setReview("Comment 1_2_1"); + comment1_2_1.setScore(1); + entityManager.persist(comment1_2_1); + + PostComment comment2 = new PostComment(); + comment2.setPost(post); + comment2.setReview("Comment 2"); + comment2.setScore(1); + entityManager.persist(comment2); + + PostComment comment2_1 = new PostComment(); + comment2_1.setParent(comment2); + comment2_1.setPost(post); + comment2_1.setReview("Comment 2_1"); + comment2_1.setScore(1); + entityManager.persist(comment2_1); + + PostComment comment2_2 = new PostComment(); + comment2_2.setParent(comment2); + comment2_2.setPost(post); + comment2_2.setReview("Comment 2_2"); + comment2_2.setScore(1); + entityManager.persist(comment2_2); + + PostComment comment3 = new PostComment(); + comment3.setPost(post); + comment3.setReview("Comment 3"); + comment3.setScore(1); + entityManager.persist(comment3); + + PostComment comment3_1 = new PostComment(); + comment3_1.setParent(comment3); + comment3_1.setPost(post); + comment3_1.setReview("Comment 3_1"); + comment3_1.setScore(10); + entityManager.persist(comment3_1); + + PostComment comment3_2 = new PostComment(); + comment3_2.setParent(comment3); + comment3_2.setPost(post); + comment3_2.setReview("Comment 3_2"); + comment3_2.setScore(-2); + entityManager.persist(comment3_2); + + PostComment comment4 = new PostComment(); + comment4.setPost(post); + comment4.setReview("Comment 4"); + comment4.setScore(-5); + entityManager.persist(comment4); + + PostComment comment5 = new PostComment(); + comment5.setPost(post); + comment5.setReview("Comment 5"); + entityManager.persist(comment5); + + entityManager.flush(); + + }); + } + + @Test + public void testJPA() { + LOGGER.info("Recursive CTE and Window Functions"); + Long postId = 1L; + int rank = 3; + List postCommentScores = postCommentScoresCTEJoin(postId, rank); + assertEquals(3, postCommentScores.size()); + } + + @Test + public void testJOOQ() { + LOGGER.info("Recursive CTE and Window Functions"); + Long postId = 1L; + int rank = 3; + List postCommentScores = + PostCommentScoreRootTransformer.INSTANCE.transform( + postCommentScores(postId, rank) + ); + assertEquals(3, postCommentScores.size()); + } + + protected List postCommentScoresCTEJoin(Long postId, int rank) { + return doInJPA(entityManager -> { + List postCommentScores = entityManager.createNativeQuery( + "SELECT id, parent_id, review, created_on, score " + + "FROM ( " + + " SELECT " + + " id, parent_id, review, created_on, score, " + + " dense_rank() OVER (ORDER BY total_score DESC) rank " + + " FROM ( " + + " SELECT " + + " id, parent_id, review, created_on, score, " + + " SUM(score) OVER (PARTITION BY root_id) total_score " + + " FROM (" + + " WITH RECURSIVE post_comment_score(id, root_id, post_id, " + + " parent_id, review, created_on, score) AS (" + + " SELECT " + + " id, id, post_id, parent_id, review, created_on, score" + + " FROM post_comment " + + " WHERE post_id = :postId AND parent_id IS NULL " + + " UNION ALL " + + " SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, " + + " pc.review, pc.created_on, pc.score " + + " FROM post_comment pc " + + " INNER JOIN post_comment_score pcs " + + " ON pc.parent_id = pcs.id " + + " WHERE pc.parent_id = pcs.id " + + " ) " + + " SELECT id, parent_id, root_id, review, created_on, score " + + " FROM post_comment_score " + + " ) score_by_comment " + + " ) score_total " + + " ORDER BY total_score DESC, id ASC " + + ") total_score_group " + + "WHERE rank <= :rank", "PostCommentScore") + .setParameter("postId", postId) + .setParameter("rank", rank) + .unwrap(NativeQuery.class) + .setResultTransformer(new PostCommentScoreResultTransformer()) + .list(); + return postCommentScores; + }); + } + + String PCS = "post_comment_score"; + String TSG = "total_score_group"; + String ST = "score_total"; + String SBC = "score_by_comment"; + + protected List postCommentScores(Long postId, int rank) { + return doInJOOQ(sql -> { + return sql.select( + field(name(TSG, "id"), Long.class), field(name(TSG, "parent_id"), Long.class), + field(name(TSG, "review"), String.class), + field(name(TSG, "created_on"), Timestamp.class), + field(name(TSG, "score"), Integer.class) + ).from( + sql.select( + field(name(ST, "id")), + field(name(ST, "parent_id")), + field(name(ST, "review")), + field(name(ST, "created_on")), + field(name(ST, "score")), + denseRank().over(orderBy(field(name(ST, "total_score")).desc())).as("rank") + ).from( + sql.select( + field(name(SBC, "id")), + field(name(SBC, "parent_id")), + field(name(SBC, "review")), + field(name(SBC, "created_on")), + field(name(SBC, "score")), + sum(field(name(SBC, "score"), Integer.class)) + .over(partitionBy(field(name(SBC, "root_id"))) + ).as("total_score") + ).from( + sql.withRecursive(withRecursiveExpression(sql, postId)) + .select( + field(name(PCS, "id")), + field(name(PCS, "parent_id")), + field(name(PCS, "root_id")), + field(name(PCS, "review")), + field(name(PCS, "created_on")), + field(name(PCS, "score"))) + .from(table(PCS)) + .asTable(SBC) + ) + .asTable(ST)) + .orderBy(field(name(ST, "total_score")).desc(), field(name(ST, "id")).asc() + ).asTable(TSG) + ) + .where(field(name(TSG, "rank"), Integer.class).le(rank)) + .fetchInto(PostCommentScore.class); + }); + } + + private CommonTableExpression> + withRecursiveExpression(DSLContext sql, Long postId) { + return name(PCS).fields("id", "root_id", "post_id", "parent_id", "review", "created_on", "score") + .as(sql.select( + POST_COMMENT.ID, POST_COMMENT.ID, POST_COMMENT.POST_ID, POST_COMMENT.PARENT_ID, POST_COMMENT.REVIEW, + POST_COMMENT.CREATED_ON, POST_COMMENT.SCORE) + .from(POST_COMMENT) + .where(POST_COMMENT.POST_ID.eq(postId).and(POST_COMMENT.PARENT_ID.isNull())) + .unionAll( + sql.select( + POST_COMMENT.ID, field(name("post_comment_score", "root_id"), Long.class), + POST_COMMENT.POST_ID, POST_COMMENT.PARENT_ID, POST_COMMENT.REVIEW, POST_COMMENT.CREATED_ON, + POST_COMMENT.SCORE) + .from(POST_COMMENT) + .innerJoin(table(name(PCS))) + .on(POST_COMMENT.PARENT_ID.eq(field(name(PCS, "id"), Long.class))) + .where(POST_COMMENT.PARENT_ID.eq(field(name(PCS, "id"), Long.class))) + ) + ); + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + @SqlResultSetMapping( + name = "PostCommentScore", + classes = @ConstructorResult( + targetClass = PostCommentScore.class, + columns = { + @ColumnResult(name = "id"), + @ColumnResult(name = "parent_id"), + @ColumnResult(name = "review"), + @ColumnResult(name = "created_on"), + @ColumnResult(name = "score") + } + ) + ) + public static class PostComment { + + @Id + @GeneratedValue(generator = "hibernate_sequence", strategy = GenerationType.SEQUENCE) + @SequenceGenerator(name = "hibernate_sequence", allocationSize = 1) + private Long id; + + @ManyToOne + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne + @JoinColumn(name = "parent_id") + private PostComment parent; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_on") + private Date createdOn = new Date(); + + private String review; + + private int score; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public PostComment getParent() { + return parent; + } + + public void setParent(PostComment parent) { + this.parent = parent; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + } +} diff --git a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/dto/PostCommentScore.java b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/dto/PostCommentScore.java new file mode 100644 index 000000000..980aed3e8 --- /dev/null +++ b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/dto/PostCommentScore.java @@ -0,0 +1,89 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.score.dto; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentScore { + + private Long id; + private Long parentId; + private String review; + private Date createdOn; + private long score; + + private List children = new ArrayList<>(); + + public PostCommentScore(Number id, Number parentId, String review, Date createdOn, Number score) { + this.id = id.longValue(); + this.parentId = parentId != null ? parentId.longValue() : null; + this.review = review; + this.createdOn = createdOn; + this.score = score.longValue(); + } + + public PostCommentScore() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public long getScore() { + return score; + } + + public void setScore(long score) { + this.score = score; + } + + public long getTotalScore() { + long total = getScore(); + for(PostCommentScore child : children) { + total += child.getTotalScore(); + } + return total; + } + + public List getChildren() { + List copy = new ArrayList<>(children); + copy.sort(Comparator.comparing(PostCommentScore::getCreatedOn)); + return copy; + } + + public void addChild(PostCommentScore child) { + children.add(child); + } +} diff --git a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/transformer/PostCommentScoreResultTransformer.java b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/transformer/PostCommentScoreResultTransformer.java new file mode 100644 index 000000000..af4568804 --- /dev/null +++ b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/transformer/PostCommentScoreResultTransformer.java @@ -0,0 +1,40 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.score.transformer; + +import com.vladmihalcea.hpjp.jooq.pgsql.score.dto.PostCommentScore; +import org.hibernate.transform.ResultTransformer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Vlad Mihalcea + */ +public class PostCommentScoreResultTransformer implements ResultTransformer { + + private Map postCommentScoreMap = new HashMap<>(); + + private List roots = new ArrayList<>(); + + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + PostCommentScore commentScore = (PostCommentScore) tuple[0]; + Long parentId = commentScore.getParentId(); + if (parentId == null) { + roots.add(commentScore); + } else { + PostCommentScore parent = postCommentScoreMap.get(parentId); + if (parent != null) { + parent.addChild(commentScore); + } + } + postCommentScoreMap.putIfAbsent(commentScore.getId(), commentScore); + return commentScore; + } + + @Override + public List transformList(List collection) { + return roots; + } +} diff --git a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/PostCommentScoreRootTransformer.java b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/transformer/PostCommentScoreRootTransformer.java similarity index 89% rename from jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/PostCommentScoreRootTransformer.java rename to jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/transformer/PostCommentScoreRootTransformer.java index 12add4d57..b24a2fd9b 100644 --- a/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/score/PostCommentScoreRootTransformer.java +++ b/jooq/jooq-pgsql-score/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/score/transformer/PostCommentScoreRootTransformer.java @@ -1,6 +1,6 @@ -package com.vladmihalcea.book.hpjp.jooq.pgsql.score; +package com.vladmihalcea.hpjp.jooq.pgsql.score.transformer; -import com.vladmihalcea.book.hpjp.hibernate.query.recursive.PostCommentScore; +import com.vladmihalcea.hpjp.jooq.pgsql.score.dto.PostCommentScore; import java.util.ArrayList; import java.util.HashMap; diff --git a/jooq/jooq-pgsql-score/src/test/resources/pgsql/clean_schema.sql b/jooq/jooq-pgsql-score/src/test/resources/pgsql/clean_schema.sql new file mode 100644 index 000000000..fbcc9294a --- /dev/null +++ b/jooq/jooq-pgsql-score/src/test/resources/pgsql/clean_schema.sql @@ -0,0 +1,4 @@ +DELETE FROM post_comment; +DELETE FROM post; + +ALTER SEQUENCE hibernate_sequence RESTART; \ No newline at end of file diff --git a/jooq/jooq-pgsql-score/src/test/resources/pgsql/initial_schema.sql b/jooq/jooq-pgsql-score/src/test/resources/pgsql/initial_schema.sql index 84c194f42..82b0d42db 100644 --- a/jooq/jooq-pgsql-score/src/test/resources/pgsql/initial_schema.sql +++ b/jooq/jooq-pgsql-score/src/test/resources/pgsql/initial_schema.sql @@ -1,20 +1,56 @@ -drop table if exists post cascade -; -drop table if exists post_comment cascade -; +drop table if exists post cascade; +drop table if exists post_comment cascade; -drop sequence hibernate_sequence -; +drop sequence hibernate_sequence; -create sequence hibernate_sequence start 1 increment 1 -; +create sequence hibernate_sequence start 1 increment 1; -create table post (id int8 not null, title varchar(255), primary key (id)) -; -create table post_comment (id int8 not null, created_on timestamp, review varchar(255), score int4 not null, parent_id int8, post_id int8, primary key (id)) -; +create table post (id int8 not null, title varchar(255), primary key (id)); +create table post_comment (id int8 not null, created_on timestamp, review varchar(255), score int4 not null, parent_id int8, post_id int8, primary key (id)); + +alter table post_comment add constraint FKmqxhu8q0j94rcly3yxlv0u498 foreign key (parent_id) references post_comment; +alter table post_comment add constraint post_comment_post_id foreign key (post_id) references post; + +drop function if exists get_post_comment_scores; -alter table post_comment add constraint FKmqxhu8q0j94rcly3yxlv0u498 foreign key (parent_id) references post_comment +CREATE OR REPLACE FUNCTION get_post_comment_scores(postId bigint, rankId integer) +RETURNS REFCURSOR AS +$$ +DECLARE + postComments REFCURSOR; +BEGIN +OPEN postComments FOR +SELECT id, parent_id, review, created_on, score +FROM ( + SELECT + id, parent_id, review, created_on, score, + dense_rank() OVER (ORDER BY total_score DESC) rank + FROM ( + SELECT + id, parent_id, review, created_on, score, + SUM(score) OVER (PARTITION BY root_id) total_score + FROM ( + WITH RECURSIVE post_comment_score(id, root_id, post_id, + parent_id, review, created_on, score) AS ( + SELECT + id, id, post_id, parent_id, review, created_on, score + FROM post_comment + WHERE post_id = postId AND parent_id IS NULL + UNION ALL + SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, + pc.review, pc.created_on, pc.score + FROM post_comment pc + INNER JOIN post_comment_score pcs ON pc.parent_id = pcs.id + ) + SELECT id, parent_id, root_id, review, created_on, score + FROM post_comment_score + ) score_by_comment + ) score_total + ORDER BY total_score DESC, id ASC +) total_score_group +WHERE rank <= rankId; +RETURN postComments; +END; +$$ +language 'plpgsql' ; -alter table post_comment add constraint FKna4y825fdc5hw8aow65ijexm0 foreign key (post_id) references post -; \ No newline at end of file diff --git a/jooq/jooq-pgsql-score/src/test/resources/pgsql/stored_procedures.sql b/jooq/jooq-pgsql-score/src/test/resources/pgsql/stored_procedures.sql deleted file mode 100644 index 03e8f0830..000000000 --- a/jooq/jooq-pgsql-score/src/test/resources/pgsql/stored_procedures.sql +++ /dev/null @@ -1,46 +0,0 @@ -drop function post_comment_scores(bigint, bigint) -; - -CREATE OR REPLACE FUNCTION post_comment_scores(postId BIGINT, rankId INT) - RETURNS REFCURSOR AS -$BODY$ - DECLARE - postComments REFCURSOR; - BEGIN - OPEN postComments FOR - SELECT id, parent_id, review, created_on, score - FROM ( - SELECT - id, parent_id, review, created_on, score, - dense_rank() OVER (ORDER BY total_score DESC) rank - FROM ( - SELECT - id, parent_id, review, created_on, score, - SUM(score) OVER (PARTITION BY root_id) total_score - FROM ( - WITH RECURSIVE post_comment_score(id, root_id, post_id, - parent_id, review, created_on, score) AS ( - SELECT - id, id, post_id, parent_id, review, created_on, score - FROM post_comment - WHERE post_id = postId AND parent_id IS NULL - UNION ALL - SELECT pc.id, pcs.root_id, pc.post_id, pc.parent_id, - pc.review, pc.created_on, pc.score - FROM post_comment pc - INNER JOIN post_comment_score pcs - ON pc.parent_id = pcs.id - WHERE pc.parent_id = pcs.id - ) - SELECT id, parent_id, root_id, review, created_on, score - FROM post_comment_score - ) score_by_comment - ) score_total - ORDER BY total_score DESC, id ASC - ) total_score_group - WHERE rank <= rankId; - RETURN postComments; - END; -$BODY$ -LANGUAGE plpgsql -; \ No newline at end of file diff --git a/jooq/jooq-pgsql/pom.xml b/jooq/jooq-pgsql/pom.xml index da154205e..d50b67fec 100644 --- a/jooq/jooq-pgsql/pom.xml +++ b/jooq/jooq-pgsql/pom.xml @@ -3,13 +3,13 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0%20http://maven.apache.org/xsd/maven-4.0.0.xsd"> - jooq - com.vladmihalcea.book + com.vladmihalcea + high-performance-java-persistence-jooq 1.0-SNAPSHOT 4.0.0 - jooq-pgsql + high-performance-java-persistence-jooq-pgsql @@ -19,11 +19,9 @@ - com.vladmihalcea.book - jooq-core - 1.0-SNAPSHOT - test-jar - test + com.vladmihalcea + high-performance-java-persistence-jooq-core + ${project.parent.version} @@ -46,6 +44,7 @@ admin true postgres-db-test + row @@ -97,16 +96,15 @@ admin - org.jooq.util.JavaGenerator - org.jooq.util.postgres.PostgresDatabase + org.jooq.meta.postgres.PostgresDatabase .* public - com.vladmihalcea.book.hpjp.jooq.pgsql.schema.crud + com.vladmihalcea.hpjp.jooq.pgsql.schema.crud ${project.build.directory}/generated-sources/java diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/AbstractJOOQPostgreSQLIntegrationTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/AbstractJOOQPostgreSQLIntegrationTest.java deleted file mode 100644 index 278148ce4..000000000 --- a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/AbstractJOOQPostgreSQLIntegrationTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.pgsql.crud; - -import com.vladmihalcea.book.hpjp.jooq.AbstractJOOQIntegrationTest; -import com.vladmihalcea.book.hpjp.util.providers.DataSourceProvider; -import com.vladmihalcea.book.hpjp.util.providers.PostgreSQLDataSourceProvider; -import org.jooq.SQLDialect; - -/** - * @author Vlad Mihalcea - */ -public abstract class AbstractJOOQPostgreSQLIntegrationTest extends AbstractJOOQIntegrationTest { - - @Override - protected String ddlFolder() { - return "pgsql"; - } - - @Override - protected SQLDialect sqlDialect() { - return SQLDialect.POSTGRES_9_5; - } - - protected DataSourceProvider dataSourceProvider() { - return new PostgreSQLDataSourceProvider(); - } -} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/BatchTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/BatchTest.java deleted file mode 100644 index 33a0d9c8e..000000000 --- a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/BatchTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.pgsql.crud; - -import org.jooq.BatchBindStep; -import org.jooq.Record; -import org.jooq.Result; -import org.junit.Test; - -import static com.vladmihalcea.book.hpjp.jooq.pgsql.schema.crud.Tables.POST; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class BatchTest extends AbstractJOOQPostgreSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testBatching() { - doInJOOQ(sql -> { - sql.delete(POST).execute(); - BatchBindStep batch = sql.batch(sql - .insertInto(POST, POST.ID, POST.TITLE) - .values((Long) null, null) - ); - for (int i = 0; i < 3; i++) { - batch.bind(i, String.format("Post no. %d", i)); - } - int[] insertCounts = batch.execute(); - assertEquals(3, insertCounts.length); - Result posts = sql.select().from(POST).fetch(); - assertEquals(3, posts.size()); - }); - } -} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/CrudTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/CrudTest.java deleted file mode 100644 index 2de968650..000000000 --- a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/CrudTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.pgsql.crud; - -import org.junit.Test; - -import static com.vladmihalcea.book.hpjp.jooq.pgsql.schema.crud.Tables.POST; -import static org.jooq.impl.DSL.field; -import static org.jooq.impl.DSL.table; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class CrudTest extends AbstractJOOQPostgreSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testCrud() { - doInJOOQ(sql -> { - sql - .deleteFrom(table("post")) - .execute(); - - assertEquals(1, sql - .insertInto(table("post")).columns(field("id"), field("title")) - .values(1L, "High-Performance Java Persistence") - .execute()); - - assertEquals("High-Performance Java Persistence", sql - .select(field("title")) - .from(table("post")) - .where(field("id").eq(1)) - .fetch().getValue(0, "title")); - - sql - .update(table("post")) - .set(field("title"), "High-Performance Java Persistence Book") - .where(field("id").eq(1)) - .execute(); - - assertEquals("High-Performance Java Persistence Book", sql - .select(field("title")) - .from(table("post")) - .where(field("id").eq(1)) - .fetch().getValue(0, "title")); - }); - } - - @Test - public void testCrudJavaSchema() { - doInJOOQ(sql -> { - sql - .deleteFrom(POST) - .execute(); - - assertEquals(1, sql - .insertInto(POST).columns(POST.ID, POST.TITLE) - .values(1L, "High-Performance Java Persistence") - .execute() - ); - - assertEquals("High-Performance Java Persistence", sql - .select(POST.TITLE) - .from(POST) - .where(POST.ID.eq(1L)) - .fetch().getValue(0, POST.TITLE) - ); - - sql - .update(POST) - .set(POST.TITLE, "High-Performance Java Persistence Book") - .where(POST.ID.eq(1L)) - .execute(); - - assertEquals("High-Performance Java Persistence Book", sql - .select(POST.TITLE) - .from(POST) - .where(POST.ID.eq(1L)) - .fetch().getValue(0, POST.TITLE) - ); - }); - } -} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/KeysetPaginationTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/KeysetPaginationTest.java deleted file mode 100644 index f633643c2..000000000 --- a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/KeysetPaginationTest.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.pgsql.crud; - -import org.jooq.Record3; -import org.jooq.SelectSeekStep2; -import org.junit.Test; - -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.List; - -import static com.vladmihalcea.book.hpjp.jooq.pgsql.schema.crud.Tables.POST; -import static com.vladmihalcea.book.hpjp.jooq.pgsql.schema.crud.Tables.POST_DETAILS; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class KeysetPaginationTest extends AbstractJOOQPostgreSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testPagination() { - String user = "Vlad Mihalcea"; - - doInJOOQ(sql -> { - sql - .deleteFrom(POST_DETAILS) - .execute(); - - sql - .deleteFrom(POST) - .execute(); - - LocalDateTime now = LocalDateTime.now(); - - for (long i = 1; i < 100; i++) { - sql - .insertInto(POST).columns(POST.ID, POST.TITLE) - .values(i, String.format("High-Performance Java Persistence - Chapter %d", i)) - .execute(); - - sql - .insertInto(POST_DETAILS).columns(POST_DETAILS.ID, POST_DETAILS.CREATED_ON, POST_DETAILS.CREATED_BY) - .values(i, Timestamp.valueOf(now.plusHours(i / 10)), user) - .execute(); - } - }); - - doInJOOQ(sql -> { - - int pageSize = 5; - - List results = nextPage(pageSize, null); - - assertEquals(5, results.size()); - - results = nextPage(pageSize, results.get(results.size() - 1)); - - assertEquals(5, results.size()); - }); - - doInJOOQ(sql -> { - - int pageSize = 5; - - PostSummary offsetPostSummary = null; - - int pageCount = 0; - - while (true) { - List results = nextPage(pageSize, offsetPostSummary); - if(results.isEmpty()) { - break; - } - - offsetPostSummary = results.get(results.size() - 1); - pageCount++; - } - - assertEquals(Long.valueOf(1), offsetPostSummary.getId()); - assertEquals(20, pageCount); - }); - } - - public List nextPage(int pageSize, PostSummary offsetPostSummary) { - return doInJOOQ(sql -> { - SelectSeekStep2, Timestamp, Long> selectStep = sql - .select(POST.ID, POST.TITLE, POST_DETAILS.CREATED_ON) - .from(POST) - .join(POST_DETAILS).on(POST.ID.eq(POST_DETAILS.ID)) - .orderBy(POST_DETAILS.CREATED_ON.desc(), POST.ID.desc()); - - return (offsetPostSummary != null) - ? selectStep - .seek(offsetPostSummary.getCreatedOn(), offsetPostSummary.getId()) - .limit(pageSize) - .fetchInto(PostSummary.class) - : selectStep - .limit(pageSize) - .fetchInto(PostSummary.class); - }); - } - - /** - * @author Vlad Mihalcea - */ - public static class PostSummary { - - private final Long id; - - private final String title; - - private final Timestamp createdOn; - - public PostSummary(Long id, String title, Timestamp createdOn) { - this.id = id; - this.title = title; - this.createdOn = createdOn; - } - - public Long getId() { - return id; - } - - public String getTitle() { - return title; - } - - public Timestamp getCreatedOn() { - return createdOn; - } - } -} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/SQLInjectionTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/SQLInjectionTest.java deleted file mode 100644 index a7353d772..000000000 --- a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/SQLInjectionTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.pgsql.crud; - -import java.sql.Statement; -import java.util.List; - -import org.junit.Test; - -import org.jooq.DSLContext; -import org.jooq.conf.Settings; -import org.jooq.conf.StatementType; -import org.jooq.impl.DSL; - -import static com.vladmihalcea.book.hpjp.jooq.pgsql.schema.crud.Tables.POST; -import static org.jooq.impl.DSL.field; -import static org.jooq.impl.DSL.table; -import static org.junit.Assert.assertEquals; - -/** - * @author Vlad Mihalcea - */ -public class SQLInjectionTest extends AbstractJOOQPostgreSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testLiteral() { - doInJOOQ(sql -> { - sql - .deleteFrom(table("post")) - .execute(); - - assertEquals(1, sql - .insertInto(table("post")).columns(field("id"), field("title")) - .values(1L, "High-Performance Java Persistence") - .execute()); - }); - - doInJDBC(connection -> { - DSLContext sql = DSL.using( - connection, - sqlDialect(), - new Settings().withStatementType( StatementType.STATIC_STATEMENT) - ); - - String sqlInjected = ((char)0xbf5c) + " or 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ) --'"; - //String sqlInjected = ((char)0x815c) + " or 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ) --'"; - - sql - .select(field("title")) - .from(table("post")) - .where(field("title").eq( sqlInjected )) - .fetch(); - }); - } -} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/StreamTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/StreamTest.java deleted file mode 100644 index 5e83146cf..000000000 --- a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/StreamTest.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.pgsql.crud; - -import com.vladmihalcea.book.hpjp.jooq.pgsql.schema.crud.tables.records.PostCommentDetailsRecord; -import org.hibernate.Session; -import org.junit.Before; -import org.junit.Test; - -import java.util.*; -import java.util.stream.Stream; - -import static com.vladmihalcea.book.hpjp.jooq.pgsql.schema.crud.Tables.POST; -import static com.vladmihalcea.book.hpjp.jooq.pgsql.schema.crud.Tables.POST_COMMENT_DETAILS; - -/** - * @author Vlad Mihalcea - */ -public class StreamTest extends AbstractJOOQPostgreSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Before - public void init() { - super.init(); - - doInJOOQ(sql -> { - sql - .deleteFrom(POST) - .execute(); - - long id = 0L; - - sql - .insertInto( - POST_COMMENT_DETAILS).columns( - POST_COMMENT_DETAILS.ID, - POST_COMMENT_DETAILS.POST_ID, - POST_COMMENT_DETAILS.USER_ID, - POST_COMMENT_DETAILS.IP, - POST_COMMENT_DETAILS.FINGERPRINT - ) - .values(++id, 1L, 1L, "192.168.0.2", "ABC123") - .values(++id, 1L, 2L, "192.168.0.3", "ABC456") - .values(++id, 1L, 3L, "192.168.0.4", "ABC789") - .values(++id, 2L, 1L, "192.168.0.2", "ABC123") - .values(++id, 2L, 2L, "192.168.0.3", "ABC456") - .values(++id, 2L, 4L, "192.168.0.3", "ABC456") - .values(++id, 2L, 5L, "192.168.0.3", "ABC456") - .execute(); - }); - } - - @Test - public void testStream() { - doInJOOQ(sql -> { - - Long lastProcessedId = 1L; - - try (Stream stream = sql - .selectFrom(POST_COMMENT_DETAILS) - .where(POST_COMMENT_DETAILS.ID.gt(lastProcessedId)) - .stream()) { - processStream(stream); - } - }); - } - - private void processStream(Stream stream) { - Map>> registryMap = new MaxSizeHashMap<>(25); - - stream.forEach(postCommentDetails -> { - Long postId = postCommentDetails.get(POST_COMMENT_DETAILS.POST_ID); - String ip = postCommentDetails.get(POST_COMMENT_DETAILS.IP); - String fingerprint = postCommentDetails.get(POST_COMMENT_DETAILS.FINGERPRINT); - Long userId = postCommentDetails.get(POST_COMMENT_DETAILS.USER_ID); - - Map> fingerprintsToPostMap = registryMap.get(postId); - if(fingerprintsToPostMap == null) { - fingerprintsToPostMap = new HashMap<>(); - registryMap.put(postId, fingerprintsToPostMap); - } - - IpFingerprint ipFingerprint = new IpFingerprint(ip, fingerprint); - - List userIds = fingerprintsToPostMap.get(ipFingerprint); - if(userIds == null) { - userIds = new ArrayList<>(); - fingerprintsToPostMap.put(ipFingerprint, userIds); - } - - if(!userIds.contains(userId)) { - userIds.add(userId); - if(userIds.size() > 1) { - notifyPossibleMultipleAccountFraud(postId, userIds); - } - } - }); - } - - @Test - public void testHibernateStream() { - doInJPA(entityManager -> { - Map>> registryMap = new MaxSizeHashMap<>(1000); - Long lastProcessedId = 1L; - - Stream stream = entityManager.unwrap(Session.class).createNativeQuery( - "select post_id, user_id, ip, fingerprint " + - "from post_comment_details " + - "where id > :id") - .setParameter("id", lastProcessedId) - .stream(); - - stream.forEach(pcd -> { - Long postId = ((Number) pcd[0]).longValue(); - Long userId = ((Number) pcd[1]).longValue(); - String ip = (String) pcd[2]; - String fingerprint = (String) pcd[3]; - - Map> fingerprintsToIpMap = registryMap.get(postId); - if(fingerprintsToIpMap == null) { - fingerprintsToIpMap = new HashMap<>(); - registryMap.put(postId, fingerprintsToIpMap); - } - - IpFingerprint ipFingerprint = new IpFingerprint(ip, fingerprint); - - List userIds = fingerprintsToIpMap.get(ipFingerprint); - if(userIds == null) { - userIds = new ArrayList<>(); - fingerprintsToIpMap.put(ipFingerprint, userIds); - } - if(!userIds.contains(userId)) { - userIds.add(userId); - } - if(userIds.size() > 1) { - notifyPossibleMultipleAccountFraud(postId, userIds); - } - }); - }); - } - - private void notifyPossibleMultipleAccountFraud(Long postId, List userIds) { - LOGGER.info("Post id {} possible fraud with user ids {}", postId, userIds); - } - - public static class IpFingerprint { - private final String ip; - private final String fingerprint; - - public IpFingerprint(String ip, String fingerprint) { - this.ip = ip; - this.fingerprint = fingerprint; - } - - public String getIp() { - return ip; - } - - public String getFingerprint() { - return fingerprint; - } - - @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - IpFingerprint that = (IpFingerprint) o; - return Objects.equals(ip, that.ip) && - Objects.equals(fingerprint, that.fingerprint); - } - - @Override public int hashCode() { - return Objects.hash(ip, fingerprint); - } - } - - public class MaxSizeHashMap extends LinkedHashMap { - private final int maxSize; - - public MaxSizeHashMap(int maxSize) { - this.maxSize = maxSize; - } - - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > maxSize; - } - } -} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/UpsertTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/UpsertTest.java deleted file mode 100644 index 41629281d..000000000 --- a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/book/hpjp/jooq/pgsql/crud/UpsertTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.vladmihalcea.book.hpjp.jooq.pgsql.crud; - -import org.jooq.DSLContext; -import org.junit.Test; - -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.concurrent.TimeUnit; - -import static com.vladmihalcea.book.hpjp.jooq.pgsql.schema.crud.Tables.POST; -import static com.vladmihalcea.book.hpjp.jooq.pgsql.schema.crud.tables.PostDetails.POST_DETAILS; - -/** - * @author Vlad Mihalcea - */ -public class UpsertTest extends AbstractJOOQPostgreSQLIntegrationTest { - - @Override - protected String ddlScript() { - return "initial_schema.sql"; - } - - @Test - public void testUpsert() { - doInJOOQ(sql -> { - sql.delete(POST_DETAILS).execute(); - sql.delete(POST).execute(); - sql - .insertInto(POST).columns(POST.ID, POST.TITLE) - .values(1L, "High-Performance Java Persistence") - .execute(); - - executeAsync(() -> { - upsertPostDetails(sql, 1L, "Alice", - Timestamp.from(LocalDateTime.now().toInstant(ZoneOffset.UTC))); - }); - executeAsync(() -> { - upsertPostDetails(sql, 1L, "Bob", - Timestamp.from(LocalDateTime.now().toInstant(ZoneOffset.UTC))); - }); - - awaitTermination(1, TimeUnit.SECONDS); - }); - } - - private void upsertPostDetails(DSLContext sql, Long id, String owner, Timestamp timestamp) { - sql - .insertInto(POST_DETAILS) - .columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON) - .values(id, owner, timestamp) - .onDuplicateKeyUpdate() - .set(POST_DETAILS.UPDATED_BY, owner) - .set(POST_DETAILS.UPDATED_ON, timestamp) - .execute(); - } -} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/batching/BatchTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/batching/BatchTest.java new file mode 100644 index 000000000..6eff17a41 --- /dev/null +++ b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/batching/BatchTest.java @@ -0,0 +1,39 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.batching; + +import com.vladmihalcea.hpjp.jooq.pgsql.util.AbstractJOOQPostgreSQLIntegrationTest; +import org.jooq.BatchBindStep; +import org.jooq.Record; +import org.jooq.Result; +import org.junit.Test; + +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.Tables.POST; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class BatchTest extends AbstractJOOQPostgreSQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Test + public void testBatching() { + doInJOOQ(sql -> { + sql.delete(POST).execute(); + BatchBindStep batch = sql.batch(sql + .insertInto(POST, POST.ID, POST.TITLE) + .values((Long) null, null) + ); + for (int i = 0; i < 3; i++) { + batch.bind(i, String.format("Post no. %d", i)); + } + int[] insertCounts = batch.execute(); + assertEquals(3, insertCounts.length); + Result posts = sql.select().from(POST).fetch(); + assertEquals(3, posts.size()); + }); + } +} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/crud/CrudTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/crud/CrudTest.java new file mode 100644 index 000000000..02d070077 --- /dev/null +++ b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/crud/CrudTest.java @@ -0,0 +1,87 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.crud; + +import com.vladmihalcea.hpjp.jooq.pgsql.util.AbstractJOOQPostgreSQLIntegrationTest; +import org.junit.Test; + +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.Tables.POST; +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.table; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class CrudTest extends AbstractJOOQPostgreSQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Test + public void testCrud() { + doInJOOQ(sql -> { + sql + .deleteFrom(table("post")) + .execute(); + + assertEquals(1, sql + .insertInto(table("post")).columns(field("id"), field("title")) + .values(1L, "High-Performance Java Persistence") + .execute()); + + assertEquals("High-Performance Java Persistence", sql + .select(field("title")) + .from(table("post")) + .where(field("id").eq(1)) + .fetch().getValue(0, "title")); + + sql + .update(table("post")) + .set(field("title"), "High-Performance Java Persistence Book") + .where(field("id").eq(1)) + .execute(); + + assertEquals("High-Performance Java Persistence Book", sql + .select(field("title")) + .from(table("post")) + .where(field("id").eq(1)) + .fetch().getValue(0, "title")); + }); + } + + @Test + public void testCrudJavaSchema() { + doInJOOQ(sql -> { + sql + .deleteFrom(POST) + .execute(); + + assertEquals(1, sql + .insertInto(POST).columns(POST.ID, POST.TITLE) + .values(1L, "High-Performance Java Persistence") + .execute() + ); + + assertEquals("High-Performance Java Persistence", sql + .select(POST.TITLE) + .from(POST) + .where(POST.ID.eq(1L)) + .fetch().getValue(0, POST.TITLE) + ); + + sql + .update(POST) + .set(POST.TITLE, "High-Performance Java Persistence Book") + .where(POST.ID.eq(1L)) + .execute(); + + assertEquals("High-Performance Java Persistence Book", sql + .select(POST.TITLE) + .from(POST) + .where(POST.ID.eq(1L)) + .fetch().getValue(0, POST.TITLE) + ); + }); + } +} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/crud/SQLInjectionTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/crud/SQLInjectionTest.java new file mode 100644 index 000000000..c9e421986 --- /dev/null +++ b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/crud/SQLInjectionTest.java @@ -0,0 +1,55 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.crud; + +import com.vladmihalcea.hpjp.jooq.pgsql.util.AbstractJOOQPostgreSQLIntegrationTest; +import org.junit.Test; + +import org.jooq.DSLContext; +import org.jooq.conf.Settings; +import org.jooq.conf.StatementType; +import org.jooq.impl.DSL; + +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.table; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class SQLInjectionTest extends AbstractJOOQPostgreSQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Test + public void testLiteral() { + doInJOOQ(sql -> { + sql + .deleteFrom(table("post")) + .execute(); + + assertEquals(1, sql + .insertInto(table("post")).columns(field("id"), field("title")) + .values(1L, "High-Performance Java Persistence") + .execute()); + }); + + doInJDBC(connection -> { + DSLContext sql = DSL.using( + connection, + sqlDialect(), + new Settings().withStatementType( StatementType.STATIC_STATEMENT) + ); + + String sqlInjected = ((char)0xbf5c) + " or 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ) --'"; + //String sqlInjected = ((char)0x815c) + " or 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ) --'"; + + sql + .select(field("title")) + .from(table("post")) + .where(field("title").eq( sqlInjected )) + .fetch(); + }); + } +} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/fetching/StreamTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/fetching/StreamTest.java new file mode 100644 index 000000000..db08b6d0f --- /dev/null +++ b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/fetching/StreamTest.java @@ -0,0 +1,182 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.fetching; + +import com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.tables.records.PostCommentDetailsRecord; +import com.vladmihalcea.hpjp.jooq.pgsql.util.AbstractJOOQPostgreSQLIntegrationTest; +import org.hibernate.Session; +import org.junit.Test; + +import java.util.*; +import java.util.stream.Stream; + +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.Tables.POST_COMMENT_DETAILS; + +/** + * @author Vlad Mihalcea + */ +public class StreamTest extends AbstractJOOQPostgreSQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + public void afterInit() { + doInJOOQ(sql -> { + long id = 0L; + + sql + .insertInto( + POST_COMMENT_DETAILS).columns( + POST_COMMENT_DETAILS.ID, + POST_COMMENT_DETAILS.POST_ID, + POST_COMMENT_DETAILS.USER_ID, + POST_COMMENT_DETAILS.IP, + POST_COMMENT_DETAILS.FINGERPRINT + ) + .values(++id, 1L, 1L, "192.168.0.2", "ABC123") + .values(++id, 1L, 2L, "192.168.0.3", "ABC456") + .values(++id, 1L, 3L, "192.168.0.4", "ABC789") + .values(++id, 2L, 1L, "192.168.0.2", "ABC123") + .values(++id, 2L, 2L, "192.168.0.3", "ABC456") + .values(++id, 2L, 4L, "192.168.0.3", "ABC456") + .values(++id, 2L, 5L, "192.168.0.3", "ABC456") + .execute(); + }); + } + + @Test + public void testStream() { + doInJOOQ(sql -> { + + Long lastProcessedId = 1L; + + try (Stream stream = sql + .selectFrom(POST_COMMENT_DETAILS) + .where(POST_COMMENT_DETAILS.ID.gt(lastProcessedId)) + .stream()) { + processStream(stream); + } + }); + } + + private void processStream(Stream stream) { + Map>> registryMap = new MaxSizeHashMap<>(25); + + stream.forEach(postCommentDetails -> { + Long postId = postCommentDetails.get(POST_COMMENT_DETAILS.POST_ID); + String ip = postCommentDetails.get(POST_COMMENT_DETAILS.IP); + String fingerprint = postCommentDetails.get(POST_COMMENT_DETAILS.FINGERPRINT); + Long userId = postCommentDetails.get(POST_COMMENT_DETAILS.USER_ID); + + Map> fingerprintsToPostMap = registryMap.get(postId); + if(fingerprintsToPostMap == null) { + fingerprintsToPostMap = new HashMap<>(); + registryMap.put(postId, fingerprintsToPostMap); + } + + IpFingerprint ipFingerprint = new IpFingerprint(ip, fingerprint); + + List userIds = fingerprintsToPostMap.get(ipFingerprint); + if(userIds == null) { + userIds = new ArrayList<>(); + fingerprintsToPostMap.put(ipFingerprint, userIds); + } + + if(!userIds.contains(userId)) { + userIds.add(userId); + if(userIds.size() > 1) { + notifyPossibleMultipleAccountFraud(postId, userIds); + } + } + }); + } + + @Test + public void testHibernateStream() { + doInJPA(entityManager -> { + Map>> registryMap = new MaxSizeHashMap<>(1000); + Long lastProcessedId = 1L; + + Stream stream = entityManager.unwrap(Session.class).createNativeQuery( + "select post_id, user_id, ip, fingerprint " + + "from post_comment_details " + + "where id > :id") + .setParameter("id", lastProcessedId) + .stream(); + + stream.forEach(pcd -> { + Long postId = ((Number) pcd[0]).longValue(); + Long userId = ((Number) pcd[1]).longValue(); + String ip = (String) pcd[2]; + String fingerprint = (String) pcd[3]; + + Map> fingerprintsToIpMap = registryMap.get(postId); + if(fingerprintsToIpMap == null) { + fingerprintsToIpMap = new HashMap<>(); + registryMap.put(postId, fingerprintsToIpMap); + } + + IpFingerprint ipFingerprint = new IpFingerprint(ip, fingerprint); + + List userIds = fingerprintsToIpMap.get(ipFingerprint); + if(userIds == null) { + userIds = new ArrayList<>(); + fingerprintsToIpMap.put(ipFingerprint, userIds); + } + if(!userIds.contains(userId)) { + userIds.add(userId); + } + if(userIds.size() > 1) { + notifyPossibleMultipleAccountFraud(postId, userIds); + } + }); + }); + } + + private void notifyPossibleMultipleAccountFraud(Long postId, List userIds) { + LOGGER.info("Post id {} possible fraud with user ids {}", postId, userIds); + } + + public static class IpFingerprint { + private final String ip; + private final String fingerprint; + + public IpFingerprint(String ip, String fingerprint) { + this.ip = ip; + this.fingerprint = fingerprint; + } + + public String getIp() { + return ip; + } + + public String getFingerprint() { + return fingerprint; + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IpFingerprint that = (IpFingerprint) o; + return Objects.equals(ip, that.ip) && + Objects.equals(fingerprint, that.fingerprint); + } + + @Override public int hashCode() { + return Objects.hash(ip, fingerprint); + } + } + + public class MaxSizeHashMap extends LinkedHashMap { + private final int maxSize; + + public MaxSizeHashMap(int maxSize) { + this.maxSize = maxSize; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > maxSize; + } + } +} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/functions/qa/QuestionAndAnswerTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/functions/qa/QuestionAndAnswerTest.java new file mode 100644 index 000000000..e62cfebe4 --- /dev/null +++ b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/functions/qa/QuestionAndAnswerTest.java @@ -0,0 +1,302 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.functions.qa; + +import com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.tables.records.GetUpdatedQuestionsAndAnswersRecord; +import com.vladmihalcea.hpjp.jooq.pgsql.util.AbstractJOOQPostgreSQLIntegrationTest; +import org.jooq.Result; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.Tables.ANSWER; +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.Tables.QUESTION; +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.tables.GetUpdatedQuestionsAndAnswers.GET_UPDATED_QUESTIONS_AND_ANSWERS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class QuestionAndAnswerTest extends AbstractJOOQPostgreSQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + public void afterInit() { + doInJOOQ(sql -> { + LocalDateTime timestamp = LocalDateTime.now().minusSeconds(1); + + sql + .insertInto(QUESTION) + .columns( + QUESTION.ID, + QUESTION.TITLE, + QUESTION.BODY, + QUESTION.SCORE, + QUESTION.CREATED_ON, + QUESTION.CREATED_ON + ) + .values( + 1L, + "How to call jOOQ stored procedures?", + "I have a PostgreSQL stored procedure and I'd like to call it from jOOQ.", + 1, + timestamp, + timestamp + ) + .execute(); + + sql + .insertInto(ANSWER) + .columns( + ANSWER.ID, + ANSWER.QUESTION_ID, + ANSWER.BODY, + ANSWER.SCORE, + ANSWER.ACCEPTED, + ANSWER.CREATED_ON, + ANSWER.CREATED_ON + ) + .values( + 1L, + 1L, + "Checkout the [jOOQ docs]" + + "(https://www.jooq.org/doc/latest/manual/sql-execution/stored-procedures/).", + 10, + true, + timestamp, + timestamp + ) + .values( + 2L, + 1L, + "Checkout [this article]" + + "(https://vladmihalcea.com/jooq-facts-sql-functions-made-easy/).", + 5, + false, + timestamp, + timestamp + ) + .execute(); + }); + + List questions = getUpdatedQuestionsAndAnswers(); + + assertEquals(1, questions.size()); + Question question = questions.get(0); + assertEquals(1, question.id().intValue()); + List answers = question.answers(); + assertEquals(2, answers.size()); + assertEquals(1, answers.get(0).id().intValue()); + assertEquals(2, answers.get(1).id().intValue()); + } + + @Test + public void testInsertAndUpdateAnswer() { + doInJOOQ(sql -> { + sql + .insertInto(ANSWER) + .columns( + ANSWER.ID, + ANSWER.QUESTION_ID, + ANSWER.BODY + ) + .values( + 3L, + 1L, + "Checkout this [video from Toon Koppelaars]" + + "(https://www.youtube.com/watch?v=8jiJDflpw4Y)." + ) + .execute(); + }); + + { + List questions = getUpdatedQuestionsAndAnswers(); + + assertEquals(1, questions.size()); + Question question = questions.get(0); + assertEquals(1, question.id().intValue()); + assertEquals( + "How to call jOOQ stored procedures?", + question.title() + ); + List answers = question.answers(); + assertEquals(3, answers.size()); + assertEquals(1, answers.get(0).id().intValue()); + assertEquals(2, answers.get(1).id().intValue()); + assertEquals(3, answers.get(2).id().intValue()); + } + + doInJOOQ(sql -> { + sql + .update(ANSWER) + .set( + ANSWER.BODY, + "Checkout this [YouTube video from Toon Koppelaars]" + + "(https://www.youtube.com/watch?v=8jiJDflpw4Y)." + ) + .where(ANSWER.ID.eq(3L)) + .execute(); + }); + + List questions = getUpdatedQuestionsAndAnswers(); + + assertEquals(1, questions.size()); + Question question = questions.get(0); + assertEquals(1, question.id().intValue()); + List answers = question.answers(); + assertEquals(3, answers.size()); + assertEquals(1, answers.get(0).id().intValue()); + assertEquals(2, answers.get(1).id().intValue()); + Answer latestAnswer = answers.get(2); + assertEquals(3, latestAnswer.id().intValue()); + assertEquals( + "Checkout this [YouTube video from Toon Koppelaars]" + + "(https://www.youtube.com/watch?v=8jiJDflpw4Y).", + latestAnswer.body() + ); + } + + @Test + public void testInsertQuestion() { + doInJOOQ(sql -> { + sql + .insertInto(QUESTION) + .columns( + QUESTION.ID, + QUESTION.TITLE, + QUESTION.BODY + ) + .values( + 2L, + "How to use the jOOQ MULTISET operator?", + "I want to know how I can use the jOOQ MULTISET operator." + ) + .execute(); + }); + + List questions = getUpdatedQuestionsAndAnswers(); + + assertEquals(1, questions.size()); + Question question = questions.get(0); + assertEquals(2, question.id().intValue()); + assertTrue(question.answers().isEmpty()); + } + + private List getUpdatedQuestionsAndAnswers() { + return doInJOOQ(sql -> { + return sql + .selectFrom(GET_UPDATED_QUESTIONS_AND_ANSWERS.call()) + .collect( + Collectors.collectingAndThen( + Collectors.toMap( + GetUpdatedQuestionsAndAnswersRecord::getQuestionId, + record -> { + Question question = new Question( + record.getQuestionId(), + record.getQuestionTitle(), + record.getQuestionBody(), + record.getQuestionScore(), + record.getQuestionCreatedOn(), + record.getQuestionUpdatedOn(), + new ArrayList<>() + ); + + Long answerId = record.getAnswerId(); + if (answerId != null) { + question.answers().add( + new Answer( + answerId, + record.getAnswerBody(), + record.getAnswerScore(), + record.getAnswerAccepted(), + record.getAnswerCreatedOn(), + record.getAnswerUpdatedOn() + ) + ); + } + + return question; + }, + (Question existing, Question replacement) -> { + existing.answers().addAll(replacement.answers()); + return existing; + }, + LinkedHashMap::new + ), + (Function, List>) map -> new ArrayList<>(map.values()) + ) + ); + }); + } + + private List getUpdatedQuestionsAndAnswersUsingManualMapping() { + return doInJOOQ(sql -> { + Result records = sql + .selectFrom( + GET_UPDATED_QUESTIONS_AND_ANSWERS.call() + ) + .fetch(); + + Map questionsMap = new LinkedHashMap<>(); + + for (GetUpdatedQuestionsAndAnswersRecord record : records) { + Long questionId = record.getQuestionId(); + + Question question = questionsMap.computeIfAbsent( + questionId, + id -> new Question( + questionId, + record.getQuestionTitle(), + record.getQuestionBody(), + record.getQuestionScore(), + record.getQuestionCreatedOn(), + record.getQuestionUpdatedOn(), + new ArrayList<>() + ) + ); + Long answerId = record.getAnswerId(); + if (answerId != null) { + question.answers().add( + new Answer( + answerId, + record.getAnswerBody(), + record.getAnswerScore(), + record.getAnswerAccepted(), + record.getAnswerCreatedOn(), + record.getAnswerUpdatedOn() + ) + ); + } + } + + return new ArrayList<>(questionsMap.values()); + }); + } + + public static record Question( + Long id, + String title, + String body, + int score, + LocalDateTime createdOn, + LocalDateTime updateOn, + List answers) { + } + + public static record Answer( + Long id, + String body, + int score, + boolean accepted, + LocalDateTime createdOn, + LocalDateTime updateOn) { + } +} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/pagination/KeysetPaginationTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/pagination/KeysetPaginationTest.java new file mode 100644 index 000000000..6ae625321 --- /dev/null +++ b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/pagination/KeysetPaginationTest.java @@ -0,0 +1,137 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.pagination; + +import com.vladmihalcea.hpjp.jooq.pgsql.util.AbstractJOOQPostgreSQLIntegrationTest; +import org.jooq.Record3; +import org.jooq.SelectSeekStep2; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.Tables.POST; +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.Tables.POST_DETAILS; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public class KeysetPaginationTest extends AbstractJOOQPostgreSQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Test + public void testPagination() { + String user = "Vlad Mihalcea"; + + doInJOOQ(sql -> { + sql + .deleteFrom(POST_DETAILS) + .execute(); + + sql + .deleteFrom(POST) + .execute(); + + LocalDateTime now = LocalDateTime.now(); + + for (long i = 1; i < 100; i++) { + sql + .insertInto(POST).columns(POST.ID, POST.TITLE) + .values(i, String.format("High-Performance Java Persistence - Chapter %d", i)) + .execute(); + + sql + .insertInto(POST_DETAILS).columns(POST_DETAILS.ID, POST_DETAILS.CREATED_ON, POST_DETAILS.CREATED_BY) + .values(i, now.plusHours(i / 10), user) + .execute(); + } + }); + + doInJOOQ(sql -> { + + int pageSize = 5; + + List results = nextPage(pageSize, null); + + assertEquals(5, results.size()); + + results = nextPage(pageSize, results.get(results.size() - 1)); + + assertEquals(5, results.size()); + }); + + doInJOOQ(sql -> { + + int pageSize = 5; + + PostSummary offsetPostSummary = null; + + int pageCount = 0; + + while (true) { + List results = nextPage(pageSize, offsetPostSummary); + if(results.isEmpty()) { + break; + } + + offsetPostSummary = results.get(results.size() - 1); + pageCount++; + } + + assertEquals(Long.valueOf(1), offsetPostSummary.getId()); + assertEquals(20, pageCount); + }); + } + + public List nextPage(int pageSize, PostSummary offsetPostSummary) { + return doInJOOQ(sql -> { + SelectSeekStep2, LocalDateTime, Long> selectStep = sql + .select(POST.ID, POST.TITLE, POST_DETAILS.CREATED_ON) + .from(POST) + .join(POST_DETAILS).on(POST.ID.eq(POST_DETAILS.ID)) + .orderBy(POST_DETAILS.CREATED_ON.desc(), POST.ID.desc()); + + return (offsetPostSummary != null) + ? selectStep + .seek(offsetPostSummary.getCreatedOn(), offsetPostSummary.getId()) + .limit(pageSize) + .fetchInto(PostSummary.class) + : selectStep + .limit(pageSize) + .fetchInto(PostSummary.class); + }); + } + + /** + * @author Vlad Mihalcea + */ + public static class PostSummary { + + private final Long id; + + private final String title; + + private final LocalDateTime createdOn; + + public PostSummary(Long id, String title, LocalDateTime createdOn) { + this.id = id; + this.title = title; + this.createdOn = createdOn; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + } +} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/upsert/UpsertAndGetConcurrencyTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/upsert/UpsertAndGetConcurrencyTest.java new file mode 100644 index 000000000..c883a11fc --- /dev/null +++ b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/upsert/UpsertAndGetConcurrencyTest.java @@ -0,0 +1,85 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.upsert; + +import com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.tables.records.PostDetailsRecord; +import com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.tables.records.PostRecord; +import com.vladmihalcea.hpjp.jooq.pgsql.util.AbstractJOOQPostgreSQLIntegrationTest; +import com.vladmihalcea.util.exception.ExceptionUtil; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.Sequences.HIBERNATE_SEQUENCE; +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.Tables.POST; +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.tables.PostDetails.POST_DETAILS; +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.val; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +public class UpsertAndGetConcurrencyTest extends AbstractJOOQPostgreSQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + private final CountDownLatch aliceLatch = new CountDownLatch(1); + + @Test + public void testUpsert() { + doInJOOQ(sql -> { + sql.delete(POST_DETAILS).execute(); + sql.delete(POST).execute(); + + PostRecord postRecord = sql + .insertInto(POST).columns(POST.ID, POST.TITLE) + .values(HIBERNATE_SEQUENCE.nextval(), val("High-Performance Java Persistence")) + .returning(POST.ID) + .fetchOne(); + + final Long postId = postRecord.getId(); + + sql + .insertInto(POST_DETAILS) + .columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON) + .values(postId, "Alice", LocalDateTime.now()) + .onDuplicateKeyIgnore() + .execute(); + + final AtomicBoolean preventedByLocking = new AtomicBoolean(); + + executeAsync(() -> { + try { + doInJOOQ(_sql -> { + setJdbcTimeout(_sql.configuration().connectionProvider().acquire()); + + _sql + .insertInto(POST_DETAILS) + .columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON) + .values(postId, "Bob", LocalDateTime.now()) + .onDuplicateKeyIgnore() + .execute(); + }); + } catch (Exception e) { + if( ExceptionUtil.isLockTimeout( e )) { + preventedByLocking.set( true ); + } + } + + aliceLatch.countDown(); + }); + + awaitOnLatch(aliceLatch); + + PostDetailsRecord postDetailsRecord = sql.selectFrom(POST_DETAILS) + .where(field(POST_DETAILS.ID).eq(postId)) + .fetchOne(); + + assertTrue(preventedByLocking.get()); + }); + } +} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/upsert/UpsertAndGetTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/upsert/UpsertAndGetTest.java new file mode 100644 index 000000000..d08e146f4 --- /dev/null +++ b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/upsert/UpsertAndGetTest.java @@ -0,0 +1,51 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.upsert; + +import com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.tables.records.PostDetailsRecord; +import com.vladmihalcea.hpjp.jooq.pgsql.util.AbstractJOOQPostgreSQLIntegrationTest; +import org.jooq.DSLContext; +import org.junit.Test; + +import java.time.LocalDateTime; + +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.Tables.POST; +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.tables.PostDetails.POST_DETAILS; +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.table; + +/** + * @author Vlad Mihalcea + */ +public class UpsertAndGetTest extends AbstractJOOQPostgreSQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Test + public void testUpsert() { + doInJOOQ(sql -> { + sql.delete(POST_DETAILS).execute(); + sql.delete(POST).execute(); + sql + .insertInto(POST).columns(POST.ID, POST.TITLE) + .values(1L, "High-Performance Java Persistence") + .execute(); + + PostDetailsRecord postDetailsRecord = upsertPostDetails(sql, 1L, "Alice", LocalDateTime.now()); + }); + } + + private PostDetailsRecord upsertPostDetails(DSLContext sql, Long id, String owner, LocalDateTime timestamp) { + sql + .insertInto(POST_DETAILS) + .columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON) + .values(id, owner, timestamp) + .onDuplicateKeyIgnore() + .execute(); + + return sql.selectFrom(POST_DETAILS) + .where(field(POST_DETAILS.ID).eq(id)) + .fetchOne(); + } +} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/upsert/UpsertTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/upsert/UpsertTest.java new file mode 100644 index 000000000..74577b1dc --- /dev/null +++ b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/upsert/UpsertTest.java @@ -0,0 +1,54 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.upsert; + +import com.vladmihalcea.hpjp.jooq.pgsql.util.AbstractJOOQPostgreSQLIntegrationTest; +import org.jooq.DSLContext; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.Tables.POST; +import static com.vladmihalcea.hpjp.jooq.pgsql.schema.crud.tables.PostDetails.POST_DETAILS; + +/** + * @author Vlad Mihalcea + */ +public class UpsertTest extends AbstractJOOQPostgreSQLIntegrationTest { + + @Override + protected String ddlScript() { + return "clean_schema.sql"; + } + + @Test + public void testUpsert() { + doInJOOQ(sql -> { + sql.delete(POST_DETAILS).execute(); + sql.delete(POST).execute(); + sql + .insertInto(POST).columns(POST.ID, POST.TITLE) + .values(1L, "High-Performance Java Persistence") + .execute(); + + executeAsync(() -> { + upsertPostDetails(sql, 1L, "Alice", LocalDateTime.now()); + }); + executeAsync(() -> { + upsertPostDetails(sql, 1L, "Bob", LocalDateTime.now()); + }); + + awaitTermination(1, TimeUnit.SECONDS); + }); + } + + private void upsertPostDetails(DSLContext sql, Long id, String owner, LocalDateTime timestamp) { + sql + .insertInto(POST_DETAILS) + .columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON) + .values(id, owner, timestamp) + .onDuplicateKeyUpdate() + .set(POST_DETAILS.UPDATED_BY, owner) + .set(POST_DETAILS.UPDATED_ON, timestamp) + .execute(); + } +} diff --git a/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/util/AbstractJOOQPostgreSQLIntegrationTest.java b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/util/AbstractJOOQPostgreSQLIntegrationTest.java new file mode 100644 index 000000000..3c0507b66 --- /dev/null +++ b/jooq/jooq-pgsql/src/test/java/com/vladmihalcea/hpjp/jooq/pgsql/util/AbstractJOOQPostgreSQLIntegrationTest.java @@ -0,0 +1,26 @@ +package com.vladmihalcea.hpjp.jooq.pgsql.util; + +import com.vladmihalcea.hpjp.jooq.AbstractJOOQIntegrationTest; +import com.vladmihalcea.util.providers.DataSourceProvider; +import com.vladmihalcea.util.providers.PostgreSQLDataSourceProvider; +import org.jooq.SQLDialect; + +/** + * @author Vlad Mihalcea + */ +public abstract class AbstractJOOQPostgreSQLIntegrationTest extends AbstractJOOQIntegrationTest { + + @Override + protected String ddlFolder() { + return "pgsql"; + } + + @Override + protected SQLDialect sqlDialect() { + return SQLDialect.POSTGRES; + } + + protected DataSourceProvider dataSourceProvider() { + return new PostgreSQLDataSourceProvider(); + } +} diff --git a/jooq/jooq-pgsql/src/test/resources/pgsql/clean_schema.sql b/jooq/jooq-pgsql/src/test/resources/pgsql/clean_schema.sql new file mode 100644 index 000000000..e265a4a88 --- /dev/null +++ b/jooq/jooq-pgsql/src/test/resources/pgsql/clean_schema.sql @@ -0,0 +1,10 @@ +DELETE FROM post_tag; +DELETE FROM tag; +DELETE FROM post_details; +DELETE FROM post_comment_details; +DELETE FROM post_comment; +DELETE FROM post; +DELETE FROM answer; +DELETE FROM question; + +ALTER SEQUENCE hibernate_sequence RESTART; \ No newline at end of file diff --git a/jooq/jooq-pgsql/src/test/resources/pgsql/initial_schema.sql b/jooq/jooq-pgsql/src/test/resources/pgsql/initial_schema.sql index 00a89b788..e6bc06dd1 100644 --- a/jooq/jooq-pgsql/src/test/resources/pgsql/initial_schema.sql +++ b/jooq/jooq-pgsql/src/test/resources/pgsql/initial_schema.sql @@ -4,15 +4,150 @@ drop table if exists post_details; drop table if exists post_tag; drop table if exists post; drop table if exists tag; +drop table if exists answer; +drop table if exists question; +drop table if exists cache_snapshot; -create table post (id int8 not null, title varchar(255), primary key (id)); -create table post_comment (id int8 not null, review varchar(255), post_id int8, primary key (id)); -create table post_details (id int8 not null, created_by varchar(255), created_on timestamp, updated_by varchar(255), updated_on timestamp, primary key (id)); +drop sequence if exists hibernate_sequence; + +create table post (id int8 not null, title varchar(250), primary key (id)); +create table post_comment (id int8 not null, review varchar(250), post_id int8, primary key (id)); +create table post_details (id int8 not null, created_by varchar(250), created_on timestamp, updated_by varchar(250), updated_on timestamp, primary key (id)); create table post_tag (post_id int8 not null, tag_id int8 not null); -create table tag (id int8 not null, name varchar(255), primary key (id)); +create table tag (id int8 not null, name varchar(50), primary key (id)); create table post_comment_details (id int8 not null, post_id int8 not null, user_id int8 not null, ip varchar(18) not null, fingerprint varchar(256), primary key (id)); -alter table post_comment add constraint FKna4y825fdc5hw8aow65ijexm0 foreign key (post_id) references post; -alter table post_details add constraint FKkl5eik513p1xiudk2kxb0v92u foreign key (id) references post; -alter table post_tag add constraint FKac1wdchd2pnur3fl225obmlg0 foreign key (tag_id) references tag; -alter table post_tag add constraint FKc2auetuvsec0k566l0eyvr9cs foreign key (post_id) references post; \ No newline at end of file +create table question (id bigint not null, body text, created_on timestamp default now(), score integer not null default 0, title varchar(250), updated_on timestamp default now(), primary key (id)); +create table answer (id bigint not null, accepted boolean not null default false, body text, created_on timestamp default now(), score integer not null default 0, updated_on timestamp default now(), question_id bigint, primary key (id)); + +create table cache_snapshot (region varchar(250), updated_on timestamp, primary key (region)); + +alter table post_comment add constraint post_comment_post_id foreign key (post_id) references post; +alter table post_details add constraint post_details_post_id foreign key (id) references post; +alter table post_tag add constraint post_tag_tag_id foreign key (tag_id) references tag; +alter table post_tag add constraint post_tag_post_id foreign key (post_id) references post; + +alter table if exists answer add constraint answer_question_id foreign key (question_id) references question; + +create sequence hibernate_sequence start with 1 increment by 1; + +drop function if exists get_updated_questions_and_answers; + +CREATE OR REPLACE FUNCTION get_updated_questions_and_answers() +RETURNS TABLE( + question_id bigint, question_title varchar(250), question_body text, + question_score integer, question_created_on timestamp, question_updated_on timestamp, + answer_id bigint, answer_body text, answer_accepted boolean, + answer_score integer, answer_created_on timestamp, answer_updated_on timestamp +) +LANGUAGE plpgsql +AS $$ +DECLARE +previous_snapshot_timestamp timestamp; + max_snapshot_timestamp timestamp; + result_set_record record; +BEGIN + previous_snapshot_timestamp = ( + SELECT + updated_on + FROM + cache_snapshot + WHERE + region = 'QA' + FOR NO KEY UPDATE + ); + IF previous_snapshot_timestamp is null THEN + INSERT INTO cache_snapshot( + region, + updated_on + ) + VALUES ( + 'QA', + to_timestamp(0) + ); + + previous_snapshot_timestamp = to_timestamp(0); + END IF; + + max_snapshot_timestamp = to_timestamp(0); + FOR result_set_record IN( + SELECT + q1.id as question_id, q1.title as question_title, + q1.body as question_body, q1.score as question_score, + q1.created_on as question_created_on, q1.updated_on as question_updated_on, + a1.id as answer_id, a1.body as answer_body, + a1.accepted as answer_accepted, a1.score as answer_score, + a1.created_on as answer_created_on, a1.updated_on as answer_updated_on + FROM + question q1 + LEFT JOIN + answer a1 on q1.id = a1.question_id + WHERE + q1.id IN ( + SELECT q2.id + FROM question q2 + WHERE + q2.updated_on > previous_snapshot_timestamp + ) OR + q1.id IN ( + SELECT a2.question_id + FROM answer a2 + WHERE + a2.updated_on > previous_snapshot_timestamp + ) + ORDER BY + question_created_on, answer_created_on + ) loop + IF result_set_record.question_updated_on > max_snapshot_timestamp THEN + max_snapshot_timestamp = result_set_record.question_updated_on; + END IF; + IF result_set_record.answer_updated_on > max_snapshot_timestamp THEN + max_snapshot_timestamp = result_set_record.answer_updated_on; + END IF; + + question_id = result_set_record.question_id; + question_title = result_set_record.question_title; + question_body = result_set_record.question_body; + question_score = result_set_record.question_score; + question_created_on = result_set_record.question_created_on; + question_updated_on = result_set_record.question_updated_on; + answer_id = result_set_record.answer_id; + answer_body = result_set_record.answer_body; + answer_accepted = result_set_record.answer_accepted; + answer_score = result_set_record.answer_score; + answer_created_on = result_set_record.answer_created_on; + answer_updated_on = result_set_record.answer_updated_on; + RETURN next; +END loop; + +UPDATE + cache_snapshot +SET updated_on = max_snapshot_timestamp +WHERE + region = 'QA'; +END +$$ +; + +drop function if exists set_updated_on_timestamp; + +CREATE FUNCTION set_updated_on_timestamp() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_on = now(); + RETURN NEW; +END +$$ +language 'plpgsql' +; + +drop trigger if exists question_set_updated_on_trigger on question; +drop trigger if exists answer_set_updated_on_trigger on answer; + +CREATE TRIGGER question_set_updated_on_trigger +BEFORE UPDATE OR DELETE ON question +FOR EACH ROW EXECUTE FUNCTION set_updated_on_timestamp(); + +CREATE TRIGGER answer_set_updated_on_trigger +BEFORE UPDATE OR DELETE ON answer +FOR EACH ROW EXECUTE FUNCTION set_updated_on_timestamp(); \ No newline at end of file diff --git a/jooq/pom.xml b/jooq/pom.xml index 021a5b317..8cbd08151 100644 --- a/jooq/pom.xml +++ b/jooq/pom.xml @@ -4,34 +4,23 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0%20http://maven.apache.org/xsd/maven-4.0.0.xsd"> high-performance-java-persistence - com.vladmihalcea.book + com.vladmihalcea 1.0-SNAPSHOT 4.0.0 - jooq + high-performance-java-persistence-jooq pom jooq-core jooq-mysql jooq-pgsql - jooq-oracle - jooq-mssql jooq-pgsql-score - - - com.vladmihalcea.book - high-performance-java-persistence-core - 1.0-SNAPSHOT - test-jar - test - - - - 3.8.4 + 3.17.7 + 1.1.1 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2ed44f638..7d478f0a0 100644 --- a/pom.xml +++ b/pom.xml @@ -3,37 +3,44 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0%20http://maven.apache.org/xsd/maven-4.0.0.xsd"> - - org.sonatype.oss - oss-parent - 7 - - 4.0.0 - com.vladmihalcea.book + com.vladmihalcea high-performance-java-persistence 1.0-SNAPSHOT pom core - + jooq - - - maven.oracle.com - - true - - - false - - https://maven.oracle.com - default - - + + + hypersistence + + + io.hypersistence + hypersistence-optimizer + ${hypersistence-optimizer.version} + jakarta + test + + + + + + + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + @@ -55,28 +62,50 @@ ${commons-lang.version} + + jakarta.persistence + jakarta.persistence-api + ${jakarta.persistence.version} + + - org.hibernate + org.hibernate.orm hibernate-core ${hibernate.version} - org.hibernate + org.hibernate.orm + hibernate-testing + ${hibernate.version} + + + org.apache.logging.log4j + log4j-core + + + log4j + log4j + + + + + + org.hibernate.orm hibernate-c3p0 ${hibernate.version} - org.hibernate + org.hibernate.orm hibernate-hikaricp ${hibernate.version} - org.hibernate + org.hibernate.orm hibernate-spatial ${hibernate.version} @@ -88,17 +117,23 @@ - org.hibernate - hibernate-validator - ${hibernate.validator.version} + org.hibernate.orm + hibernate-jpamodelgen + ${hibernate.version} - org.hibernate - hibernate-jpamodelgen + org.hibernate.orm + hibernate-envers ${hibernate.version} + + org.hibernate.validator + hibernate-validator + ${hibernate.validator.version} + + javax.el javax.el-api @@ -112,17 +147,36 @@ - org.hibernate - hibernate-ehcache + javax.servlet + javax.servlet-api + 3.0.1 + + + + org.hibernate.orm + hibernate-jcache ${hibernate.version} - + jakarta + + + org.glassfish.jaxb + jaxb-runtime + + + + + + org.ehcache + ehcache-transactions + ${ehcache.version} + jakarta + @@ -153,15 +207,16 @@ - com.oracle.jdbc - ojdbc8 - ${oracle.version} + com.mysql + mysql-connector-j + ${mysql.version} + test - mysql - mysql-connector-java - ${mysql.version} + org.mariadb.jdbc + mariadb-java-client + ${mariadb.version} test @@ -173,16 +228,23 @@ - net.sourceforge.jtds - jtds - ${jtds.version} + com.oracle.database.jdbc + ojdbc8 + ${oracle.version} + test + + + + com.yugabyte + jdbc-yugabytedb + ${yugabytedb.version} test io.dropwizard.metrics metrics-core - ${codahale.metrics.version} + ${dropwizard.metrics.version} @@ -195,19 +257,32 @@ slf4j-api - provided - org.codehaus.btm - btm - ${btm.version} - - - org.slf4j - slf4j-api - - + org.jboss.narayana.jta + narayana-jta-jakarta + ${narayana-jta.version} + + + + dev.snowdrop + narayana-spring-boot-starter + ${narayana-ds.version} + + + + com.atomikos + transactions-jta + ${atomikos.version} + jakarta + + + + com.atomikos + transactions-jdbc + ${atomikos.version} + jakarta @@ -218,20 +293,26 @@ com.vladmihalcea.flexy-pool - flexy-pool-core + flexy-hikaricp ${flexy-pool.version} - com.vladmihalcea.flexy-pool - flexy-btm - ${flexy-pool.version} + io.hypersistence + hypersistence-utils-hibernate-63 + ${hypersistence-utils.version} + + + org.glassfish.jaxb + jaxb-runtime + + - com.vladmihalcea - db-util - ${db-util.version} + io.hypersistence + hypersistence-tsid + ${hypersistence-tsid.version} @@ -274,12 +355,60 @@ ${spring.version} + + org.springframework.data + spring-data-jpa + ${spring-data.version} + + + + org.springframework.data + spring-data-envers + ${spring-data.version} + + + + org.springframework + spring-web + ${spring.version} + + + + org.springframework + spring-instrument + ${spring.version} + + com.fasterxml.jackson.core jackson-databind ${jackson.version} + + javax.xml.bind + jaxb-api + ${jaxb-api.version} + + + + com.sun.xml.bind + jaxb-impl + ${jaxb-api.version} + + + + com.sun.xml.bind + jaxb-core + ${jaxb-api.version} + + + + javax.annotation + javax.annotation-api + 1.3.1 + + @@ -289,6 +418,13 @@ test + + org.junit.vintage + junit-vintage-engine + ${junit-vintage.engine.version} + test + + org.springframework spring-test @@ -297,91 +433,152 @@ test + + + + org.testcontainers + mysql + test + + + + org.testcontainers + postgresql + test + + + + org.testcontainers + oracle-xe + test + + + + org.testcontainers + mssqlserver + test + + + + org.testcontainers + mariadb + test + + + + org.testcontainers + yugabytedb + test + + + + org.testcontainers + cockroachdb + test + + + + org.antlr + antlr4 + ${antlr.version} + + - 1.7.7 - 1.1.2 - 3.3.2 - 5.2.10.Final - 5.2.3.Final - 3.18.1-GA - 2.6.9 - 1.3.3 - 2.1.4 - 2.3.2 - 9.4-1202-jdbc41 - 12.2.0.1 - - 5.1.38 - 6.1.0.jre8 - 1.3.1 + 17 + 3.0.0 + 3.8.0 + 2.22.2 + 2.4 + + UTF-8 + + ${cmd.args} + + 2.0.13 + 1.5.6 + 3.17.0 + 3.1.0 + 6.6.33.Final + 6.2.5.Final + 3.18.1-GA + 3.10.8 + 1.10 + 3.9.1 + + 1.18.3 + 2.7.2 + 42.7.7 + 23.4.0.24.05 + 9.2.0 + 3.3.3 + 12.6.1.jre11 + 42.3.0 + + 5.12.6.Final + 3.2.0 + 6.0.0 + + 2.2.7 2.2.4 1.1 - 3.1.0 - 1.3.3 - 2.1.4 - 1.2.4 + 3.1.0 + 5.0.1 + 3.0.2 + + 1.9.19 + 6.2.11 + 3.5.4 - 1.8.7 - 4.3.3.RELEASE + 2.14.3 - 2.7.4 + 6.4.4 + 3.9.2 + 2.1.4 - 3.2.1 - 0.0.1 + 1.6.13 - 4.12 + 2.8.1 + + 4.13.2 + 5.10.3 + + 4.13.1 + + + Spring + + true + + https://repo.spring.io/milestone + + + + org.apache.maven.plugins maven-compiler-plugin - 3.2 + ${maven-compiler-plugin.version} - 1.8 - 1.8 - -proc:none + ${jdk.version} - - org.codehaus.mojo - properties-maven-plugin - - - - set-system-properties - - - - - - - - - - org.apache.maven.plugins maven-surefire-plugin - 2.19.1 + ${maven-surefire-plugin.version} - -Xms1024m -Xmx1024m - - - ${project.build.directory}/btm1.log - ${project.build.directory}/btm2.log - + ${argLine} - \ No newline at end of file +