1. 概述
在数据库中,有些条目具有自然标识符,比如书籍的ISBN或个人的社会保障号(SSN)。除了传统的数据库ID,Hibernate允许我们声明某些字段为自然ID,并基于这些属性轻松查询。
本教程将讨论@NaturalId
注解,并在Spring Boot项目中学习如何使用和实现它。
2. 简单的自然ID
只需通过@NaturalId
注解标记一个字段,即可将其指定为自然标识符。这样,我们可以利用Hibernate的API无缝查询关联的列。
本文示例中,我们将使用HotelRoom
和ConferenceRoom
数据模型。首先,我们将实现ConferenceRoom
实体,其独特的name
属性用于区分:
@Entity
public class ConferenceRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NaturalId
private String name;
private int capacity;
public ConferenceRoom(String name, int capacity) {
this.name = name;
this.capacity = capacity;
}
protected ConferenceRoom() {
}
// getters
}
首先,我们需要在name
字段上添加@NaturalId
注解。请注意,该字段是不可变的:它在构造函数中声明,不提供setter方法。此外,Hibernate需要一个无参数构造函数,但我们可以将其设置为protected
,避免直接使用。
现在,我们可以使用bySimpleNaturalId
方法,通过名称(自然ID)轻松在数据库中搜索会议室:
@Service
public class HotelRoomsService {
private final EntityManager entityManager;
// constructor
public Optional<ConferenceRoom> conferenceRoom(String name) {
Session session = entityManager.unwrap(Session.class);
return session.bySimpleNaturalId(ConferenceRoom.class)
.loadOptional(name);
}
}
运行测试并检查生成的SQL以确认预期行为。为了查看Hibernate/JPA SQL日志,我们需要添加适当的日志配置:
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
现在,调用conferenceRoom
方法,查找自然ID为"Colorado"的会议室:
@Test
void whenWeFindBySimpleNaturalKey_thenEntityIsReturnedCorrectly() {
conferenceRoomRepository.save(new ConferenceRoom("Colorado", 100));
Optional<ConferenceRoom> result = service.conferenceRoom("Colorado");
assertThat(result).isPresent()
.hasValueSatisfying(room -> "Colorado".equals(room.getName()));
}
我们可以检查生成的SQL,期望它会根据name
列查询conference_room
表:
select c1_0.id,c1_0.capacity,c1_0.name
from conference_room c1_0
where c1_0.name=?
3. 复合自然ID
自然标识符也可以由多个字段组成。在这种情况下,可以为所有相关字段都添加@NaturalId
注解。
例如,考虑GuestRoom
实体,其复合键由roomNumber
和floor
字段组成:
@Entity
public class GuestRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NaturalId
private Integer roomNumber;
@NaturalId
private Integer floor;
private String name;
private int capacity;
public GuestRoom(int roomNumber, int floor, String name, int capacity) {
this.roomNumber = roomNumber;
this.floor = floor;
this.name = name;
this.capacity = capacity;
}
protected GuestRoom() {
}
// getters
}
与第一个例子类似,我们将使用Hibernate的Session
的byNaturalId
方法。然后,我们将使用流式API指定构成复合键的字段值:
public Optional<GuestRoom> guestRoom(int roomNumber, int floor) {
Session session = entityManager.unwrap(Session.class);
return session.byNaturalId(GuestRoom.class)
.using("roomNumber", roomNumber)
.using("floor", floor)
.loadOptional();
}
现在,尝试通过测试方法查询第三层的23号客房:
@Test
void whenWeFindByNaturalKey_thenEntityIsReturnedCorrectly() {
guestRoomJpaRepository.save(new GuestRoom(23, 3, "B-423", 4));
Optional<GuestRoom> result = service.guestRoom(23, 3);
assertThat(result).isPresent()
.hasValueSatisfying(room -> "B-423".equals(room.getName()));
}
检查SQL时,我们应该看到一个直接使用复合键的简单查询:
select g1_0.id,g1_0.capacity,g1_0.floor,g1_0.name,g1_0.room_number
from guest_room g1_0
where g1_0.floor=?
and g1_0.room_number=?
4. 与Spring Data集成
Spring Data(/the-persistence-layer-with-spring-data-jpa)的JpaRepository
接口默认不支持通过自然标识符查询。然而,我们可以扩展这些接口来添加额外的方法以实现此功能。首先,我们需要声明扩展后的接口:
@NoRepositoryBean
public interface NaturalIdRepository<T, ID> extends JpaRepository<T, ID> {
Optional<T> naturalId(ID naturalId);
}
接下来,我们将创建这个接口的泛型实现,并需要将泛型类型转换为领域实体。为此,我们可以扩展JPA的SimpleJpaRepository
,利用其getDomainClass
方法:
public class NaturalIdRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements NaturalIdRepository<T, ID> {
private final EntityManager entityManager;
public NaturalIdRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
}
@Override
public Optional<T> naturalId(ID naturalId) {
return entityManager.unwrap(Session.class)
.bySimpleNaturalId(this.getDomainClass())
.loadOptional(naturalId);
}
}
此外,我们需要添加@EnableJpaRepositories
注解,让Spring扫描整个包并注册我们的自定义仓库:
@Configuration
@EnableJpaRepositories(repositoryBaseClass = NaturalIdRepositoryImpl.class)
public class NaturalIdRepositoryConfig {
}
这将使我们能够扩展NaturalIdRepository
接口,为具有自然ID的实体创建仓库:
@Repository
public interface ConferenceRoomRepository extends NaturalIdRepository<ConferenceRoom, String> {
}
因此,我们将能够使用增强的仓库API,利用naturalId
方法进行简单查询:
@Test
void givenNaturalIdRepository_whenWeFindBySimpleNaturalKey_thenEntityIsReturnedCorrectly() {
conferenceRoomJpaRepository.save(new ConferenceRoom("Nevada", 200));
Optional result = conferenceRoomRepository.naturalId("Nevada");
assertThat(result).isPresent()
.hasValueSatisfying(room -> "Nevada".equals(room.getName()));
}
最后,检查生成的SQL语句:
select c1_0.id,c1_0.capacity,c1_0.name
from conference_room c1_0
where c1_0.name=?
5. 总结
在这篇文章中,我们了解了拥有自然标识符的实体,并发现Hibernate的API允许我们轻松根据这些特殊标识符进行查询。随后,我们在Spring Data JPA中创建了一个通用仓库,并扩展了它,以利用Hibernate的这一特性。
如往常一样,本文的代码示例可在GitHub找到。