图源:
在表结构设计中,我们用主键来确保一条数据在表中的唯一性。类似的,可以用唯一索引确保某列数据都是唯一的。如果需要限制多个列的唯一性,还可以使用联合唯一索引。
本文将探讨如何在 JPA (Hibernate)中使用主键和唯一索引。
主键
通过,我们已经很熟悉怎么在实体中设置一个自增主键了,这也是表结构设计中最常见的方式。但严格来说,对于关系型数据库这并不是必须的。实际上这是日常开发应用中的一种“改良”,使用一个唯一的自增索引可以降低表结构设计的复杂度,并且在利用 Id 查找和定位数据上有一定帮助。
当然,我们可以在必要的时候不使用自增主键,转而使用更传统的主键或联合主键。
我们先看不使用自增主键的单一主键要怎么设置:
name = "USER_STUDENT")
(public class Student {
Exclude
. min = 10, max = 10)
(
private String number;
min = 5, max = 50)
( private String name;
AccessLevel.NONE)
( private LocalDate birthDay;
AccessLevel.NONE)
( AccessLevel.NONE)
( Exclude
.
private Integer age;
EnumType.ORDINAL)
( private Gender gender;
// ...
}
注意,这里不再是使用自增的id
作为主键,而是使用了number
(学号)作为主键,具体的学号生成规则我们可以自行定义,只要确保其唯一性即可。
示例中使用 Hibernate Validation 注解(如
@Size
)来约束实体类的属性格式(比如字符串长度),这样做的好处是同时可以影响到 DDL。这一点在中有过讨论。
Hibernate 自动生成的 DDL:
CREATE TABLE `user_student` (
`number` varchar(10) COLLATE utf8mb4_general_ci NOT NULL,
`birth_day` date NOT NULL,
`gender` tinyint NOT NULL,
`name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
PRIMARY KEY (`number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
此时不会生成
xxx_seq
,这是很自然的事情。
测试用例比较简单,这里不再赘述,感兴趣的可以查看。
联合主键
对于某些“关系表”,会需要使用多个字段作为主键,即联合主键。
下面看一个实际示例,这里有两个实体,分别表示学生和班级:
// ...
name = "USER_STUDENT")
(public class Student {
Exclude
. min = 10, max = 10)
(
private String number;
// ...
}
// ...
name = "user_class")
(public class ClassRoom {
strategy = GenerationType.AUTO)
( private Long id;
/**
* 班级名称,如一年级二班
*/
min = 5, max = 25)
( unique = true)
( private String name;
}
Student
实体的主键是number
,ClassRoom
的主键是id
。
假设我们需要添加了一个实体表示班级和学生的关系,并在其中保存某些附加在这个关系上的信息(比如座位号):
name = "user_student_class")
(public class StudentClass {
/**
* 班级 id
*/
private Long classRoomId;
/**
* 学号
*/
private String studentNumber;
/**
* 座位号
*/
private int seatNumber;
}
显然,这个实体类要使用classRoomId
和studentNumber
两个字段作为联合主键。
首先,需要为联合主键创建一个单独的类并包含相应的字段:
public class StudentClassId implements Serializable {
private Long classRoomId;
private String studentNumber;
}
作为联合主键的类必须满足以下要求:
-
是可序列化的(
Serializable
) -
包含一个默认构造器(无参构造器)
-
实现了
equals
和hashCode
方法
在StudentClass
实体中使用这个类:
// ...
StudentClassId.class)
(public class StudentClass {
private Long classRoomId;
private String studentNumber;
// ...
}
需要使用@IdClass
注解指定可以作为联合主键的类定义,以及对实体类中作为联合主键的属性使用@Id
注解。
要注意的是,并没有任何手段映射实体类StudentClass
中作为联合主键的字段和StudentClassId
类中的相应字段,Hibernate 仅仅使用属性名称和类型进行映射(匹配),所以这里必须保证对应的属性名称和类型完全一致。
可以利用联合主键构成的关系表进行联表查询,比如:
"SpringJavaAutowiredFieldsWarningInspection")
(
public class ClassRoomService {
private StudentClassRepository studentClassRepository;
private StudentRepository studentRepository;
private ClassRoomRepository classRoomRepository;
/**
* 获取指定班级的所有学生
*
* @param classId 班级id
* @return
*/
public List<Student> findStudentsByClassId(long classId) {
var scList = studentClassRepository.findAll(Example.of(StudentClass.builder()
.classRoomId(classId)
.build()));
var studentNumbers = scList.stream().map(sc -> sc.getStudentNumber()).collect(Collectors.toSet());
return studentRepository.findAllById(studentNumbers);
}
/**
* 根据学号查询其所属班级
*
* @param studentNumber
* @return
*/
public ClassRoom findClassRoomByStudentNumber(String studentNumber) {
var sc = studentClassRepository.findOne(Example.of(StudentClass.builder()
.studentNumber(studentNumber)
.build())).get();
return classRoomRepository.findById(sc.getClassRoomId()).get();
}
}
@EmbeddedId
除了上述方式外,还有一种方式同样可以实现联合主键。
我们在中介绍过@Embeddable
和@Embedded
的作用,实际上可以用类似的方式实现联合主键。
下面用一个示例说明,在这个示例中将创建一个教师(Teacher)实体,并由教师-班级实体(TeacherClass)表示教师和班级的关联关系(多对多)。
先定义一个教师实体:
name = "user_teacher_class")
(public class TeacherClass {
private TeacherClassId teacherClassId;
/**
* 是否为班主任
*/
private Boolean classLeader = false;
}
定义用于教师-班级实体的联合主键类:
@Getter
@Embeddable
@EqualsAndHashCode
@NoArgsConstructor
@Builder
@AllArgsConstructor
public class TeacherClassId implements Serializable {
private long teacherId;
private long classRoomId;
}
和之前的要求类似,这里同样要求这个类型是可序列化、拥有默认构造器以及实现equals
和hashCode
方法的。
定义教师-班级实体:
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user_teacher_class")
public class TeacherClass {
@EmbeddedId
private TeacherClassId teacherClassId;
/**
* 是否为班主任
*/
@NotNull
private Boolean classLeader = false;
}
这里使用@EmbeddedId
以“嵌入”的方式添加了一个联合主键属性,这个属性实际上会映射到 DDL 中的相应字段:
CREATE TABLE `user_teacher_class` (
`class_room_id` bigint NOT NULL,
`teacher_id` bigint NOT NULL,
`class_leader` bit(1) NOT NULL,
PRIMARY KEY (`class_room_id`,`teacher_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
相对来说,个人认为这种实现方式更简洁一些。
唯一索引
如果要确保某一列数据是唯一的,除了将其设置为主键外,还可以使用唯一索引。
在单个字段上添加唯一索引很简单:
// ...
@Entity
@Table(name = "user_teacher")
public class Teacher {
// ...
@Column(unique = true)
private String name;
}
在这个示例中,通过使用@Column(unique = true)
可以确保user_teacher
表的name
字段是唯一的。
Hibernate 自动生成的 DDL 证明了这一点:
CREATE TABLE `user_teacher` (
`id` bigint NOT NULL,
`name` varchar(25) COLLATE utf8mb4_general_ci NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_de5s198srx0o9jrgfxmsd271e` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
联合唯一索引
如果要对一张表中的多个列的进行限制,让其组合后具备唯一性,就需要使用联合唯一索引。
这种情况并不常见,一般在关系表使用自增主键的情况下使用,以确保“真实”主键的唯一性。
下面看一个示例:
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user_club")
public class Club {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotBlank
@NotNull
@Length(max = 50)
private String name;
}
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "user_student_club")
public class StudentClub {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String studentNumber;
private Long clubId;
}
这个示例中Club
表示学生社团实体,StudentClub
表示学生社团和学生的对应关系。
值得注意的是,在关系实体中并没有使用联合主键,依然使用单一的自增主键。这样做有一个问题,我们难以确保user_student_club
表在执行完数据库插入后其字段studentNumber
和clubId
具备唯一的对应关系。因为这两个字段实际上起着“逻辑主键”的作用,它们联合起来确定了唯一的“学生-社团”对应关系,我们必须要确保其在表数据中是唯一的。
可以使用联合唯一索引来解决这个问题:
// ...
@Table(name = "user_student_club", uniqueConstraints = {
@UniqueConstraint(name = "student_club_idx", columnNames = {"studentNumber", "clubId"})
})
public class StudentClub {
// ...
}
这里用@UniqueConstraint
定义了一个“唯一约束”,并包含在@Table
的uniqueConstraints
属性中。
换言之,可以通过uniqueConstraints
属性添加多个“唯一约束”。
这反映在 Hibernate 生成的 DDL 中就是联合唯一索引:
CREATE TABLE `user_student_club` (
`id` bigint NOT NULL,
`club_id` bigint DEFAULT NULL,
`student_number` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `student_club_idx` (`student_number`,`club_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
创建唯一索引除了可以约束唯一性外,额外的好处是作为索引,可以优化查询速度。
最后可以通过测试用例证明唯一索引生效:
@Test
void testAddStudentClub(@Autowired List<Student> students,
@Autowired List<Club> clubs,
@Autowired Student student,
@Autowired Club club) {
students.forEach(s -> {
clubs.forEach(c -> {
studentClubRepository.save(StudentClub.builder()
.clubId(c.getId())
.studentNumber(s.getNumber())
.build());
});
});
//单独插入一条重复数据,这里应当报错
Assertions.assertThrows(DataIntegrityViolationException.class, () -> {
studentClubRepository.save(StudentClub.builder()
.studentNumber(student.getNumber())
.clubId(club.getId())
.build());
});
}
The End,谢谢阅读。
可以从获取本文的完整示例代码。
参考资料
文章评论