Spring Boot 中的 Hibernate 自然 ID
一、概述
一些數據庫條目擁有一個自然標識符,例如一本書的 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
}
首先,我們需要用@NaturalId 註釋name
字段@NaturalId.
讓我們注意到該字段是不可變的:它是在構造函數中聲明的,並且不公開 setter。此外,Hibernate 需要一個無參數的構造函數,但我們可以protected
它並避免使用它。
我們現在可以使用bySimpleNaturalId
方法使用名稱作為自然標識符輕鬆地在數據庫中搜索會議室:
@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 並期望它使用其自然 ID( name
列)查詢conference_room
表:
select c1_0.id,c1_0.capacity,c1_0.name
from conference_room c1_0
where c1_0.name=?
3.複合自然身份證
自然標識符也可以由多個字段組成。在這種情況下,我們可以使用@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 會話中的byNaturalId
方法Session.
之後,我們將使用流暢的 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 的GuestRoom
來測試該方法:
@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 的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 上找到本文的代碼示例。