3 Masalah Umum Kinerja Hibernasi dan Cara Menemukannya di File Log Anda

1. Perkenalan

Anda mungkin pernah membaca beberapa keluhan tentang kinerja Hibernate yang buruk atau mungkin Anda sendiri yang mengalami masalah dengan beberapa di antaranya. Saya telah menggunakan Hibernate selama lebih dari 15 tahun sekarang dan saya telah mengalami lebih dari cukup masalah ini.

Selama bertahun-tahun, saya telah belajar bahwa masalah ini dapat dihindari dan Anda dapat menemukannya banyak di file log Anda. Dalam posting ini, saya ingin menunjukkan kepada Anda bagaimana Anda dapat menemukan dan memperbaiki 3 di antaranya.

2. Temukan dan Perbaiki Masalah Kinerja

2.1. Catat Pernyataan SQL dalam Produksi

Masalah kinerja pertama sangat mudah dikenali dan sering diabaikan. Ini pencatatan pernyataan SQL di lingkungan produksi.

Menulis beberapa pernyataan log kedengarannya bukan masalah besar, dan ada banyak aplikasi di luar sana yang melakukan hal itu. Tetapi ini sangat tidak efisien, terutama melalui System.out.println seperti yang dilakukan Hibernate jika Anda mengatur parameter show_sql dalam konfigurasi Hibernate Anda ke true :

Hibernate: select order0_.id as id1_2_, order0_.orderNumber as orderNum2_2_, order0_.version as version3_2_ from purchaseOrder order0_ Hibernate: select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.order_id as order_id4_0_1_, items0_.product_id as product_5_0_1_, items0_.quantity as quantity2_0_1_, items0_.version as version3_0_1_ from OrderItem items0_ where items0_.order_id=? Hibernate: select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.order_id as order_id4_0_1_, items0_.product_id as product_5_0_1_, items0_.quantity as quantity2_0_1_, items0_.version as version3_0_1_ from OrderItem items0_ where items0_.order_id=? Hibernate: select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.order_id as order_id4_0_1_, items0_.product_id as product_5_0_1_, items0_.quantity as quantity2_0_1_, items0_.version as version3_0_1_ from OrderItem items0_ where items0_.order_id=?

Di salah satu proyek saya, saya meningkatkan kinerja sebesar 20% dalam beberapa menit dengan menyetel show_sql ke false . Itu adalah jenis pencapaian yang ingin Anda laporkan pada stand-up meeting berikutnya ?

Sangat jelas bagaimana Anda dapat memperbaiki masalah kinerja ini. Buka saja konfigurasi Anda (misalnya file persistence.xml Anda) dan setel parameter show_sql ke false . Anda tidak memerlukan informasi ini dalam produksi.

Tetapi Anda mungkin membutuhkannya selama pengembangan. Jika tidak, Anda menggunakan 2 konfigurasi Hibernate yang berbeda (yang seharusnya tidak Anda lakukan), Anda juga menonaktifkan pencatatan pernyataan SQL di sana. Solusi untuk itu adalah dengan menggunakan 2 konfigurasi log yang berbeda untuk pengembangan dan produksi yang dioptimalkan untuk persyaratan khusus lingkungan runtime.

Konfigurasi Pengembangan

Konfigurasi pengembangan harus menyediakan informasi berguna sebanyak mungkin sehingga Anda dapat melihat bagaimana Hibernate berinteraksi dengan database. Oleh karena itu, Anda setidaknya harus mencatat pernyataan SQL yang dihasilkan dalam konfigurasi pengembangan Anda. Anda dapat melakukan ini dengan mengaktifkan pesan DEBUG untuk kategori org.hibernate.SQL . Jika Anda juga ingin melihat nilai parameter mengikat Anda, Anda harus menyetel tingkat log org.hibernate.type.descriptor.sql ke TRACE :

log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n log4j.rootLogger=info, stdout # basic log level for all messages log4j.logger.org.hibernate=info # SQL statements and parameters log4j.logger.org.hibernate.SQL=debug log4j.logger.org.hibernate.type.descriptor.sql=trace

Potongan kode berikut menunjukkan beberapa pesan log contoh yang ditulis Hibernate dengan konfigurasi log ini. Seperti yang Anda lihat, Anda mendapatkan informasi mendetail tentang kueri SQL yang dieksekusi dan semua nilai parameter yang ditetapkan dan diambil:

23:03:22,246 DEBUG SQL:92 - select order0_.id as id1_2_, order0_.orderNumber as orderNum2_2_, order0_.version as version3_2_ from purchaseOrder order0_ where order0_.id=1 23:03:22,254 TRACE BasicExtractor:61 - extracted value ([id1_2_] : [BIGINT]) - [1] 23:03:22,261 TRACE BasicExtractor:61 - extracted value ([orderNum2_2_] : [VARCHAR]) - [order1] 23:03:22,263 TRACE BasicExtractor:61 - extracted value ([version3_2_] : [INTEGER]) - [0]

Hibernate memberi Anda lebih banyak informasi internal tentang Sesi jika Anda mengaktifkan statistik Hibernate. Anda dapat melakukan ini dengan menyetel properti sistem hibernate.generate_statistics ke true.

Tapi tolong, aktifkan statistik pada pengembangan atau lingkungan pengujian Anda. Mengumpulkan semua informasi ini memperlambat aplikasi Anda dan Anda mungkin membuat masalah kinerja Anda sendiri jika Anda mengaktifkannya dalam produksi.

Anda dapat melihat beberapa contoh statistik dalam cuplikan kode berikut:

23:04:12,123 INFO StatisticalLoggingSessionEventListener:258 - Session Metrics { 23793 nanoseconds spent acquiring 1 JDBC connections; 0 nanoseconds spent releasing 0 JDBC connections; 394686 nanoseconds spent preparing 4 JDBC statements; 2528603 nanoseconds spent executing 4 JDBC statements; 0 nanoseconds spent executing 0 JDBC batches; 0 nanoseconds spent performing 0 L2C puts; 0 nanoseconds spent performing 0 L2C hits; 0 nanoseconds spent performing 0 L2C misses; 9700599 nanoseconds spent executing 1 flushes (flushing a total of 9 entities and 3 collections); 42921 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections) }

Saya secara teratur menggunakan statistik ini dalam pekerjaan harian saya untuk menemukan masalah kinerja sebelum terjadi dalam produksi dan saya dapat menulis beberapa posting tentang itu. Jadi mari kita fokus pada yang paling penting.

Baris 2 sampai 5 menunjukkan kepada Anda berapa banyak koneksi JDBC dan pernyataan Hibernate yang digunakan selama sesi ini dan berapa banyak waktu yang dihabiskan untuk itu. Anda harus selalu melihat nilai-nilai ini dan membandingkannya dengan harapan Anda.

Jika ada lebih banyak pernyataan daripada yang Anda harapkan, kemungkinan besar Anda mengalami masalah kinerja yang paling umum, masalah pilih n + 1. Anda dapat menemukannya di hampir semua aplikasi, dan ini mungkin menciptakan masalah kinerja yang sangat besar pada database yang lebih besar. Saya menjelaskan masalah ini lebih detail di bagian selanjutnya.

Baris 7 sampai 9 menunjukkan bagaimana Hibernate berinteraksi dengan cache tingkat ke-2. Ini adalah salah satu dari 3 cache Hibernate, dan menyimpan entitas dalam cara sesi independen. Jika Anda menggunakan level kedua dalam aplikasi Anda, Anda harus selalu memantau statistik ini untuk melihat apakah Hibernate mendapatkan entitas dari sana.

Konfigurasi Produksi

Konfigurasi produksi harus dioptimalkan untuk kinerja dan menghindari pesan apa pun yang tidak segera diperlukan. Secara umum, itu berarti Anda hanya mencatat pesan kesalahan. Jika Anda menggunakan Log4j, Anda dapat mencapainya dengan konfigurasi berikut:

Jika Anda menggunakan Log4j, Anda dapat mencapainya dengan konfigurasi berikut:

log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n log4j.rootLogger=info, stdout # basic log level for all messages log4j.logger.org.hibernate=error

2.2. N + 1 Pilih Masalah

Seperti yang sudah saya jelaskan, masalah pilih n + 1 adalah masalah kinerja yang paling umum. Banyak pengembang menyalahkan konsep Pemetaan ATAU untuk masalah ini, dan mereka tidak sepenuhnya salah. Tetapi Anda dapat dengan mudah menghindarinya jika Anda memahami bagaimana Hibernate memperlakukan hubungan yang diambil dengan malas. Oleh karena itu, pengembang juga harus disalahkan karena merupakan tanggung jawabnya untuk menghindari masalah semacam ini. Jadi izinkan saya menjelaskan terlebih dahulu mengapa masalah ini ada dan kemudian menunjukkan kepada Anda cara mudah untuk mencegahnya. Jika Anda sudah terbiasa dengan masalah pilih n + 1, Anda dapat langsung beralih ke solusi.

