JPA — Dynamic Spring Projection

Description: This application is a sample of using dynamic Spring projections.

Key points:

  • declare query-methods in a generic manner (e.g., <T> List<T> findByGenre(String genre, Class<T> type);)
public interface AuthorGenreDto {
public String getGenre();
}

AuthorNameEmailDto.java

public interface AuthorNameEmailDto {
public String getName();
public String getEmail();
}

Author.java

@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Author implements Serializable {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
private String genre;
private int age;
private String email;
private String address;
private String rating;
}

AuthorRepository.java

@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {

<T> List<T> findByGenre(String genre, Class<T> type);

<T> T findByName(String name, Class<T> type);

@Query("SELECT a FROM Author a WHERE a.name=?1 AND a.age=?2")
<T> T findByNameAndAge(String name, int age, Class<T> type);

}

BookStoreService.java

@Service
@RequiredArgsConstructor
public class BookstoreService {

private final AuthorRepository authorRepository;

@Transactional(readOnly = true)
public void fetchAuthorByNameAsEntityJpql() {
Author author = authorRepository.findByName("Joana Nimar", Author.class);
System.out.println(author);
}

@Transactional(readOnly = true)
public void fetchAuthorByNameAsDtoNameEmailJpql() {
AuthorNameEmailDto author = authorRepository.findByName("Joana Nimar", AuthorNameEmailDto.class);
System.out.println(author.getEmail() + ", " + author.getName());
}

@Transactional(readOnly = true)
public void fetchAuthorByNameAsDtoGenreJpql() {
AuthorGenreDto author = authorRepository.findByName("Joana Nimar", AuthorGenreDto.class);
System.out.println(author.getGenre());
}

@Transactional(readOnly = true)
public void fetchAuthorByNameAndAgeAsEntityJpql() {
Author author = authorRepository.findByNameAndAge("Joana Nimar", 34, Author.class);
System.out.println(author);
}

@Transactional(readOnly = true)
public void fetchAuthorByNameAndAgeAsDtoNameEmailJpql() {
AuthorNameEmailDto author = authorRepository.findByNameAndAge("Joana Nimar", 34, AuthorNameEmailDto.class);
System.out.println(author.getEmail() + ", " + author.getName());
}

@Transactional(readOnly = true)
public void fetchAuthorByNameAndAgeAsDtoGenreJpql() {
AuthorGenreDto author = authorRepository.findByNameAndAge("Joana Nimar", 34, AuthorGenreDto.class);
System.out.println(author.getGenre());
}

@Transactional(readOnly = true)
public void fetchAuthorsAsEntitiesJpql() {
List<Author> authors = authorRepository.findByGenre("Anthology", Author.class);
System.out.println(authors);
}

@Transactional(readOnly = true)
public void fetchAuthorsAsDtoJpql() {
List<AuthorNameEmailDto> authors = authorRepository.findByGenre("Anthology", AuthorNameEmailDto.class);
authors.forEach(a -> System.out.println(a.getName() + ", " + a.getEmail()));
}
}

MainApp.java

@SpringBootApplication
public class MainApplication {

@Autowired
private BookstoreService bookstoreService;

public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}

@Bean
public ApplicationRunner init() {
return args -> {
System.out.println("\n\n Calling fetchAuthorByNameAsEntityJpql():");
bookstoreService.fetchAuthorByNameAsEntityJpql();

System.out.println("\n\n Calling fetchAuthorByNameAsDtoNameEmailJpql():");
bookstoreService.fetchAuthorByNameAsDtoNameEmailJpql();

System.out.println("\n\n Calling fetchAuthorByNameAsDtoGenreJpql():");
bookstoreService.fetchAuthorByNameAsDtoGenreJpql();

System.out.println("\n\n Calling fetchAuthorByNameAndAgeAsEntityJpql():");
bookstoreService.fetchAuthorByNameAndAgeAsEntityJpql();

System.out.println("\n\n Calling fetchAuthorByNameAndAgeAsDtoNameEmailJpql():");
bookstoreService.fetchAuthorByNameAndAgeAsDtoNameEmailJpql();

System.out.println("\n\n Calling fetchAuthorByNameAndAgeAsDtoGenreJpql():");
bookstoreService.fetchAuthorByNameAndAgeAsDtoGenreJpql();

System.out.println("\n\n Calling fetchAuthorsAsEntitiesJpql():");
bookstoreService.fetchAuthorsAsEntitiesJpql();

System.out.println("\n\n Calling fetchAuthorsAsDtoJpql():");
bookstoreService.fetchAuthorsAsDtoJpql();
};
}
}

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/bookstoredb?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect

spring.jpa.open-in-view=false

spring.datasource.initialization-mode=always
spring.datasource.platform=mysql

data-mysql.sql

insert into author (age, name, genre, email, address, rating, id) values (23, "Mark Janel", "Anthology", "markj@gmail.com", "mark's address", 99, 1);
insert into author (age, name, genre, email, address, rating, id) values (43, "Olivia Goy", "Horror", "oliviag@gmail.com", "olivia's address", 89, 2);
insert into author (age, name, genre, email, address, rating, id) values (51, "Quartis Young", "Anthology", "young@gmail.com", "quartis's address", 84, 3);
insert into author (age, name, genre, email, address, rating, id) values (34, "Joana Nimar", "History", "jn@gmail.com", "joana's address", 95, 4);
insert into author (age, name, genre, email, address, rating, id) values (33, "Marin Kyrab", "History", "marin@gmail.com", "marin's address", 82, 5);

Console output

Calling fetchAuthorByNameAsEntityJpql():
Hibernate: select author0_.id as id1_0_, author0_.address as address2_0_, author0_.age as age3_0_, author0_.email as email4_0_, author0_.genre as genre5_0_, author0_.name as name6_0_, author0_.rating as rating7_0_ from author author0_ where author0_.name=?
Author(id=4, name=Joana Nimar, genre=History, age=34, email=jn@gmail.com, address=joana’s address, rating=95)

Calling fetchAuthorByNameAsDtoNameEmailJpql():
Hibernate: select author0_.name as col_0_0_, author0_.email as col_1_0_ from author author0_ where author0_.name=?
jn@gmail.com, Joana Nimar

Calling fetchAuthorByNameAsDtoGenreJpql():
Hibernate: select author0_.genre as col_0_0_ from author author0_ where author0_.name=?
History

Calling fetchAuthorByNameAndAgeAsEntityJpql():
Hibernate: select author0_.id as id1_0_, author0_.address as address2_0_, author0_.age as age3_0_, author0_.email as email4_0_, author0_.genre as genre5_0_, author0_.name as name6_0_, author0_.rating as rating7_0_ from author author0_ where author0_.name=? and author0_.age=?
Author(id=4, name=Joana Nimar, genre=History, age=34, email=jn@gmail.com, address=joana’s address, rating=95)

Calling fetchAuthorByNameAndAgeAsDtoNameEmailJpql():
Hibernate: select author0_.id as id1_0_, author0_.address as address2_0_, author0_.age as age3_0_, author0_.email as email4_0_, author0_.genre as genre5_0_, author0_.name as name6_0_, author0_.rating as rating7_0_ from author author0_ where author0_.name=? and author0_.age=?
jn@gmail.com, Joana Nimar

Calling fetchAuthorByNameAndAgeAsDtoGenreJpql():
Hibernate: select author0_.id as id1_0_, author0_.address as address2_0_, author0_.age as age3_0_, author0_.email as email4_0_, author0_.genre as genre5_0_, author0_.name as name6_0_, author0_.rating as rating7_0_ from author author0_ where author0_.name=? and author0_.age=?
History

