如何使用Hibernate与关系数据库进行交互

by Emre Savcı

由EmreSavcı

处理深潜关系,延迟加载和N + 1问题 (Dealing with Deep Dive Relations, Lazy Loading, and the N+1 Problem)

Hibernate is an Object Relational Mapping tool which allows us to use objects for interaction with relational databases. It has many features like code first modeling, lazy loading, change tracking, caching, auditing etc.

Hibernate是一个对象关系映射工具,它使我们能够使用对象与关系数据库进行交互。 它具有许多功能,例如代码优先建模,延迟加载,更改跟踪,缓存,审核等。

In this post I will present and discuss the relationships OneToMany and ManyToOne. Also we will see what are the consequences of building the wrong relationships or using the wrong fetch type. I will also examine Bidirectional and Unidirectional relations.

在这篇文章中,我将介绍并讨论OneToManyManyToOne的关系 我们还将看到建立错误关系或使用错误提取类型的后果。 我还将研究双向和单向关系。

一对多 (OneToMany)

Let’s start with the OneToMany UniDirectional relationship. Imagine that we have a Shipment entity and a shipment may contain many Items.

让我们从OneToMany UniDirectional关系开始。 想象一下,我们有一个Shipment实体,而一个货运可能包含许多Item

Note: the below code is just here to show you the straightforward way implementing a relationship, but it is not recommended to use in production. I will explain at the end of code examples.

注意:下面的代码只是在这里向您展示实现关系的简单方法,但是不建议在生产环境中使用。 我将在代码示例的末尾进行解释。

First our persistence.xml:

首先我们的persistence.xml:

<?xml version="1.0" encoding="UTF-8"?><persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence             http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">    <persistence-unit name="testConfig" transaction-type="RESOURCE_LOCAL">        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>        <class>deneme.Shipment</class>        <class>deneme.Item</class>        <properties>            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/jpadb"/>            <property name="javax.persistence.jdbc.user" value="root"/>            <property name="javax.persistence.jdbc.password" value="root"/>            <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>            <property name="hibernate.logging.level" value="FINE"/>            <property name="hibernate.show_sql" value="true"/>            <property name="hibernate.format_sql" value="true"/>            <property name="hibernate.ddl-generation" value="auto"/>            <property name="hibernate.hbm2ddl.auto" value="update"/>        </properties>    </persistence-unit></persistence>

And our pom.xml dependencies:

还有我们的pom.xml依赖项:

<dependencies><dependency>    <groupId>org.projectlombok</groupId>    <artifactId>lombok</artifactId>    <version>1.18.2</version>    <scope>provided</scope></dependency>
<dependency>    <groupId>org.hibernate</groupId>    <artifactId>hibernate-entitymanager</artifactId>    <version>5.3.3.Final</version></dependency><!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-core --><dependency>    <groupId>org.hibernate</groupId>    <artifactId>hibernate-core</artifactId>    <version>5.3.3.Final</version></dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --><dependency>    <groupId>mysql</groupId>    <artifactId>mysql-connector-java</artifactId>    <version>5.1.6</version></dependency><dependencies>

Here are our entities:

这是我们的实体:

@Entity@Table(name = "shipments")@Getter@Setterclass Shipment {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private int id;    private String cargoName;    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)    @JoinTable(name = "shipment_items")    private List<Item> childList;        public void addItem(Item item) {        if (childList == null) {            childList = new ArrayList();        }        childList.add(item);    }}@Entity@Table(name = "items")@Getter@Setterclass Item {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private int id;    private String itemName;}

Let’s elaborate upon the annotations which are defined in our entities.

让我们详细说明一下我们实体中定义的注释。

@Entity annotation tells Hibernate to evaluation of our class as an entity which makes Hibernate keep track of it in PersistenceContext.

@Entity注释告诉Hibernate作为一个实体来评估我们的类,这使Hibernate在PersistenceContext中对其进行跟踪

@Table annotation tells Hibernate what is our database table name.

@ Table注释告诉Hibernate我们的数据库表名称是什么。

@Getter and @Setter annotations come from Lombok to eliminate boilerplate code.