Hibernate menyediakan pemetaan yang sangat nyaman untuk hubungan antar entitas. Anda hanya memerlukan atribut dengan jenis entitas terkait dan beberapa penjelasan untuk menentukannya:

@Entity @Table(name = "purchaseOrder") public class Order implements Serializable { @OneToMany(mappedBy = "order", fetch = FetchType.LAZY) private Set items = new HashSet(); ... }

Saat Anda sekarang memuat entitas Order dari database, Anda hanya perlu memanggil metode getItems () untuk mendapatkan semua item dari order ini. Hibernate menyembunyikan query database yang diperlukan untuk mendapatkan entitas OrderItem terkait dari database.

Ketika Anda memulai dengan Hibernate, Anda mungkin belajar bahwa Anda harus menggunakan FetchType.LAZY untuk sebagian besar hubungan dan itu default untuk hubungan ke banyak. Ini memberitahu Hibernate untuk hanya mengambil entitas terkait jika Anda menggunakan atribut yang memetakan hubungan. Mengambil hanya data yang Anda butuhkan adalah hal yang baik secara umum, tetapi juga membutuhkan Hibernate untuk menjalankan kueri tambahan untuk menginisialisasi setiap hubungan. Ini dapat menghasilkan sejumlah besar kueri, jika Anda mengerjakan daftar entitas, seperti yang saya lakukan di cuplikan kode berikut:

List orders = em.createQuery("SELECT o FROM Order o").getResultList(); for (Order order : orders) { log.info("Order: " + order.getOrderNumber()); log.info("Number of items: " + order.getItems().size()); }

Anda mungkin tidak akan berharap bahwa beberapa baris kode ini dapat membuat ratusan atau bahkan ribuan kueri database. Tapi itu terjadi jika Anda menggunakan FetchType.LAZY untuk hubungan ke entitas OrderItem :

22:47:30,065 DEBUG SQL:92 - select order0_.id as id1_2_, order0_.orderNumber as orderNum2_2_, order0_.version as version3_2_ from purchaseOrder order0_ 22:47:30,136 INFO NamedEntityGraphTest:58 - Order: order1 22:47:30,140 DEBUG SQL:92 - select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.order_id as order_id4_0_1_, items0_.product_id as product_5_0_1_, items0_.quantity as quantity2_0_1_, items0_.version as version3_0_1_ from OrderItem items0_ where items0_.order_id=? 22:47:30,171 INFO NamedEntityGraphTest:59 - Number of items: 2 22:47:30,171 INFO NamedEntityGraphTest:58 - Order: order2 22:47:30,172 DEBUG SQL:92 - select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.order_id as order_id4_0_1_, items0_.product_id as product_5_0_1_, items0_.quantity as quantity2_0_1_, items0_.version as version3_0_1_ from OrderItem items0_ where items0_.order_id=? 22:47:30,174 INFO NamedEntityGraphTest:59 - Number of items: 2 22:47:30,174 INFO NamedEntityGraphTest:58 - Order: order3 22:47:30,174 DEBUG SQL:92 - select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.order_id as order_id4_0_1_, items0_.product_id as product_5_0_1_, items0_.quantity as quantity2_0_1_, items0_.version as version3_0_1_ from OrderItem items0_ where items0_.order_id=? 22:47:30,176 INFO NamedEntityGraphTest:59 - Number of items: 2

Hibernate performs one query to get all Order entities and an additional one for each of the n Order entities to initialize the orderItem relationship. So you now know why this kind of issue is called n+1 select issue and why it can create huge performance problems.

What makes it even worse is, that you often don’t recognize it on a small test database, if you haven’t checked your Hibernate statistics. The code snippet requires only a few dozen queries if the test database doesn’t contain a lot of orders. But that will be completely different if you use your productive database which contains several thousand of them.

I said earlier that you can easily avoid these issues. And that’s true. You just have to initialize the orderItem relationship when you select the Order entities from the database.

But please, only do that, if you use the relationship in your business code and don’t use FetchType.EAGER to always fetch the related entities. That just replaces your n+1 issue with another performance problem.