Calling fetchAuthorsAsEntitiesJpql():
Hibernate: select author0_.id as id1_0_, author0_.address as address2_0_, author0_.age as age3_0_, author0_.email as email4_0_, author0_.genre as genre5_0_, author0_.name as name6_0_, author0_.rating as rating7_0_ from author author0_ where author0_.genre=?
[Author(id=1, name=Mark Janel, genre=Anthology, age=23, email=markj@gmail.com, address=mark’s address, rating=99), Author(id=3, name=Quartis Young, genre=Anthology, age=51, email=young@gmail.com, address=quartis’s address, rating=84)]

Calling fetchAuthorsAsDtoJpql():
Hibernate: select author0_.name as col_0_0_, author0_.email as col_1_0_ from author author0_ where author0_.genre=?
Mark Janel, markj@gmail.com
Quartis Young, young@gmail.com

— — — — — — — — — — — — — — — —

How To Enable Dirty Tracking In A Spring Boot Application

Note: The Hibernate Dirty Checking mechanism is responsible to identify the entities modifications at flush-time and to trigger the corresponding UPDATE statements in our behalf.

Description: Prior to Hibernate version 5, the Dirty Checking mechanism relies on Java Reflection API for checking every property of every managed entity. Starting with Hibernate version 5, the Dirty Checking mechanism can rely on the Dirty Tracking mechanism (which is the capability of an entity to track its own attributes changes) which requires Hibernate Bytecode Enhancement to be present in the application. The Dirty Tracking mechanism sustain a better performance, especially when you have a relatively large number of entitites.

For Dirty Tracking, during Bytecode Enhancement process, the entity classes bytecode is instrumented by Hibernate by adding a tracker, \$\$_hibernate_tracker. At flush time, Hibernate will use this tracker to discover the entities changes (each entity tracker will report the changes). This is better than checking every property of every managed entity.

Commonly (by default), the instrumentation takes place at build-time, but it can be configured to take place at runtime or deploy-time as well. It is preferable to take place at build-time for avoiding an overhead in the runtime.

Adding Bytecode Enhancement and enabling Dirty Tracking can be done via a plugin added via Maven or Gradle (Ant can be used as well). We use Maven, therefore we add it in pom.xml.

Key points:

  • Hibernate come with Bytecode Enhancement plugins for Maven, Gradle (Ant can be used as well)
  • for Maven, add the Bytecode Enhancement plugin in the pom.xml file

Output example:

The Bytecode Enhancement effect can be seen on Author.class here. Notice how the bytecode was instrumented with \$\$_hibernate_tracker.

Author.java

@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Author implements Serializable {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
private String genre;
private int age;
}

AuthorRepository.java

@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
}

BookStoreService.java

@Service
@RequiredArgsConstructor
public class BookstoreService {

private final AuthorRepository authorRepository;

@Transactional
public void saveAndUpdateUser() {
Author author = new Author();
author.setName("Joana Nimar");
author.setGenre("History");
author.setAge(34);

authorRepository.save(author);
// dirty checking !!
author.setAge(35);
}
}

MainApp.java

@SpringBootApplication
@RequiredArgsConstructor
public class MainApplication {
private final BookstoreService userService;

public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}

@Bean
public ApplicationRunner init() {
return args -> { userService.saveAndUpdateUser(); };
}
}

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/bookstoredb?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

spring.jpa.open-in-view=false

Console output —

Hibernate: insert into author (age, genre, name) values (?, ?, ?)
Hibernate: update author set age=?, genre=?, name=? where id=?

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — -

How To Include In The UPDATE Statement Only The Modified Columns Via Hibernate @DynamicUpdate

Description: This application is an example of using the Hibernate-specific, @DynamicUpdate. By default, even if we modify only a subset of columns, the triggered UPDATE statements will include all columns. By simply annotating the corresponding entity at class-level with @DynamicUpdate the generated UPDATE statement will include only the modified columns.

