Hibernate 中主键映射的助攻

1. 前言

本节课和大家一起聊聊 Hibernate 中的主键策略。通过本节课程,你将了解到:

  • 什么是主键策略及主键生成器的种类;
  • 如何映射复合主键。

2. 主键策略

Hibernate 进行数据库操作时,可依靠主键生成器组件更快速、准确地进行一系列操作。这便是主键策略

2.1 主键生成器

主键是关系数据库中的概念,目的是唯一标识表中记录,保证实体数据的完整性。

  • 关系数据库中表与表中数据的关系描述需依赖主键实现 ;
  • 另有外键概念,所谓外键是在另一张表中对引用表的主键值的引用称呼。

主外键关系指在不同的表中通过共同的字段信息建立起表中数据依赖(引用)关系。

回到 Hibernate 的世界!先展示一段代码:

Student student = new Student(2, "Configuration老二", "男");
session.save(student);

上面的代码功能:把应用程序中的数据写入到数据库中,没毛病呀!

来!没毛病找点毛病出来:

实际操作时,要求 Hibernate 把程序中 stuId 属性的值插入到表中同名的 stuId 主键字段中。

主键有什么特点?

唯一性!回答得对。

请问在应用程序中构建数据时,如何确保赋值给 stuId 的值在表中不存在!这就是问题所在。

如何解决?

使用 Hibernate 主键生成器。

所谓主键生成器其作用就是在 Hibernate 向表中插入数据时,负责生成表中数据记录的主键。

Hibernate 主键生成器 API 介绍:

  • Hibernate 的主键生成器(generator)都实现了 org.hibernate.id.IdentityGenerator 接口;
 public class IdentityGenerator extends AbstractPostInsertGenerator { …… }
  • 开发者可以遵循这个接口规范提供自己的主键生成方案;
  • Hibernate 内置有较多主键生成器主键生成器都有自己的实现类,并提供有快捷名称方便在注解或 XML 中引用。

常用主键生成器一览:

  • org.hibernate.id.IncrementGenerator(increment):对 longshortint 的数据列生成自动增长主键;

  • org.hibernate.id.IdentityGeneratoridentity): 适用于 SQL server,MySql 等支持自动增长列的数据库,适合 longshortint 数据列类型;

  • org.hibernate.id.SequenceGeneratorsequecne):适用 oracle,DB2 等支持 Sequence 的数据库,适合 long、shortint 数据列类型;

  • org.hibernate.id.UUIDGeneratoruuid):对字符串列的数据采用 128 - 位 uuid 算法生成唯一的字符串主键;

  • org.hibernate.id.Assigned(assigned):由应用程序指定,也是默认生成策略。

默认使用 assigned 生成器。这种方案要求开发者在应用程序中提供自己的主键生成算法:

  • 调用保存方法之前,先带着指定的值往数据库中跑一趟,检索是否存在重复,如果有,再试其它值;
  • 调用保存方法之前,先检索到表中 stuId 字段值的最大值,返回应用程序后递增 1,用于 stuId 新值。如果多个用户同时向数据中插入数据,这种方案会出问题,不适合并发操作环境。

使用 assigned 生成器除非有一个很完美的解决方案,否则建议只用于学习或测试环境。

本课程使用的是 Mysql 数据库,最佳选择 identity 生成器,主键值交给数据库的自动增长列自动生成。

2.2 使用主键生成器重构代码

  1. 在 Student 类的标识属性(stuId)上标注如下注解;
 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 public Integer getStuId() {
     return stuId;
 }

简单得难以置信!空灵而干净!!

使用 @GeneratedValue 注解确定主键生成器类型。GenerationType 是一个枚举类型,有如下几个选择:

  • AUTOHibernate 区分数据库系统,自动选择最佳策略;
  • IDENTITY: 适合具有自动增长类型的数据库,如 MySql……
  • SEQUENCE: 适合如 Oracle 类型数据库;
  • TABLE: 使用 Hibernate 提供的 TableGenerator 生成器,不常用。
  1. 为了更好观察生成的新数据,重建数据库中的表。主配置文件中修改或添加如下配置信息;
 <property name="hbm2ddl.auto">create</property>
  1. 执行插入数据实例;
  // 打开事务
  try{
      transaction = session.beginTransaction();
      // 添加一条学生信息,此处没有指定学生编号
      Student student = new Student("Hibernate 01", "男");
      session.save(student);
      transaction.commit();  
  } catch(Exception e) {
      transaction.rollback(); 
  } finally {
      session.close();
  }
  1. 进入 Mysql 系统查看,表结构中 stuId 自动设为主键,且为自动递增;
    图片描述

  2. 查看表中数据,主键值自动生成;

  3. 试着多加几条数据,别忘记修改如下配置信息。

 <property name="hbm2ddl.auto">update</property>

图片描述

大功告成!!

2.3 主键生成器

使用注解 @GeneratedValue 指定生成器类型后,Hibernate 一般情况下会自动创建对应的生成器对象,如前面指定类型为 IDENTITY,则创建生成 org.hibernate.id.IdentityGenerator 对象。

如果需要个性化定制生成器对象,则需要显示指定生成器对象,如为 Oracle 数据库指定主键生成器时,则配置可如下:

XML 映射方式:

<id name="stuId" type="Integer" column="stuId">
    <generator class="sequence">
        <param name="sequence">mySeq</param>
    </generator>
</id>

注解映射方式:

@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE,generator="mySeqIdGen")
@SequenceGenerator(name="mySeqIdGen",sequenceName="mySeq")
public Integer getStuId() {
    return stuId;
}

