65.9K
CodeProject 正在变化。 阅读更多。
Home

关于Hibernate - One To Many的注意事项

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2017年12月25日

CPOL

4分钟阅读

viewsIcon

11333

downloadIcon

90

Hibernate一对多映射。

背景

这是一篇关于 Hibernate 单对多映射的笔记。在开始示例之前,我想先澄清几个 Hibernate 中常用的术语。

SessionFactory 与 Session

  • SessionFactory - Hibernate 中的 SessionFactory 是一个包含如何连接到数据库的所有配置信息的对象。
    • ASessionFactory 是不可变的;
    • SessionFactory 的行为由配置时提供的属性控制;
    • 创建 SessionFactory 是一个缓慢且开销很大的过程;
    • 一个应用程序通常只有一个 SessionFactory
    • SessionFactory 对象是线程安全的。
  • Session - Hibernate 中的 session 是一个与数据库通信的对象。
    • 每个 session 都有自己的数据库连接;
    • session 是由 SessionFactory 对象的 openSession() 方法创建的;
    • 创建 session 对象是一个轻量级的过程;
    • session 应在数据库操作完成后关闭,以释放数据库连接;
    • session 对象不是线程安全的。

瞬态 (Transient) vs. 持久化 (Persistent) vs. 游离 (Detached)

根据 Hibernate Session 文档,实体对象可以有三种状态。

  • 如果一个实体对象在 Hibernate session 外部创建,并且从未与 Hibernate session 关联,则它是瞬态的;
  • 如果一个实体对象已与 Hibernate session 关联,则它是持久化的;
    • 瞬态对象可以通过 session.save()session.persist() 方法添加到 session 中;
    • 如果实体对象是通过 Hibernate session 从数据库加载/获取的,则它是持久化状态。
  • 如果 session 被关闭,或者在该对象上调用了 Session.evict() 方法,则持久化对象会变成游离对象。

FlushMode

FlushMode 代表一种刷新策略。刷新过程通过检测状态变化并执行 SQL 语句来同步数据库状态与 session 状态。Hibernate 有四种刷新模式。

  • ALWAYS - Session 在每次查询之前都会被刷新;
  • AUTO - Session 有时会在查询执行前刷新,以确保查询永远不会返回过时的状态;
  • COMMIT - Session 在调用 Transaction.commit() 时被刷新;
  • MANUAL - Session 仅在应用程序显式调用 Session.flush() 时才被刷新。

示例

附带的是一个 Maven 项目,它对与单对多映射相关的实体执行一组单元测试 CRUD 操作。该示例使用 MySQL 数据库。如果您不熟悉 MySQL,可以参考 我之前的笔记。您可以使用以下 SQL 来创建测试中使用的表。

DROP DATABASE IF EXISTS experimentA;
CREATE DATABASE experimentA;
    
USE experimentA;
    
CREATE TABLE Parent (
  Id int(11) NOT NULL AUTO_INCREMENT,
  Name varchar(45) NOT NULL,
  PRIMARY KEY (Id)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    
    
CREATE TABLE Child (
  Id int(11) NOT NULL AUTO_INCREMENT,
  ParentID int(11) NOT NULL,
  Name varchar(45) NOT NULL,
  PRIMARY KEY (Id),
  FOREIGN KEY (ParentID) REFERENCES Parent (Id)
    
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    
CREATE TABLE GrandChild (
  Id int(11) NOT NULL AUTO_INCREMENT,
  ParentID int(11) NOT NULL,
  Name varchar(45) NOT NULL,
  PRIMARY KEY (Id),
  FOREIGN KEY (ParentID) REFERENCES Child (Id)
    
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

[Child] 表在 [Parent] 表上有一个外键引用,[GrandChild] 表在 [Child] 表上有一个外键引用。以下是对应的 Hibernate 实体类。

package com.song.example.hibernate.entities;
    
import java.util.HashSet;
import java.util.Set;
    
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.OneToMany;
import javax.persistence.Table;
    
@Entity
@Table(name = "Parent")
public class Parent {
    private Integer id;
    private String name;
    private Set<Child> children = new HashSet<Child>();
    
    public Parent() {}
    public Parent(String name) { this.name = name; }
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "Id")
    public Integer getId() { return this.id; }
    public void setId(Integer id) { this.id = id; }
    
    @Column(name = "Name", length = 100)
    public String getName() { return this.name; }
    public void setName(String name) { this.name = name; }
    
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "parent", cascade = CascadeType.ALL)
    public Set<Child> getChildren() { return this.children; }
    public void setChildren(Set<Child> children) { this.children = children; }
}
package com.song.example.hibernate.entities;
    
import java.util.HashSet;
import java.util.Set;
    
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.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
    
@Entity
@Table(name = "Child")
public class Child {
    private Integer id;
    private String name;
    private Parent parent;
    private Set<GrandChild> grandChildren = new HashSet<GrandChild>();
    
    public Child() {}
    public Child(String name, Parent parent) { 
        this.name = name; 
        this.parent = parent;
    }
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "Id")
    public Integer getId() { return this.id; }
    public void setId(Integer id) { this.id = id; }
    
    @Column(name = "Name", length = 100)
    public String getName() { return this.name; }
    public void setName(String name) { this.name = name; }
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ParentID", nullable = false)
    public Parent getParent() { return this.parent; }
    public void setParent(Parent parent) { this.parent = parent; }
    
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "parent", cascade = CascadeType.ALL)
    public Set<GrandChild> getChildren() { return this.grandChildren; }
    public void setChildren(Set<GrandChild> grandChildren) { 
        this.grandChildren = grandChildren;
    }
}
package com.song.example.hibernate.entities;
    
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.ManyToOne;
import javax.persistence.Table;
    