来自Lombok的 @ Getter和@ Setter注释消除了样板代码。

@OneToMany (fetch = FetchType.LAZY, cascade = CascadeType.ALL) annotation defines how to fetch child objects from the databases using the parameter FetchType.Lazy. CascadeType.All means that it cascades all operations from parent to child.

@ 一对多 (取= FetchType。 懒惰 ,级联= CascadeType的。ALL)注释定义如何从使用参数FetchType.Lazy数据库获取子对象。 CascadeType.All意味着它将所有操作从父级层叠到子级。

@JoinTable (name = "shipment_items") annotation defines the owner of the relationship which is the shipment entity is our case. It creates an intermediate table to hold relations.

@ JoinTable (name =“ shipment_items”)批注定义关系的所有者,这是装运实体的情况。 它创建一个中间表来保存关系。

Because we use OneToMany annotation only in the parent side, it is called a Unidirectional relationship. Hence we can not acquire the parent object from the child-side.

因为我们仅在父侧使用OneToMany批注,所以将其称为单向关系。 因此,我们不能从子端获取父对象。

Now we need to acquire EntityManager from EntityManagerFactory to perform database operations.

现在,我们需要从获得的EntityManagerFactory 的EntityManager来执行数据库操作。

Now we’ll see how to save our entities.

现在,我们将了解如何保存我们的实体。

public class Main {    public static void main(String[] args) {        EntityManagerFactory emfactory = Persistence.createEntityManagerFactory("testConfig");        EntityManager entitymanager = emfactory.createEntityManager();        Shipment shipment = new Shipment();        shipment.setCargoName("test cargo");        Item item = new Item();        item.setItemName("test item");        shipment.addItem(item);        entitymanager.close();        emfactory.close();    }}

When we save our entities the first time, Hibernate creates corresponding tables and relations. Let’s see what queries are generated by Hibernate:

当我们第一次保存实体时,Hibernate创建相应的表和关系。 让我们看看Hibernate生成了哪些查询:

Hibernate: create table items (id int identity not null, itemName varchar(255), primary key (id))
Hibernate: create table shipment_items (Shipment_id int not null, childList_id int not null)
Hibernate: create table shipments (id int identity not null, cargoName varchar(255), primary key (id))
Hibernate: alter table shipment_items drop constraint UK_8ipo6hqqte0sdoftcqflf3hie
Hibernate: alter table shipment_items add constraint UK_8ipo6hqqte0sdoftcqflf3hie unique (childList_id)
Hibernate: alter table shipment_items add constraint FK20iu625dsmyisso2hrcc2qpk foreign key (childList_id) references items
Hibernate: alter table shipment_items add constraint FK6nronhhlbku40g81rfte2p02t foreign key (Shipment_id) references shipments

In our database, when we run the queries:

在我们的数据库中,当我们运行查询时:

SELECT * FROM shipments
SELECT * FROM items
SELECT * FROM shipment_items

It returns empty results:

它返回空结果:

Why? Because we did not persist out entities into entityManager.

为什么? 因为我们没有将实体持久化到entityManager中。

Let’s persist them:

让我们坚持下去:

shipment.addItem(item);entitymanager.persist(shipment);

When we run the code again and check the database, we’ll see that there is still no data. That’s because of we did not start any transaction. We need to start a transaction and then commit it just after persisting.

当我们再次运行代码并检查数据库时,我们将看到仍然没有数据。 那是因为我们没有开始任何交易。 我们需要启动一个事务,然后在持久化之后立即提交它。

entitymanager.getTransaction().begin();entitymanager.persist(shipment);entitymanager.getTransaction().commit();

When we run it again, we’ll see that Hibernate generates queries which save our entities into the database:

再次运行它时,我们会看到Hibernate生成查询,这些查询将我们的实体保存到数据库中:

Hibernate: insert into shipments (cargoName) values (?)
Hibernate: insert into items (itemName) values (?)
Hibernate: insert into shipment_items (Shipment_id, childList_id) values (?, ?)

Here we see that our data is in the database:

在这里,我们看到我们的数据在数据库中:

You remember that we use CascadeType.ALL in OneToMany annotation — so why did we use that? What happens if we don’t specify it?

