Modeling your domain model with JPA is quite easy and for a smaller amount of data, you can easily rely on the default configuration of JPA. But when it comes to performance issues in your application you need a deeper understanding of JPA and Hibernate. One big performance boost could be Lazy Loading. When it comes to Lazy Loading, most of the developers think of using this optimization for entity relationships like @OneToMany/@OneToOne/@ManyToMany
, which is commonly used to disable the loading of the whole entity graph within fetching one entity. Another, not widely used, optimization is the Lazy Loading of attributes for a JPA entity, which I'll demonstrate in this blog post.
JPA entity setup for lazy loading
Let's assume you are writing a web application and you want to store the file uploads of your users to your database (notwithstanding the fact this is a good idea or not).
A possible JPA entity for this task could look like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Data // Lombok annotation to generate constructor/getter/setter... @Entity public class FileUpload { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String fileName; private String fileType; private byte[] fileContent; @CreationTimestamp // Hibernate annotation private LocalDateTime uploadedAt; } |
One possible use case might be to inspect all uploaded files from a user in Java code. Therefore you don't want to load all the byte array representation of the files in your memory at first. To lazy load the fileContent
you have to modify the attribute and add the following @Basic(fetch = FetchType.LAZY)
annotation:
1 2 3 4 5 6 7 8 9 10 11 | @Data // Lombok annotation to generate constructor/getter/setter... @Entity public class FileUpload { // ... same as above @Lob @Basic(fetch = FetchType.LAZY) private byte[] fileContent; } |
With just this annotation, Hibernate wouldn't be able to lazy load the file content per default. To get this running you have to enhance the bytecode of your JPA entity. Use the following Maven plugin from Hibernate to achieve this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <plugin> <groupId>org.hibernate.orm.tooling</groupId> <artifactId>hibernate-enhance-maven-plugin</artifactId> <version>${hibernate.version}</version> <executions> <execution> <configuration> <failOnError>true</failOnError> <enableLazyInitialization>true</enableLazyInitialization> </configuration> <goals> <goal>enhance</goal> </goals> </execution> </executions> </plugin> |
After running mvn clean compile
or mvn clean package/install
the FileUpload
class file contains some more fields and methods with the prefix $$_hibernate_ like the following:
1 2 3 4 5 6 7 | public byte[] $$_hibernate_read_fileContent() { /* compiled code */ } public void $$_hibernate_write_fileContent(byte[] bytes) { /* compiled code */ } |
With this enhancement, Hibernate is now able to lazy load access to the fileContent
. To demonstrate this, I'll use a small Spring Boot app where I store the application.properties
file in an in-memory database (H2) on startup and query for it in another class. For visualization of the lazy loading, I enabled the logging of the SQL statements:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | @Component @Order(1) @Slf4j public class StoreFileOnStartup implements CommandLineRunner { @PersistenceContext private EntityManager entityManager; @Override @Transactional public void run(String... args) throws Exception { File file = new File(getClass().getClassLoader().getResource("application.properties").getFile()); byte[] fileBytes = new byte[(int) file.length()]; FileInputStream fis = new FileInputStream(file); fis.read(fileBytes); FileUpload fileUpload = new FileUpload(); fileUpload.setFileName(file.getName()); fileUpload.setFileType("Property file"); fileUpload.setFileContent(fileBytes); entityManager.persist(fileUpload); entityManager.flush(); log.info("--- File successfully stored to the database"); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | @Component @Order(2) @Slf4j public class ReadFileOnStartup implements CommandLineRunner { @PersistenceContext private EntityManager entityManager; @Transactional @Override public void run(String... args) throws Exception { log.info("--- Loading file from database"); FileUpload allFileUploads = entityManager.find(FileUpload.class, 1L); log.info("--- File successfully loaded from database"); log.info("--- Accessing fileContent"); byte[] content = allFileUploads.getFileContent(); log.info("--- the file has: " + content.length + " bytes"); } } |
When running the Spring Boot application the following log is produced:
1 2 3 4 5 6 7 8 | Hibernate: insert into file_upload (id, file_content, file_name, file_type, uploaded_at) values (null, ?, ?, ?, ?) INFO 12992 --- [main] d.r.b.l.StoreFileOnStartup: --- File successfully stored to the database INFO 12992 --- [main] d.r.b.l.ReadFileOnStartup: --- Loading file from database Hibernate: select fileupload0_.id as id1_0_0_, fileupload0_.file_name as file_nam3_0_0_, fileupload0_.file_type as file_typ4_0_0_, fileupload0_.uploaded_at as uploaded5_0_0_ from file_upload fileupload0_ where fileupload0_.id=? INFO 12992 --- [main] d.r.b.l.ReadFileOnStartup: --- File successfully loaded from database INFO 12992 --- [main] d.r.b.l.ReadFileOnStartup: --- Accessing fileContent Hibernate: select fileupload_.file_content as file_con2_0_ from file_upload fileupload_ where fileupload_.id=? INFO 12992 --- [main] d.r.b.l.ReadFileOnStartup: --- the file has: 55 bytes |
You can see that Hibernate created three SQL queries, one for the insert, another for the select. The third is for accessing the fileContent
.
The source code for this demo project is available on GitHub.
PS: This was one of the many great tips/hints/insights about JPA and Hibernate I got from the excellent book High-Performance Java Persistence from @vlad_mihalcea. Read my review of his video course here.
Happy Lazy Loading with JPA,
Phil