How to Use Hibernate Natural IDs in Spring Boot
This article is a quick practical guide for using Hibernate Natural IDs (@NaturalId) in Spring Data style.
Join the DZone community and get the full member experience.
Join For FreeThis article is a quick practical guide for using Hibernate Natural IDs (@NaturalId
) in Spring Data style. Mainly, we want to expose the Hibernate,bySimpleNaturalId()
and byNaturalId()
methods via a typical Spring Data repository, and to call them exactly as we call the well-known, findAll()
,findOne()
, and so on.
Implementation
First, let's focus on the implementation of the needed classes. Having all this in place, we will be able to provide repositories for our entities with natural IDs.
Writing an Entity With Natural ID
Let's consider the following entity that has an auto-generated ID and a natural ID (thecode
column). This is just a typical entity using one natural ID via @NaturalId
:
@Entity
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@NaturalId(mutable = false)
@Column(nullable = false, updatable = false, unique = true, length = 50)
private String code;
// getters and setters
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Product)) {
return false;
}
Product naturalIdProduct = (Product) o;
return Objects.equals(getCode(), naturalIdProduct.getCode());
}
@Override
public int hashCode() {
return Objects.hash(getCode());
}
@Override
public String toString() {
return "Product{" + "id=" + id + ", name=" + name + ", code=" + code + '}';
}
}
Writing the NaturalRepository Contract
We begin by writing an interface named, NaturalRepository
. Basically, when we want to fine-tune a repository, we can rely on @NoRepositoryBean
annotation. In our case, we want to enrich the Spring Data methods arsenal with two more, findBySimpleNaturalId()
andfindByNaturalId()
:
@NoRepositoryBean
public interface NaturalRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
// use this method when your entity has a single field annotated with @NaturalId
Optional<T> findBySimpleNaturalId(ID naturalId);
// use this method when your entity has more than one field annotated with @NaturalId
Optional<T> findByNaturalId(Map<String, Object> naturalIds);
}
Writing the NaturalRepository Implementation
Further, we extend theSimpleJpaRepository
class and implement the NaturalRepository
. By extending theSimpleJpaRepository
we can customize the base repository by adding our needed methods. Mainly, we extend the persistence technology-specific repository base class and use this extension as the custom base class for the repository proxies:
@Transactional(readOnly = true)
public class NaturalRepositoryImpl<T, ID extends Serializable>
extends SimpleJpaRepository<T, ID> implements NaturalRepository<T, ID> {
private final EntityManager entityManager;
public NaturalRepositoryImpl(JpaEntityInformation entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
}
@Override
public Optional<T> findBySimpleNaturalId(ID naturalId) {
Optional<T> entity = entityManager.unwrap(Session.class)
.bySimpleNaturalId(this.getDomainClass())
.loadOptional(naturalId);
return entity;
}
@Override
public Optional<T> findByNaturalId(Map<String, Object> naturalIds) {
NaturalIdLoadAccess<T> loadAccess
= entityManager.unwrap(Session.class).byNaturalId(this.getDomainClass());
naturalIds.forEach(loadAccess::using);
return loadAccess.loadOptional();
}
}
Setting the NaturalRepositoryImpl as the Base Class
Next, we have to instruct Spring Data to rely on our customized repository base class. In Java configuration, you can do so by using the repositoryBaseClass
attribute via the @EnableJpaRepositories
annotation:
@SpringBootApplication
@EnableJpaRepositories(repositoryBaseClass = NaturalRepositoryImpl.class)
public class NaturalIdApplication {
...
}
Let's See How It Works
Now, let's see if it works as expected. First, let's define the ProductRepository
:
@Repository
public interface ProductRepository<T, ID> extends NaturalRepository<Product, Long>{
}
One Natural ID
Further, let's save two products with unique codes (natural ids):
Product tshirt = new Product();
tshirt.setName("T-Shirt");
tshirt.setCode("014-tshirt-2019");
Product socks = new Product();
socks.setName("Socks");
socks.setCode("012-socks-2018");
productRepository.save(tshirt);
productRepository.save(socks);
Finally, let's find the T-Shirt product by its natural ID:
// this should return the t-shirt product wrapped in an Optional
Optional<Product> tshirt = productRepository.findBySimpleNaturalId("014-tshirt-2019");
Note: You can still use productRepository.findById(Object id)
if you want to search by the auto-generated id.
More Natural IDs
An entity can use more than one natural ID. For example, let's assume that next to code
we have sku
as natural ID as well:
@NaturalId(mutable = false)
@Column(nullable = false, updatable = false, unique = true)
private Long sku;
Now, when we persist our product they will look like follows:
Product tshirt = new Product();
tshirt.setName("T-Shirt");
tshirt.setCode("014-tshirt-2019");
tshirt.setSku(1L);
Product socks = new Product();
socks.setName("Socks");
socks.setCode("012-socks-2018");
socks.setSku(2L);
productRepository.save(tshirt);
productRepository.save(socks);
Finding the T-Shirt product can be done like this:
Map<String, Object> ids = new HashMap<>();
ids.put("code", "014-tshirt-2019");
ids.put("sku", 1L);
Optional<Product> tshirt = productRepository.findByNaturalId(ids);
Thanks for reading. Let me know your thoughts in the comments. The source code can be found on GitHub.
Opinions expressed by DZone contributors are their own.
Comments