您还记得我们在OneToMany批注中使用CascadeType.ALL -为什么要使用它? 如果不指定该怎么办?

Let’s remove it from the annotation:

让我们将其从注释中删除:

@OneToMany(fetch = FetchType.LAZY)@JoinTable(name = "shipment_items")private List<Item> childList;

And persist our entity again:

并再次坚持我们的实体:

entitymanager.getTransaction().begin();entitymanager.persist(shipment);entitymanager.getTransaction().commit();

We see two thing here: one is an exception thrown by Hibernate and the other is that there is no SQL statement generated for saving our parent-child relationship when we remove the cascade type parameter. And of course, there is no data written into the database.

我们在这里看到两件事:一是Hibernate抛出的异常,另一是当删除级联类型参数时,没有生成用于保存父子关系SQL语句。 当然,没有数据写入数据库。

What we need to do? Is using the CascadeType.ALL is the only way to go? Of course not, but it’s the recommended way according to this use case.

我们需要做什么? 使用CascadeType.ALL是唯一的方法吗? 当然不是,但这是根据此用例推荐的方式。

For more information about cascade types take a look here.

有关级联类型的更多信息,请在这里查看

We can save the child entity explicitly and see that our relationship is saved smoothly:

我们可以显式保存子实体,并看到我们的关系可以顺利保存:

entitymanager.getTransaction().begin();entitymanager.persist(item);entitymanager.persist(shipment);entitymanager.getTransaction().commit();

Now Hibernate generates the correct insert statements and we see our data in the database:

现在,Hibernate生成了正确的插入语句,我们在数据库中看到了我们的数据:

Hibernate: insert into items (itemName) values (?)
Hibernate: insert into shipments (cargoName) values (?)
Hibernate: insert into shipment_items (Shipment_id, childList_id) values (?, ?)

处理保存的数据 (Dealing with saved data)

Okay, we have seen how to save entities in the form of parent-child relationships into the database. Now it’s time to retrieve the data which we saved.

好的,我们已经看到了如何将父子关系形式的实体保存到数据库中。 现在是时候检索我们保存的数据了。

Shipment shipment = entitymanager.find(Shipment.class, 1);System.out.println(shipment.getCargoName());

When we look at the output we’ll see the generated query and the getCargoName() return value:

当我们查看输出时,将看到生成的查询和getCargoName()返回值:

Hibernate: select shipment0_.id as id1_2_0_, shipment0_.cargoName as cargoNam2_2_0_ from shipments shipment0_ where shipment0_.id=?
test cargo

No interesting things here. But let’s print the size of childList using getChildList().size() on our shipment object.

这里没有有趣的事情。 但是,让我们在运送对象上使用getChildList().size()打印childList的getChildList().size()

Shipment shipment = entitymanager.find(Shipment.class, 1);System.out.println(shipment.getCargoName());System.out.println("Size of childList : " + shipment.getChildList().size());

When we run the code we’ll see an additional query which fetches the child objects in the output:

运行代码时,我们将看到一个附加查询,该查询在输出中获取子对象:

Hibernate:
SELECTshipment0_.id AS id1_2_0_,shipment0_.cargoName AS cargoNam2_2_0_FROM shipments shipment0_WHERE shipment0_.id = ?
test cargo //the below query generated after calling getChildList() method
Hibernate:
SELECTchildlist0_.Shipment_id AS Shipment1_1_0_,childlist0_.childList_id AS childLis2_1_0_,item1_.id AS id1_0_1_,item1_.itemName AS itemName2_0_1_FROM shipment_items childlist0_INNER JOIN items item1_ON childlist0_.childList_id = item1_.idWHERE childlist0_.Shipment_id = ?
Size of childList : 1

What happened here is known as Lazy Loading. Because we defined fetch = FetchType.Lazy in @OneToMany annotation when we loaded ther shipment object the first time, it does not load child entities together.

这里发生的事情称为“ 延迟加载” 。 因为我们在第一次加载运输对象时fetch = FetchType.Lazy in @OneToMany批注中定义了fetch = FetchType.Lazy in @OneToMany ,所以它不会将子实体一起加载。