@Entity
@Table(name = "GrandChild")
public class GrandChild {
    private Integer id;
    private String name;
    private Child parent;
    
    public GrandChild() {}
    public GrandChild(String name, Child parent) {
        this.name = name;
        this.parent = parent;
    }
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "Id")
    public Integer getId() { return this.id; }
    public void setId(Integer id) { this.id = id; }
    
    @Column(name = "Name", length = 100)
    public String getName() { return this.name; }
    public void setName(String name) { this.name = name; }
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ParentID", nullable = false)
    public Child getParent() { return this.parent; }
    public void setParent(Child parent) { this.parent = parent; }

}

为了创建 Hibernate SessionFactory,将以下 hibernate.cfg.xml 文件添加到 resources 目录。

<hibernate-configuration>
    <session-factory>
        <property name="connection.driver_class">
            com.mysql.cj.jdbc.Driver
        </property>
        <property name="connection.url">
            jdbc:mysql:///experimentA
        </property>
        <property name="connection.username">root</property>
        <property name="connection.password">password</property>
    
        <property name="dialect">
            org.hibernate.dialect.MySQLDialect
        </property>
    
        <property name="cache.provider_class">
            org.hibernate.cache.NoCacheProvider
        </property>
        
        <property name="org.hibernate.flushMode">MANUAL</property>
    
        <property name="show_sql">false</property>
        <property name="format_sql">false</property>
    
        <mapping class="com.song.example.hibernate.entities.Parent" />
        <mapping class="com.song.example.hibernate.entities.Child" />
        <mapping class="com.song.example.hibernate.entities.GrandChild" />
    </session-factory>
</hibernate-configuration>

SessionFactoryInstance.java 用于创建一个单例 SessionFactory 实例。这可能不是创建 SessionFactory 的最佳方法,但对于小型单元测试项目来说,它可以节省我一些打字的时间。

package com.song.example.hibernate;
    
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
    
public final class SessionFactoryInstance {
    private SessionFactoryInstance(){}
    
    public final static SessionFactory Instance
        = new Configuration().configure().buildSessionFactory();
}

SessionFactoryPCG_Test.java 中用于执行 CRUD 操作的单元测试。

package com.song.example.hibernate;
    
import java.sql.Statement;
    
import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.testng.Assert;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
    
import com.song.example.hibernate.entities.Child;
import com.song.example.hibernate.entities.GrandChild;
import com.song.example.hibernate.entities.Parent;
    
public class PCG_Test {
    private final SessionFactory sessionFactory
        = SessionFactoryInstance.Instance;
    
    private Parent parent = null;
    private Child child = null;
    private GrandChild grandChild = null;
    
    @BeforeTest
    public void Init() {
        Assert.assertNotNull(sessionFactory);
        
        try (Session session = sessionFactory.openSession()) {
            FlushMode flushmode = session.getHibernateFlushMode();
            Assert.assertEquals(flushmode, FlushMode.MANUAL);
            
            session.doWork(connection -> {
    
                try (Statement statement = connection.createStatement()) {
                    statement.execute("SET FOREIGN_KEY_CHECKS = 0");
                    statement.execute("TRUNCATE TABLE GrandChild;");
                    statement.execute("TRUNCATE TABLE Child;");
                    statement.execute("TRUNCATE TABLE Parent;");
                    statement.execute("SET FOREIGN_KEY_CHECKS = 1");
                }
            });    
        }
        
        // Initiate test data
        this.parent = new Parent("Parent");
        this.child = new Child("Child", this.parent);
        this.grandChild = new GrandChild("Grand child", this.child);
        
        this.parent.getChildren().add(this.child);
        this.child.getChildren().add(this.grandChild);
    }
    
