Spring Boot中的Hibernate自然ID

2025/03/23

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
}

首先,我们需要用@NaturalId标注name字段。请注意,该字段是不可变的:它在构造函数中声明,并且不公开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. 复合自然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的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扫描整个包并注册我们的自定义Repository:

@Configuration
@EnableJpaRepositories(repositoryBaseClass = NaturalIdRepositoryImpl.class)
public class NaturalIdRepositoryConfig {
}

这将允许我们扩展NaturalIdRepository接口来为拥有自然ID的实体创建Repository:

@Repository
public interface ConferenceRoomRepository extends NaturalIdRepository<ConferenceRoom, String> {
}

因此,我们将能够使用丰富的Repository 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 Repository并对其进行了丰富,以利用Hibernate的这一功能。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章