In lazy relationships, a child entity loads only when you access it the first time.

在惰性关系中,子实体仅在您首次访问时才加载。

Now let’s see the eager version — we’ll change the Lazy parameter to Eager:

现在,让我们看看渴望的版本-我们将Lazy参数更改为Eager:

@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)

Run the code again and see the output:

再次运行代码,然后查看输出:

Hibernate:
SELECTshipment0_.id AS id1_2_0_,shipment0_.cargoName AS cargoNam2_2_0_,childlist1_.Shipment_id AS Shipment1_1_1_,item2_.id AS childLis2_1_1_,item2_.id AS id1_0_2_,item2_.itemName AS itemName2_0_2_FROM shipments shipment0_LEFT OUTER JOIN shipment_items childlist1_ON shipment0_.id = childlist1_.Shipment_idLEFT OUTER JOIN items item2_ON childlist1_.childList_id = item2_.idWHERE shipment0_.id = ?
test cargoSize of childList : 1

We see that there is not any additional query. It fetched all of it as a whole. (Now you can discard that change — it was just to show you what happens under the hood. We’ll keep going with lazy loading in the examples.)

我们看到没有任何其他查询。 它从整体上获取了所有内容。 (现在您可以放弃所做的更改,这只是为了向您展示幕后发生的事情。在示例中,我们将继续进行延迟加载。)

Now let’s try to add an Item to the existing shipment.

现在,让我们尝试将项目添加到现有货件中。

Shipment shipment = entitymanager.find(Shipment.class, 1);Item item = new Item();item.setItemName("item to existing shipment");shipment.addItem(item);entitymanager.getTransaction().begin();entitymanager.persist(shipment);entitymanager.getTransaction().commit();

Generated queries:

生成的查询:

Hibernate:     select        shipment0_.id as id1_2_0_,        shipment0_.cargoName as cargoNam2_2_0_     from        shipments shipment0_     where        shipment0_.id=?
Hibernate:     select        childlist0_.Shipment_id as Shipment1_1_0_,        childlist0_.childList_id as childLis2_1_0_,        item1_.id as id1_0_1_,        item1_.itemName as itemName2_0_1_     from        shipment_items childlist0_     inner join        items item1_             on childlist0_.childList_id=item1_.id     where        childlist0_.Shipment_id=?
Hibernate:     insert     into        items        (itemName)     values        (?)
Hibernate:     insert     into        shipment_items        (Shipment_id, childList_id)     values        (?, ?)

It selects a shipment, then fetches the child entity with a join query, inserts the child entity to a table and inserts ids into intermediate table which hold relationships.

它选择一个货件,然后通过联接查询获取子实体,将子实体插入表中,并将ID插入具有关系的中间表中。

Okay, when we think about it, it looks and works as expected. Now run the code again to add another item to the shipment.

好的,当我们考虑它时,它的外观和效果都可以预期。 现在再次运行代码以将另一个项目添加到货件中。

Now take a look closer at the generated queries, especially the bold ones:

现在仔细看一下生成的查询,尤其是粗体查询:

Hibernate:     select        shipment0_.id as id1_2_0_,        shipment0_.cargoName as cargoNam2_2_0_     from        shipments shipment0_     where        shipment0_.id=?
Hibernate:     select        childlist0_.Shipment_id as Shipment1_1_0_,        childlist0_.childList_id as childLis2_1_0_,        item1_.id as id1_0_1_,        item1_.itemName as itemName2_0_1_     from        shipment_items childlist0_     inner join        items item1_             on childlist0_.childList_id=item1_.id     where        childlist0_.Shipment_id=?
Hibernate:     insert     into        items        (itemName)     values        (?)
Hibernate:     delete     from        shipment_items     where        Shipment_id=?
Hibernate:     insert     into        shipment_items        (Shipment_id, childList_id)     values        (?, ?)
Hibernate:     insert     into        shipment_items        (Shipment_id, childList_id)     values        (?, ?)

What happened here? We see one delete and two insert at the end of our queries.