Initialize a Relationships with a @NamedEntityGraph

There are several different options to initialize relationships. I prefer to use a @NamedEntityGraph which is is one of my favorite features introduced in JPA 2.1. It provides a query independent way to specify a graph of entities which Hibernate shall fetch from the database. In following code snippet, you can see an example of a simple graph that lets Hibernate eagerly fetch the items attribute of an entity:

@Entity @Table(name = "purchase_order") @NamedEntityGraph( name = "graph.Order.items", attributeNodes = @NamedAttributeNode("items")) public class Order implements Serializable { ... }

There isn’t much you need to do to define an entity graph with a @NamedEntityGraph annotation. You just have to provide a unique name for the graph and one @NamedAttributeNode annotation for each attribute Hibernate shall fetch eagerly. In this example, it’s only the items attribute which maps the relationship between an Order and several OrderItem entities.

Now you can use the entity graph to control the fetching behaviour or a specific query. You, therefore, have to instantiate an EntityGraph based on the @NamedEntityGraph definition and provide it as a hint to the EntityManager.find() method or your query. I do this in the following code snippet where I select the Order entity with id 1 from the database:

EntityGraph graph = this.em.getEntityGraph("graph.Order.items"); Map hints = new HashMap(); hints.put("javax.persistence.fetchgraph", graph); return this.em.find(Order.class, 1L, hints);

Hibernate uses this information to create one SQL statement which gets the attributes of the Order entity and the attributes of the entity graph from the database:

17:34:51,310 DEBUG [org.hibernate.loader.plan.build.spi.LoadPlanTreePrinter] (pool-2-thread-1) LoadPlan(entity=blog.thoughts.on.java.jpa21.entity.graph.model.Order) - Returns - EntityReturnImpl( entity=blog.thoughts.on.java.jpa21.entity.graph.model.Order, querySpaceUid=, path=blog.thoughts.on.java.jpa21.entity.graph.model.Order) - CollectionAttributeFetchImpl( collection=blog.thoughts.on.java.jpa21.entity.graph.model.Order.items, querySpaceUid=, path=blog.thoughts.on.java.jpa21.entity.graph.model.Order.items) - (collection element) CollectionFetchableElementEntityGraph( entity=blog.thoughts.on.java.jpa21.entity.graph.model.OrderItem, querySpaceUid=, path=blog.thoughts.on.java.jpa21.entity.graph.model.Order.items.) - EntityAttributeFetchImpl(entity=blog.thoughts.on.java.jpa21.entity.graph.model.Product, querySpaceUid=, path=blog.thoughts.on.java.jpa21.entity.graph.model.Order.items..product) - QuerySpaces - EntityQuerySpaceImpl(uid=, entity=blog.thoughts.on.java.jpa21.entity.graph.model.Order) - SQL table alias mapping - order0_ - alias suffix - 0_ - suffixed key columns - {id1_2_0_} - JOIN (JoinDefinedByMetadata(items)) :  ->  - CollectionQuerySpaceImpl(uid=, collection=blog.thoughts.on.java.jpa21.entity.graph.model.Order.items) - SQL table alias mapping - items1_ - alias suffix - 1_ - suffixed key columns - {order_id4_2_1_} - entity-element alias suffix - 2_ - 2_entity-element suffixed key columns - id1_0_2_ - JOIN (JoinDefinedByMetadata(elements)) :  ->  - EntityQuerySpaceImpl(uid=, entity=blog.thoughts.on.java.jpa21.entity.graph.model.OrderItem) - SQL table alias mapping - items1_ - alias suffix - 2_ - suffixed key columns - {id1_0_2_} - JOIN (JoinDefinedByMetadata(product)) :  ->  - EntityQuerySpaceImpl(uid=, entity=blog.thoughts.on.java.jpa21.entity.graph.model.Product) - SQL table alias mapping - product2_ - alias suffix - 3_ - suffixed key columns - {id1_1_3_} 17:34:51,311 DEBUG [org.hibernate.loader.entity.plan.EntityLoader] (pool-2-thread-1) Static select for entity blog.thoughts.on.java.jpa21.entity.graph.model.Order [NONE:-1]: select order0_.id as id1_2_0_, order0_.orderNumber as orderNum2_2_0_, order0_.version as version3_2_0_, items1_.order_id as order_id4_2_1_, items1_.id as id1_0_1_, items1_.id as id1_0_2_, items1_.order_id as order_id4_0_2_, items1_.product_id as product_5_0_2_, items1_.quantity as quantity2_0_2_, items1_.version as version3_0_2_, product2_.id as id1_1_3_, product2_.name as name2_1_3_, product2_.version as version3_1_3_ from purchase_order order0_ left outer join OrderItem items1_ on order0_.id=items1_.order_id left outer join Product product2_ on items1_.product_id=product2_.id where order0_.id=?