@SequenceGenerator 注解显示指明使用 org.hibernate.id.SequenceGenerator 生成器对象,并指定使用数据库中的命名为 mySeq 的序列化器。

其它主键生成器的使用本文不再复述,抛砖引玉,学习者可自行深入!

3. 复合主键

3.1 什么是复合主键

关系数据库中,主键可指定一个字段实现,也可指定多个字段实现,这样的主键叫复合主键

从数据库表设计原则分析,尽可能少用复合主键,但并不排除需要使用的场景。

对使用 Hibernate 的开发者而言,将面对一个新问题:在应用程序中,如何映射表中的复合主键?

应用程序中,复合主键映射方案有三:

  • 嵌入类注解为 @Embeddable,并将实体类的属性注解为 @Id;
  • 实体类的属性注解为 @EmbeddedId;
  • 实体类注解为 @IdClass,并将该实体类所有属于主键的属性都注解为 @Id

先分清楚两个概念:

  • 实体类:使用 @entity 注解的类;
  • 嵌入类:使用 @Embeddable 注解的类;

3.2 复合主键映射方案一

实施流程

  1. 假设学生表中使用了 stuId,stuName 两字段构成复合主键;

  2. 应用程序中构建两个类;

    嵌入类

 @Embeddable  
 public class StudentId {
     private Integer stuId;
     private String stuName;
     public StudentId()  {
         super();
     }      
     public  StudentId(Integer stuId, String stuName) {
         super();
         this.stuId = stuId;  
         this.stuName = stuName;
     }      
    //……省略get、set方法

嵌入类说明:

  • 标注有 @Embeddable;

  • 类中包括 stuId、stuName 两个属性与表中的复合字段相呼应;

  • 必须实现 Serializable!!!后续章节会聊到为什么。

实体类:

  @Entity  
  public class Student_ {
      private StudentId studentId;
      private String stuSex;
      public Student_() {
          super();
      }      
      public  Student_(StudentId studentId, String stuSex) {
          super();
          this.studentId = studentId;
          this.stuSex = stuSex;
      }      
      @Id      
      public StudentId  getStudentId() {
          return studentId;
      }      
      //……省略其它set、get方法

实体类说明:

实体类使用 @Entity 注解;
关键代码分析:

关键点一: 内部添加引用嵌入类属性。

     private StudentId studentId;

关键点二: studentId 属性上需要添加 @Id 注解。

    @Id
    public StudentId getStudentId() {
        return studentId;
    }
  1. 重新创建数据库中的学生表:
   <property name="hbm2ddl.auto">create</property>
  1. 运行测试实例。

    Tips: 对于复合主键,需要在代码级别指定值。

   // 打开事务 
   transaction = session.beginTransaction();
   // 添加一条学生信息 
   Student_ student = new Student_();
   // 复合主键信息
   StudentId studentId=new StudentId(1, "Hibernate是老大");
   student.setStudentId(studentId);
   student.setStuSex("男");
   session.save(student);
   transaction.commit();
  1. 查看 MySql,会发现新表 student_ 中指定复合主键,且数据添加成功。

图片描述

如上所述,嵌入类就是复合主键映射类!

3.3 复合主键映射方案二

与第一方案相比,保留 @Entity 注解的实体类。

第一方案中的嵌入类上不再使用 @Embedded 注解,嵌入类降维成普通类。

实体类中不再使用 @Id 注解,而是使用 @EmbeddedId,此注解语义明确:一注解承担两注解任务。

可理解 @EmbeddedId 注解是 @Embedded@Id 两个注解的综合体。

@EmbeddedId
public StudentId getStudentId() {
    return studentId;
}

和第一方案一样进行代码测试,结果没什么不一样。

3.4 复合主键映射方案三

方案三与前两个方案区别:

  1. 没有嵌入类概念,前面的嵌入类降维成一个普通类,不加任何注解描述;

此类的作用仅仅在逻辑上把两个标识属性归为一组!

 public  class StudentId implements Serializable{
     private Integer stuId;    
     private String stuName;
     public StudentId() {
         super();
     }    
     public StudentId(Integer stuId, String stuName) {
         super();
         this.stuId = stuId;
         this.stuName = stuName; 
     }    
    //……省略set、get方法
  1. 实体类中使用 @IdClass 指定内部有标识属性的类,另在实体类中也重复出现标识属性且上面使用 @Id 注解。
@Entity
//指明实体类中标注有 @Id 的属性为同一类型
@IdClass(StudentId.class)
public class Student_ {
    private Integer stuId;
    private String stuName;
    private String stuSex;
    public Student_() {
        super();
    }
    public Student_(Integer stuId, String stuName, String stuSex) {
        super();
        this.stuId = stuId;
        this.stuName = stuName;
        this.stuSex = stuSex;
    }
    @Id
    public Integer getStuId() {
        return stuId;
    }
    @Id
    public String getStuName() {
        return stuName;
    }
    //……省略set、get方法
}

测试代码,结果和前面 2 个方案一样。

3.5 方案比较

通过代码的编写过程,3 种方案优劣比较明显:

  • 第一种方案和第二种方案本质上没有太多区别,只是一个使用 @Id@Embeddable 两个注解;一个是使用 @EmbeddedId 注解行使两个注解的功能;
  • 显然,第二种方案稍优于第一方案,至少可少使用一个注解;
  • 第三种方案代码有重复之处,与 OOP 中的重用原则相违背,请慎用。

4. 小结

本节课,聊到了主键生成器,通过主键生成器这个助攻手,能有效地保持主键的唯一性,从而保证数据的完整性。

另聊了复合主键,复合主键映射备选方案虽多,但你可只记你心中最钟情的那个。