这里发生了什么? 我们在查询末尾看到一个删除和两个插入。

  • It selects a shipment

    选择货件
  • Fetches the child with a join query

    通过联接查询获取子级
  • Inserts the new item

    插入新项目
  • Deletes all previous relationships from the intermediate table

    从中间表中删除所有先前的关系

  • Inserts all relationships from zero

    从零插入所有关系

Which is why it generates two insert statements at the end. Now I think you can imagine what happens if we have thousands of child items. Of course, we will not use this technique. It is a bottleneck for performance .

这就是为什么它在末尾生成两个插入语句的原因。 现在,我想您可以想象如果我们有成千上万的子项会发生什么。 当然,我们不会使用这种技术。 这是性能瓶颈

In the above approach, we use an intermediate table to hold relationships. As you have noticed, it generates additional queries for inserting relationships. Because we defined a JoinTable annotation on the shipment entity…

在上述方法中,我们使用中间表来保存关系。 您已经注意到,它会生成用于插入关系的其他查询。 因为我们在货运实体上定义了JoinTable批注...

用另一种方式做:ManyToOne (Doing it another way: ManyToOne)

How we avoid this situation? Well, by using the JoinColumn annotation instead of JoinTable.Let’s see how to implement it with the JoinColumn annotation.

我们如何避免这种情况? 好吧,通过使用JoinColumn批注而不是JoinTable. 让我们看看如何使用JoinColumn批注实现它。

@Entity@Table(name = "shipments")@Getter@Setterclass Shipment {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private int id;    private String cargoName;    @OneToMany(            fetch = FetchType.LAZY,            cascade = CascadeType.ALL,            mappedBy = "shipment")    private List<Item> childList;    public void addItem(Item item) {        if (childList == null) {            childList = new ArrayList();        }        childList.add(item);        item.setShipment(this);    }}@Entity@Table(name = "items")@Getter@Setterclass Item {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private int id;    private String itemName;    @ManyToOne    @JoinColumn(name = "shipment_id")    private Shipment shipment;}

Note: there are two things to consider in the new relationship model above. We add the ManyToOne and JoinColumn annotations to Item. Also we add the mappedBy parameter to Shipment.

注意:在上面的新关系模型中,有两点要考虑。 我们向项目添加ManyToOne和JoinColumn批注。 同样,我们将mappedBy参数添加到“货件”。

Now let’s delete all the previous tables for clarity and a fresh start. And when we run the code for the first time it creates tables and relationships as well (I changed the database from mssql to mysql because I wrote this post over a few separate days. This is why you see InnoDB in the generated queries below):

现在,为了清楚起见和重新开始,我们删除所有先前的表。 而且,当我们第一次运行代码时,它也会创建表和关系(我将数据库从mssql更改为mysql,因为我是在几天后才写这篇文章的。这就是为什么您在下面的生成的查询中看到InnoDB的原因):

Hibernate:create table items (       id integer not null auto_increment,        itemName varchar(255),        shipment_id integer,        primary key (id)    ) engine=InnoDB
Hibernate:         create table shipments (       id integer not null auto_increment,        cargoName varchar(255),        primary key (id)    ) engine=InnoDB
Hibernate:         alter table items        add constraint FKcpv8kcpjc081l551hre32f2rg        foreign key (shipment_id)        references shipments (id)

Now again, when we insert a shipment with an item:

现在,当我们插入带有物品的货件时:

Shipment shipment = new Shipment();shipment.setCargoName("test cargo");Item item = new Item();item.setItemName("test item");shipment.addItem(item);entitymanager.getTransaction().begin();entitymanager.persist(shipment);entitymanager.getTransaction().commit();

Hibernate generates only two sql queries as expected:

Hibernate仅生成两个SQL查询,如预期的那样:

Hibernate:     insert     into        shipments        (cargoName)     values        (?)
Hibernate:     insert     into        items        (itemName, shipment_id)     values        (?, ?)

And our database records look like below:

我们的数据库记录如下所示:

What about selecting a shipment from db? How will it change our queries?

从数据库中选择货件怎么办? 它将如何改变我们的查询?

Let’s run the below code and see our generated queries:

让我们运行以下代码,看看我们生成的查询:

Shipment shipment = entitymanager.find(Shipment.class, 1);System.out.println(shipment.getCargoName());System.out.println(shipment.getChildList().size());

Output:

输出:

Hibernate:     select        shipment0_.id as id1_1_0_,        shipment0_.cargoName as cargoNam2_1_0_     from        shipments shipment0_     where        shipment0_.id=?
test cargo
Hibernate:     select        childlist0_.shipment_id as shipment3_0_0_,        childlist0_.id as id1_0_0_,        childlist0_.id as id1_0_1_,        childlist0_.itemName as itemName2_0_1_,        childlist0_.shipment_id as shipment3_0_1_     from        items childlist0_     where        childlist0_.shipment_id=?

First hibernate selects a shipment from the db. And then because we call the getChildList().size() method, it lazy loads the child entities. Do you see that there are no Join queries when loading the child entities because there is no intermediate table like in the first example?

首先,Hibernate模式从数据库中选择一个货件。 然后,因为我们调用了getChildList()。size()方法,所以它延迟加载了子实体。 您是否看到加载子实体时没有Join查询,因为没有第一个示例中的中间表?

Now let’s add an item to the existing shipment and compare the generated queries with the first example. As you remember from the first example, when we try to add a new item to the existing shipment, it loads all child entities and deletes relationships from the intermediate table. Then it inserts the relationships again. It is a huge performance issue when you work with heavy data sets.

现在,我们将一个商品添加到现有货件中,并将生成的查询与第一个示例进行比较。 您从第一个示例中还记得,当我们尝试向现有货件中添加新项目时,它将加载所有子实体并从中间表中删除关系。 然后,它再次插入关系。 当您处理大量数据集时,这是一个巨大的性能问题。

But with our new approach, it is easy to solve this problem:

但是,使用我们的新方法,可以轻松解决此问题:

Shipment shipment = entitymanager.find(Shipment.class, 1);Item item = new Item();item.setItemName("item to existing shipment");shipment.addItem(item);entitymanager.getTransaction().begin();entitymanager.persist(shipment);entitymanager.getTransaction().commit();

It generates just two queries as it should, one select for the shipment and one insert for the child:

它只生成两个查询,一个查询货件,一个插入子查询:

Hibernate:     select        shipment0_.id as id1_1_0_,        shipment0_.cargoName as cargoNam2_1_0_     from        shipments shipment0_     where        shipment0_.id=?
Hibernate:     insert     into        items        (itemName, shipment_id)     values        (?, ?)

Now it is time to show you one common problem while working with ORM tools. Suppose we have 3 shipments and every shipment has 3 child items. What happens when we retrieve those shipments and try to iterate their children?

现在是时候向您展示使用ORM工具时的一个常见问题。 假设我们有3批货,每批有3个子项。 当我们取回这些货件并尝试对其子女进行迭代时,会发生什么?

We insert our initial data:

我们插入初始数据:

for (int i = 1; i <= 3; i++) {    Shipment shipment = new Shipment();    shipment.setCargoName("test shipment " + i);    for (int j = 1; j <= 3; j++) {        Item item = new Item();        item.setItemName("test item " + j);        shipment.addItem(item);    }    entitymanager.getTransaction().begin();    entitymanager.persist(shipment);    entitymanager.getTransaction().commit();}

Let’s list all shipment entities in our database and print their child item’s sizes:

让我们在数据库中列出所有货运实体,并打印其子项目的尺寸:

EntityManagerFactory emfactory = Persistence.createEntityManagerFactory("Hibernate_Jpa");EntityManager entitymanager = emfactory.createEntityManager();List<Shipment> shipments = entitymanager.createQuery("select s from Shipment s").getResultList();for (Shipment shipment : shipments) {    System.out.println(shipment.getChildList().size());}entitymanager.close();emfactory.close();

Here are our generated queries:

这是我们生成的查询:

Hibernate:     select        shipment0_.id as id1_1_,        shipment0_.cargoName as cargoNam2_1_     from        shipments shipment0_
Hibernate:     select        childlist0_.shipment_id as shipment3_0_0_,        childlist0_.id as id1_0_0_,        childlist0_.id as id1_0_1_,        childlist0_.itemName as itemName2_0_1_,        childlist0_.shipment_id as shipment3_0_1_     from        items childlist0_     where        childlist0_.shipment_id=?
Child item size for shipment id:1 is 3
Hibernate:     select        childlist0_.shipment_id as shipment3_0_0_,        childlist0_.id as id1_0_0_,        childlist0_.id as id1_0_1_,        childlist0_.itemName as itemName2_0_1_,        childlist0_.shipment_id as shipment3_0_1_     from        items childlist0_     where        childlist0_.shipment_id=?
Child item size for shipment id:2 is 3
Hibernate:     select        childlist0_.shipment_id as shipment3_0_0_,        childlist0_.id as id1_0_0_,        childlist0_.id as id1_0_1_,        childlist0_.itemName as itemName2_0_1_,        childlist0_.shipment_id as shipment3_0_1_     from        items childlist0_     where        childlist0_.shipment_id=?
Child item size for shipment id:3 is 3

What happened here is called the N + 1 problem. Because 1 query executed for selecting all shipments and N (where N is the size of shipments, which is 3 for our example) queries executed for selecting child entities.

这里发生的事情称为N + 1问题。 因为执行了1个查询以选择所有货件,并且执行了N个查询(其中N是货件的大小,在我们的示例中为3),所以执行了选择子实体的查询。

To solve this problem we have several options.

为了解决这个问题,我们有几种选择。

One of them is to use BatchSize annotation.

其中之一是使用BatchSize批注。

It is possible to tune the batch size for each query. Let’s put the annotation on our relationship and see the generated SQL.

可以调整每个查询的批处理大小。 让我们在关系上添加注释,然后查看生成SQL。

@OneToMany(        fetch = FetchType.LAZY,        cascade = CascadeType.ALL,        mappedBy = "shipment")@BatchSize(size = 10)private List<Item> childList;

I gave batch size 10 for testing purposes. And the below query was generated.

为了测试目的,我给了批量10。 并生成以下查询。

Hibernate:     select        shipment0_.id as id1_1_,        shipment0_.cargoName as cargoNam2_1_     from        shipments shipment0_Hibernate:     select        childlist0_.shipment_id as shipment3_0_1_,        childlist0_.id as id1_0_1_,        childlist0_.id as id1_0_0_,        childlist0_.itemName as itemName2_0_0_,        childlist0_.shipment_id as shipment3_0_0_     from        items childlist0_     where        childlist0_.shipment_id in (            ?, ?, ?        )
Child item size for shipment id:1 is 3Child item size for shipment id:2 is 3Child item size for shipment id:3 is 3

Because we have 3 shipments and the batch size is 10, it adds the 3 shipment ids in the where query. If batch size was set to 2, there would be 2 shipment ids in the where query.

因为我们有3个发货,并且批大小为10,所以它将在where查询中添加3个发货ID。 如果批次大小设置为2,则where查询中将有2个发货ID。

Another solution is using fetch join in the jpql.

另一种解决方案是在jpql中使用fetch join联接。

We change our query to:

我们将查询更改为:

List<Shipment> shipments = entitymanager.createQuery("select s from Shipment s join fetch s.childList").getResultList();

And we run the code again. It generates the following output:

然后,我们再次运行代码。 它生成以下输出:

Hibernate:     select        shipment0_.id as id1_1_0_,        childlist1_.id as id1_0_1_,        shipment0_.cargoName as cargoNam2_1_0_,        childlist1_.itemName as itemName2_0_1_,        childlist1_.shipment_id as shipment3_0_1_,        childlist1_.shipment_id as shipment3_0_0__,        childlist1_.id as id1_0_0__     from        shipments shipment0_     inner join        items childlist1_             on shipment0_.id=childlist1_.shipment_id
Child item size for shipment id:1 is 3Child item size for shipment id:1 is 3Child item size for shipment id:1 is 3Child item size for shipment id:2 is 3Child item size for shipment id:2 is 3Child item size for shipment id:2 is 3Child item size for shipment id:3 is 3Child item size for shipment id:3 is 3Child item size for shipment id:3 is 3