Initializing only one relationship is good enough for a blog post but in a real project, you will most likely want to build more complex graphs. So let's do that.

You can, of course, provide an array of @NamedAttributeNode annotations to fetch multiple attributes of the same entity and you can use @NamedSubGraph to define the fetching behaviour for an additional level of entities. I use that in the following code snippet to fetch not only all related OrderItem entities but also the Product entity for each OrderItem:

@Entity @Table(name = "purchase_order") @NamedEntityGraph( name = "graph.Order.items", attributeNodes = @NamedAttributeNode(value = "items", subgraph = "items"), subgraphs = @NamedSubgraph(name = "items", attributeNodes = @NamedAttributeNode("product"))) public class Order implements Serializable { ... }

As you can see, the definition of a @NamedSubGraph is very similar to the definition of a @NamedEntityGraph. You can then reference this subgraph in a @NamedAttributeNode annotation to define the fetching behaviour for this specific attribute.

The combination of these annotations allows you to define complex entity graphs which you can use to initialize all relationships you use in your use case and avoid n+1 select issues. If you want to specify your entity graph dynamically at runtime, you can do this also via a Java API.

2.3. Update Entities One by One

Updating entities one by one feels very natural if you think in an object oriented way. You just get the entities you want to update and call a few setter methods to change their attributes like you do it with any other object.

This approach works fine if you only change a few entities. But it gets very inefficient when you work with a list of entities and is the third performance issues you can easily spot in your log file. You just have to look for a bunch SQL UPDATE statements that look completely the same, as you can see in the following log file:

22:58:05,829 DEBUG SQL:92 - select product0_.id as id1_1_, product0_.name as name2_1_, product0_.price as price3_1_, product0_.version as version4_1_ from Product product0_ 22:58:05,883 DEBUG SQL:92 - update Product set name=?, price=?, version=? where id=? and version=? 22:58:05,889 DEBUG SQL:92 - update Product set name=?, price=?, version=? where id=? and version=? 22:58:05,891 DEBUG SQL:92 - update Product set name=?, price=?, version=? where id=? and version=? 22:58:05,893 DEBUG SQL:92 - update Product set name=?, price=?, version=? where id=? and version=? 22:58:05,900 DEBUG SQL:92 - update Product set name=?, price=?, version=? where id=? and version=?

The relational representation of the database records is a much better fit for these use cases than the object oriented one. With SQL, you could just write one SQL statement that updates all the records you want to change.

You can do the same with Hibernate if you use JPQL, native SQL or the CriteriaUpdate API. All 3 of very similar, so let’s use JPQL in this example.

You can define a JPQL UPDATE statement in a similar way as you know it from SQL. You just define which entity you want to update, how to change the values of its attributes and limit the affected entities in the WHERE statement.

You can see an example of it in the following code snippet where I increase the price of all products by 10%:

em.createQuery("UPDATE Product p SET p.price = p.price*0.1").executeUpdate();

Hibernate creates an SQL UPDATE statement based on the JPQL statement and sends it to the database which performs the update operation.

It’s pretty obvious that this approach is a lot faster if you have to update a huge number entities. But it also has a drawback. Hibernate doesn’t know which entities are affected by the update operation and doesn’t update its 1st level cache. You should, therefore, make sure not to read and update an entity with a JPQL statement within the same Hibernate Session or you have to detach it to remove it from the cache.

3. Summary

Within this post, I've shown you 3 Hibernate performance issues which you can find in your log files.

2 di antaranya disebabkan oleh sejumlah besar pernyataan SQL. Ini adalah alasan umum untuk masalah kinerja, jika Anda bekerja dengan Hibernate. Hibernate menyembunyikan akses database di balik API-nya dan itu sering membuat sulit untuk menebak jumlah sebenarnya dari pernyataan SQL. Oleh karena itu, Anda harus selalu memeriksa pernyataan SQL yang dieksekusi saat membuat perubahan pada tingkat persistensi Anda.