Saturday, November 16, 2024

Implementing UUIDs as Primary Keys in Spring Boot

springboot,java,hibernate,jpa,programming,software development,technology,software engineering
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.
Implementing UUIDs as Primary Keys in Spring Boot
Now, look at our order master database table.
Implementing UUIDs as Primary Keys in Spring Boot
We can see that ULID is set as values to the Id column.

And here is our order details database table.
Implementing UUIDs as Primary Keys in Spring Boot
ULID is also a value for the primary and foreign key columns.

Now we'll call another endpoint to get order details by ID.
Implementing UUIDs as Primary Keys in Spring Boot

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!!! 😊

Popular posts