    @Test
    public void Insert_Test() {        
        try (Session session = sessionFactory.openSession()) {
            Transaction tx = session.getTransaction();
            
            try {
                tx.begin();
                session.save(parent);
                session.flush();
                tx.commit();
            }
            catch(Exception e) {
                tx.rollback();
                Assert.fail("Failed - Insert_Test");
            }
        }
        
        // Validate data is inserted
        try (Session session = sessionFactory.openSession()) {
            Parent parent = session.get(Parent.class, this.parent.getId());
            Child child = parent.getChildren().iterator().next();
            GrandChild grandChild = child.getChildren().iterator().next();
            
            Assert.assertEquals(parent.getName(), this.parent.getName());
            Assert.assertEquals(child.getName(), this.child.getName());
            Assert.assertEquals(grandChild.getName(), this.grandChild.getName());
        }
    }
    
    @Test(dependsOnMethods = {"Insert_Test"})
    public void Update_Test() {
        final String newParentName = "New Parent Name";
        final String newChildName = "New Child Name";
        final String newGrandChildName = "New Grand Child Name";
        
        try (Session session = sessionFactory.openSession()) {
            Transaction tx = session.getTransaction();
            
            Integer id = this.parent.getId();
            try {
                tx.begin();
                Parent parent = session.get(Parent.class, id);
                Child child = parent.getChildren().iterator().next();
                GrandChild grandChild = child.getChildren().iterator().next();
                
                parent.setName(newParentName);
                child.setName(newChildName);
                grandChild.setName(newGrandChildName);
                
                session.flush();
                tx.commit();
            }
            catch(Exception e) {
                tx.rollback();
                Assert.fail("Failed - Update_Test");
            }
        }
        
        // Validate data is updated
        try (Session session = sessionFactory.openSession()) {
            Parent parent = session.get(Parent.class, this.parent.getId());
            Child child = parent.getChildren().iterator().next();
            GrandChild grandChild = child.getChildren().iterator().next();
            
            Assert.assertEquals(parent.getName(), newParentName);
            Assert.assertEquals(child.getName(), newChildName);
            Assert.assertEquals(grandChild.getName(), newGrandChildName);
        }
    }
    
    @Test(dependsOnMethods = {"Update_Test"})
    public void Delete_Child_Test() {
        try (Session session = sessionFactory.openSession()) {
            Transaction tx = session.getTransaction();
            
            Integer id = this.parent.getId();
            try {
                tx.begin();
                Parent parent = session.get(Parent.class, id);
                Child child = parent.getChildren().iterator().next();
                parent.getChildren().remove(child);
                
                session.delete(child);
                session.flush();
                tx.commit();
            }
            catch(Exception e) {
                tx.rollback();
                Assert.fail("Failed - Delete_Child_Test");
            }
        }
        
        // Validate child is deleted
        try (Session session = sessionFactory.openSession()) {
            Parent parent = session.get(Parent.class, this.parent.getId());
            Assert.assertEquals(parent.getChildren().size(), 0);
        }
    }
    
    @Test(dependsOnMethods = {"Delete_Child_Test"})
    public void Delete_ALL_Test() {
        try (Session session = sessionFactory.openSession()) {
            Transaction tx = session.getTransaction();
            
            Integer id = this.parent.getId();
            try {
                tx.begin();
                Parent parent = session.get(Parent.class, id);
                session.delete(parent);
                session.flush();
                tx.commit();
            }
            catch(Exception e) {
                tx.rollback();
                Assert.fail("Failed - Delete_ALL_Test");
            }
        }
        
        // Validate all data is deleted
        try (Session session = sessionFactory.openSession()) {
            Parent parent = session.get(Parent.class, this.parent.getId());
            Assert.assertNull(parent);
        }
    }
}
  • Init() 方法清除数据库中的所有数据,为测试提供一个全新的开始;
  • Insert_Test() 方法将所有数据插入数据库。session.save() 只应用于父对象,但所有子对象和孙对象都会级联保存;
  • Update_Test() 方法更新已保存在数据库中的数据;
  • Delete_Child_Test() 方法从父对象中删除一个子对象;
  • Delete_ALL_Test() 方法删除数据库中的所有对象。session.delete() 只应用于父对象,但所有子对象和孙对象都会级联删除。

运行单元测试

要运行单元测试,您需要运行 MySQL 服务器。您可以通过以下命令在 Linux 系统中启动 MySQL 服务器。

sudo service mysql start

然后您就可以通过 Maven 命令运行单元测试了。

mvn test

如果您将项目加载到 Eclipse 中,也可以通过 TestNG 插件运行单元测试。

关注点

  • 这是一篇关于 Hibernate 单对多映射的笔记。
  • 希望您喜欢我的帖子,也希望这篇笔记能以某种方式帮助到您。

历史

  • 2017年12月24日:首次修订
© . All rights reserved.