Wow, what just happened? Why are there 9 outputs for printing sizes? Because the jpql query generates an inner join SQL query, it causes duplicate records for every child. Hence, 3 shipments x 3 children = 9 duplicated outputs. To avoid this duplication, we need to add one more keyword to our jqpl which is the distinct keyword.

哇,刚才发生了什么? 为什么有9个输出用于打印尺寸? 因为jpql查询会生成一个内部联接SQL查询,所以它会为每个孩子产生重复的记录。 因此,有3批货物x 3个孩子= 9个重复的输出。 为了避免这种重复,我们需要一个更关键字添加到我们的jqpl这是distinct关键字。

List<Shipment> shipments = entitymanager.createQuery("select distinct s from Shipment s join fetch s.childList").getResultList();

Now the output had been generated just as expected:

现在已经按预期生成了输出:

Hibernate:     select        distinct shipment0_.id as id1_1_0_,        childlist1_.id as id1_0_1_,        shipment0_.cargoName as cargoNam2_1_0_,        childlist1_.itemName as itemName2_0_1_,        childlist1_.shipment_id as shipment3_0_1_,        childlist1_.shipment_id as shipment3_0_0__,        childlist1_.id as id1_0_0__     from        shipments shipment0_     inner join        items childlist1_             on shipment0_.id=childlist1_.shipment_id
Child item size for shipment id:1 is 3Child item size for shipment id:2 is 3Child item size for shipment id:3 is 3

And this way we solve the duplicate issues.

这样我们就解决了重复的问题。

I think that is enough for one post as it has become quite long already. Thank you all for reading so far. I hope it is now clear to you how Hibernate manages relationships, what queries are generated under the hood, and how to avoid some common mistakes.

我认为对于一个帖子来说已经足够了,因为它已经很长了。 谢谢大家到目前为止的阅读。 我希望您现在很清楚Hibernate如何管理关系,在幕后产生了哪些查询以及如何避免一些常见错误。

结论 (Conclusion)

  • Prefer lazy loading when you can use it

    可以使用时最好选择延迟加载
  • Use Bidirectional association with ManyToOne annotation on child entities when you need to (rather than OneToMany)

    需要时,将双向关联与子实体上的ManyToOne批注结合使用(而不是OneToMany)
  • Give the responsibility of relationships to the child side whenever possible

    尽可能将关系的责任交给孩子
  • To avoid the N + 1 problem, use BatchSize annotation or write jpql with fetch join

    为避免N + 1问题,请使用BatchSize批注或将jpql与fetch join存联接一起编写

  • Use JoinColumn instead of JoinTable on OneToMany relationships to avoid additional join queries

    在OneToMany关系上使用JoinColumn代替JoinTable以避免其他JoinTable查询

You can find the example code in my github repository: hibernate-examples

您可以在我的github存储库中找到示例代码: hibernate-examples

Resources

资源资源

HIBERNATE - Relational Persistence for Idiomatic JavaHibernate not only takes care of the mapping from Java classes to database tables (and from Java data types to SQL data…docs.jboss.orgWhat is the N+1 SELECT query issue?SELECT N+1 is generally stated as a problem in Object-Relational mapping (ORM) discussions, and I understand that it…stackoverflow.comA beginner's guide to JPA and Hibernate Cascade Types - Vlad Mihalcea(Last Updated On: April 25, 2018)Introduction JPA translates entity state transitions to database DML statements…vladmihalcea.com

HIBERNATE-惯用Java的关系持久性 Hibernate不仅负责从Java类到数据库表(以及从Java数据类型到SQL数据的映射…… docs.jboss.org 什么是N + 1 SELECT查询问题? SELECT N + 1通常在对象关系映射(ORM)讨论中被认为是一个问题,我理解它… stackoverflow.com JPA和Hibernate级联类型的初学者指南-Vlad Mihalcea (最新更新:2018年4月25日)JPA简介翻译实体状态转换为数据库DML语句… vladmihalcea.com

翻译自: https://www.freecodecamp.org/news/hibernate-deep-dive-relations-lazy-loading-n-1-problem-common-mistakes-aff1fa390446/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值