Key points:

  • pro: avoid updating unmodified indexes (useful for heavy indexing)
  • con: cannot reuse the same UPDATE for different subsets of columns via JDBC statements caching (each triggered UPDATE string will be cached and reused accordingly)

Author.java

@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
@DynamicUpdate
public class Author implements Serializable {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
private String genre;
private int age;
private int sellrank;
private int royalties;
private int rating;
}

AuthorRepository.java

@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
}

BookStoreService.java

@Service
@RequiredArgsConstructor
public class BookstoreService {

private final AuthorRepository authorRepository;

@Transactional
public void updateAuthor() {
Author author = authorRepository.findById(1L).get();
author.setSellrank(222);
}
}

MainApp

@SpringBootApplication
@RequiredArgsConstructor
public class MainApplication {

private final BookstoreService bookstoreService;

public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}

@Bean
public ApplicationRunner init() {
return args -> {
bookstoreService.updateAuthor();
};
}
}

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/bookstoredb?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

spring.jpa.open-in-view=false

spring.datasource.initialization-mode=always
spring.datasource.platform=mysql

logging.level.org.hibernate.type.descriptor.sql=TRACE

data-mysql.java

insert into author (age, name, genre, royalties, sellrank, rating, id) values (23, "Mark Janel", "Anthology", 1200, 289, 3, 1);
insert into author (age, name, genre, royalties, sellrank, rating, id) values (43, "Olivia Goy", "Horror", 4000, 490, 5, 2);
insert into author (age, name, genre, royalties, sellrank, rating, id) values (51, "Quartis Young", "Anthology", 900, 122, 4, 3);
insert into author (age, name, genre, royalties, sellrank, rating, id) values (34, "Joana Nimar", "History", 5600, 554, 4, 4);
insert into author (age, name, genre, royalties, sellrank, rating, id) values (47, "Kakki Jou", "Anthology", 1000, 231, 5, 5);
insert into author (age, name, genre, royalties, sellrank, rating, id) values (56, "Fair Pouille", "Anthology", 3400, 344, 5, 6);

Hibernate: select author0_.id as id1_0_0_, author0_.age as age2_0_0_, author0_.genre as genre3_0_0_, author0_.name as name4_0_0_, author0_.rating as rating5_0_0_, author0_.royalties as royaltie6_0_0_, author0_.sellrank as sellrank7_0_0_ from author author0_ where author0_.id=?
2021–06–18 13:42:37.503 TRACE 8428 — — [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] — [1]
2021–06–18 13:42:37.518 TRACE 8428 — — [ main] o.h.type.descriptor.sql.BasicExtractor : extracted value ([age2_0_0_] : [INTEGER]) — [23]
2021–06–18 13:42:37.521 TRACE 8428 — — [ main] o.h.type.descriptor.sql.BasicExtractor : extracted value ([genre3_0_0_] : [VARCHAR]) — [Anthology]
2021–06–18 13:42:37.521 TRACE 8428 — — [ main] o.h.type.descriptor.sql.BasicExtractor : extracted value ([name4_0_0_] : [VARCHAR]) — [Mark Janel]
2021–06–18 13:42:37.521 TRACE 8428 — — [ main] o.h.type.descriptor.sql.BasicExtractor : extracted value ([rating5_0_0_] : [INTEGER]) — [3]
2021–06–18 13:42:37.521 TRACE 8428 — — [ main] o.h.type.descriptor.sql.BasicExtractor : extracted value ([royaltie6_0_0_] : [INTEGER]) — [1200]
2021–06–18 13:42:37.522 TRACE 8428 — — [ main] o.h.type.descriptor.sql.BasicExtractor : extracted value ([sellrank7_0_0_] : [INTEGER]) — [289]
Hibernate: update author set sellrank=? where id=?
2021–06–18 13:42:37.545 TRACE 8428 — — [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [INTEGER] — [222]
2021–06–18 13:42:37.545 TRACE 8428 — — [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [BIGINT] — [1]