Hibernates offers several identifier generation strategies, the most popular
being the AUTO strategy. UUIDs (universally unique identifiers)
are frequently preferred over auto-incrementing integers. They provide
uniqueness across distributed systems without requiring coordination. In this
article, we'll write a new annotation type to use UUIDs as primary keys in a
Spring Boot application.
In this article, we will use
ULID (Universally Unique Lexicographically Sortable Identifier) as
the UUID value for primary or foreign keys. The ULID is a
128-bit identifier. The first 48 bits indicate the number of milliseconds
since Unix Epoch. The final 80 bits are generated by a secure random number
generator. We can store ULID values as a 26-character string.
Therefore, to use ULIDs, we will develop a new annotation type, which we will
use in our entity class.
Custom Annotation Type
We developed an UlidGenerator class that implements the BeforeExecutionGenerator interface provided
by Hibernate.
public class UlidGenerator implements BeforeExecutionGenerator {
@Override
public Object generate(SharedSessionContractImplementor sharedSessionContractImplementor,
Object o, Object o1, EventType eventType) {
return UlidCreator.getUlid().toString();
}
@Override
public EnumSet<EventType> getEventTypes() {
return EventTypeSets.INSERT_ONLY;
}
}
BeforeExecutionGenerator
is a generator that produces a value before the execution of a
specific task, such as a method, transaction, or process. The
generate() method executes any Java code. In this case, it produces a
ULID. We used
ULID Creator
to generate ULIDs.
EventTypeSets.INSERT_ONLY
is generally used to produce
identifiers.
Now, it's time to create a new annotation type. Our custom annotation is
Uuid, which was created with
@interface.
@IdGeneratorType(UlidGenerator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Uuid {
}
The annotation type is annotated with
@IdGeneratorType (UlidGenerator.class),
@Retention (RetentionPolicy.RUNTIME), and
@Target ({ElementType.FIELD, ElementType.METHOD}). These annotations
are considered meta-annotations.
@Retention(RetentionPolicy.RUNTIME) indicates that annotations of this
type will be retained by the VM and can be read reflectively at run-time.
@Target({ElementType.FIELD, ElementType.METHOD}) indicates that Uuid
can annotate variable and method declarations.
@IdGeneratorType(UlidGenerator.class) is used to set up a custom
identifier generator. The identifier is generated using the UlidGenerator, which we
previously created.
Entity
Here is our first entity OrderMaster. We have used our custom
annotation, @Uuid along with @Id to generate primary key values.
@Entity
@Table(name = "ORDER_MASTER")
public class OrderMaster {
@Id
@Ulid
@Column(name = "id", unique = true, nullable = false, length = 26)
private String id;
@Column(name = "order_date")
private LocalDate orderDate;
@Column(name = "customer_id", length = 40)
private String customerId;
@Column(name = "order_posted_date", nullable = false, updatable = false)
@CreationTimestamp
private LocalDateTime orderPostedDate;
@JsonManagedReference
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "orderMaster")
private List<OrderDetails> orderDetails;
public String getId() {
return id;
}
public OrderMaster setId(String id) {
this.id = id;
return this;
}
public LocalDate getOrderDate() {
return orderDate;
}
public OrderMaster setOrderDate(LocalDate orderDate) {
this.orderDate = orderDate;
return this;
}
public String getCustomerId() {
return customerId;
}
public OrderMaster setCustomerId(String customerId) {
this.customerId = customerId;
return this;
}
public LocalDateTime getOrderPostedDate() {
return orderPostedDate;
}
public OrderMaster setOrderPostedDate(LocalDateTime orderPostedDate) {
this.orderPostedDate = orderPostedDate;
return this;
}
public List<OrderDetails> getOrderDetails() {
return orderDetails;
}
public OrderMaster setOrderDetails(List<OrderDetails> orderDetails) {
this.orderDetails = orderDetails
.stream()
.map(o -> o.setOrderMaster(this))
.toList();
return this;
}
}
Another entity is OrderDetails. To generate primary key values, we used
both @Uuid and @Id.
@Entity
@Table(name = "ORDER_DETAILS")
public class OrderDetails {
@Id
@Ulid
@Column(name = "ID", unique = true, nullable = false, length = 26)
private String id;
@Column(name = "item_id", length = 40)
private String itemId;
@Column(name = "quantity")
private Integer quantity;
@Column(name = "unit_price")
private BigDecimal unitPrice;
@JsonBackReference
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "order_master_id", referencedColumnName = "ID", nullable = false)
private OrderMaster orderMaster;
public String getId() {
return id;
}
public OrderDetails setId(String id) {
this.id = id;
return this;
}
public String getItemId() {
return itemId;
}
public OrderDetails setItemId(String itemId) {
this.itemId = itemId;
return this;
}
public Integer getQuantity() {
return quantity;
}
public OrderDetails setQuantity(Integer quantity) {
this.quantity = quantity;
return this;
}
public BigDecimal getUnitPrice() {
return unitPrice;
}
public OrderDetails setUnitPrice(BigDecimal unitPrice) {
this.unitPrice = unitPrice;
return this;
}
public OrderMaster getOrderMaster() {
return orderMaster;
}
public OrderDetails setOrderMaster(OrderMaster orderMaster) {
this.orderMaster = orderMaster;
return this;
}
}
We kept these entities, OrderMaster and OrderDetails, in the
@OneToMany relationship. As the primary keys of these entities are of
type UUID, their foreign key values will be of the same type.
Repository
We created IOrderMasterRepository, a repository that extends
JpaRepository.
@Repository
public interface IOrderMasterRepository extends JpaRepository<OrderMaster, String> {
}
Service
OrderMasterService class contains two methods: one to save a new order
and another to fetch order details by Id.
@Service
public class OrderMasterService {
private final IOrderMasterRepository orderMasterRepository;
@Autowired
public OrderMasterService(IOrderMasterRepository orderMasterRepository) {
this.orderMasterRepository = orderMasterRepository;
}
public OrderMaster saveOrder(OrderMasterInput input) {
List<OrderDetails> orderDetails = new ArrayList<>();
if (input.detailsInputs().isPresent()) {
orderDetails = input.detailsInputs().get()
.stream()
.map(e -> new OrderDetails()
.setItemId(e.itemId())
.setQuantity(e.quantity())
.setUnitPrice(e.unitPrice())
).toList();
}
OrderMaster orderMaster = new OrderMaster()
.setOrderDate(input.orderDate())
.setCustomerId(input.customerId())
.setOrderDetails(orderDetails);
orderMaster = this.orderMasterRepository.save(orderMaster);
return orderMaster;
}
public OrderMaster getDetails(String id) {
Optional<OrderMaster> orderMaster = orderMasterRepository.findById(id);
if (orderMaster.isPresent()) {
return orderMaster.get();
} else {
return null;
}
}
}
Controller
The OrderController class contains two endpoints: one for saving new
orders and one for retrieving order details by Id.
@RestController
@Tag(description = "API related to Order.", name = "Order")
@RequestMapping("/order")
public class OrderController {
private final OrderMasterService orderMasterService;
@Autowired
public OrderController(OrderMasterService orderMasterService) {
this.orderMasterService = orderMasterService;
}
@Operation(summary = "Save a new Order.", description = "Save a new Order details.")
@PostMapping(value = "/v1")
public ResponseEntity<AppApiResponse<OrderMaster>> saveOrder(@RequestBody OrderMasterInput input) {
return new ResponseEntity<>(new AppApiResponse<>("SUCCESS", orderMasterService.saveOrder(input), "Data saved!"),
HttpStatus.OK);
}
@Operation(summary = "Get a Order details.", description = "Get a Order details by ID.")
@RequestMapping(value = "/v1", method = RequestMethod.GET)
public ResponseEntity<AppApiResponse<OrderMaster>> getDetails(@RequestParam String id) {
return new ResponseEntity<>(new AppApiResponse<>>("SUCCESS", orderMasterService.getDetails(id), "Data fetched!"),
HttpStatus.OK);
}
}
Testing
First, we'll call the endpoint to save order information to the database.
Now, look at our order master database table.
We can see that ULID is set as values to the Id column.
And here is our order details database table.
ULID is also a value for the primary and foreign key columns.
Now we'll call another endpoint to get order details by ID.
Traditional UUIDs do not follow a natural order. It causes random insertions
in database indexes. On the other hand, as the first 48 bits represent the
number of milliseconds, ULID values follow a natural order by generation
time.
So, in this article, we learned how to set ULID as a database key value
and retrieve details using ULIDs.
You can get the source code from
here.
Happy coding!!! 😊