Skip to content

Latest commit

 

History

History
229 lines (170 loc) · 8.59 KB

File metadata and controls

229 lines (170 loc) · 8.59 KB

On Persistence

  • Author: Philippe Collet

We focus here on the setup of the persistence layer using:

  • a dockerized Postgresql DB (as a real relational database)
  • a H2 in-memory database for testing (which is the default setup in Spring JPA for testing)

Configuration

We have to first extend the pom.xml:

        <dependency>
            <groupId>org.springframework.boot</groupId> <!-- JPA + hibernate-core default support -->
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>

Since SpringBoot 3.2, we don't have to define persistence.properties files, and everything should be defined in the main application.yaml file in src/main/resources and in src/test/resources.

For the main application.yaml file, we define the following properties:

# postgres DB # IN DOCKER COMPOSE SHOULD BE OVERRIDEN BY ENV VARIABLES

spring:
  datasource:
    # POSTGRES_USER
    username: postgresuser
    # POSTGRES_PASSWORD
    password: postgrespass
    url: jdbc:postgresql://${POSTGRES_HOST}/tcf-db
    driver-class-name: org.postgresql.Driver

  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: true
    show-sql: true
    generate-ddl: true
    open-in-view: false

While the test/resources/application.yaml file is simply :

spring:
  ...
  jpa:
    properties:
      hibernate:
        format_sql: true
    show-sql: true
    open-in-view: false

Actually, there is no definition for a datasource as we reuse the default H2 in-memory database provided by SpringBoot for tests. In our setup, the JPA and H2 configuration enables one to see SQL queries generated on all JPA calls on the console (change the spring.jpa.show-sql to false to switch it off).

Annotating Classes to create Entities

Now, our technical setup is complete, once and for all. We can focus on the business part, i.e., storing cookies, customers and related orders.

JPA Entities are simple POJO with annotations. Excepting that the annotations here are related to persistence instead of functional concerns. And that entities need a little bit more than simple annotations:

  • An empty constructor
  • A proper equals method that relies on business items
  • a proper hashCode method to support objects identification in JPA caches.

The equals and hashCode methods must rely on so-called business keys. It is extremely dangerous (and an hideous abstraction leak) to rely at the application level on elements generated by the database layer: you are losing control on object uniqueness. As a consequence, you must be able to define how two entities are equals from a business point of view (the architectural level), and not a technical one (the database one). Here is the example from the Customer class:

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Customer customer)) return false;
        return Objects.equals(name, customer.name) && Objects.equals(creditCard, customer.creditCard);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, creditCard);
    }

Persisting Items

An Item does not exist by itself: it is part of a given cart, or a given order. As a consequence, it is an Embeddable entity. Its associated cookie is based on an enumeration, and thus declared as an Enumerated property. For clarity and log purposes, we prefer to use string literals (the cookie's name) instead of ordinals (the cookie's index in the enum) at the persistence level. The quantity cannot be null, so we simply add a validation constraint that will prevent to persist an entity with a null quantity of cookies. The cookie cannot be null too, so we use the same kind of constraints.

@Embeddable
public class Item {

    @Enumerated(EnumType.STRING)
    @NotNull
    private Cookies cookie;

    @NotNull
    private int quantity;
	
	//...
}

Persisting Orders

The `Order`` class is persisted using the following annotations:

  • The orderStatus field refers to an enumerated, this is the very same situation than the one encountered before for the cookie stored inside an item;
  • The customer attribute refers to another entity, and considering that (i) an order will belong to a single customer and (ii) a customer will hopefully make multiple orders, we annotate this attribute as a ManyToOne: many orders related to one single customer;
  • The items field is a collection of elements that cannot exist by themselves (as we defined an Item as an embeddable entity). We use the brand new ElementCollection annotation (available since JPA 2.0) that implements this intention.
  • The price needs to be positive while the String payReceiptId has to be a real String, which can be specified by @NotBlank.

We are still encountering a big issue. The Order class will be, like the others, automatically translated by OpenJPA into a SQL entity, i.e., a table named ORDER. You're not seeing the point? Imagine the query that will select the data associated to a given customer, and order her orders based on their status. Seeing it? Ordering the orders? ORDER is a SQL keyword, and thus cannot be used as a regular identifier! We need to adapt the way JPA will map the name, using the @Table annotation.

import javax.annotation.processing.SupportedAnnotationTypes;

@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    @NotNull
    private Customer customer;

    @ElementCollection
    private Set<Item> items;

    @Positive
    private double price;

    @NotBlank
    private String payReceiptId;

    @Enumerated(EnumType.STRING)
    @NotNull
    private OrderStatus status;

    // ...
}

Persisting Customers

We make the following changes to the Customer class:

  • The name cannot be blank;
  • The creditCard follows a regular pattern that we should verify to avoid storing inconsistent data. The pattern is defined as a regular expression (10 digits, i.e., \\d{10}+ in java words);
  • The orders field is actually the counterpart of the customer one in the Order entity. As a consequence it is defined as a OneToMany relationship: one customer is linked to many orders. As the customer is the owner of the orders, it defines the mappedBy attribute needed to ensure the bidirectional relationship.
  • The equals method changed to be persistent-compliant as shown above
@Entity
public class Customer {

    @Id
    @GeneratedValue
    private Long id;

    @NotBlank
    @Column(unique = true)
    private String name;

    @Pattern(regexp = "\\d{10}+", message = "Invalid creditCardNumber")
    private String creditCard;

    @OneToMany(cascade = {CascadeType.REMOVE}, fetch = FetchType.LAZY, mappedBy = "customer")
    private Set<Order> orders = new HashSet<>();

    @ElementCollection
    private Set<Item> cart = new HashSet<>();

	// ...
}

Repositories

We don't need the in-memory implementation anymore. We rely here on the Spring Data repositories for Customers and Orders:

@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
    Optional<Customer> findCustomerByName(String name);
}

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
}

We can then use them by injection, for example to implement the Orderer with save. Note that this is the DB that gives the ID value.

@Service
public class Orderer implements OrderCreator, OrderFinder, OrderModifier {

    final OrderRepository orderRepository;

    public Orderer(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    @Transactional(propagation = Propagation.MANDATORY) // must be called within a transaction
    public Order createOrder(Customer customer, double price, String payReceiptId) {
        return orderRepository.save(new Order(customer, customer.getCart(), price, payReceiptId));
    }

Note that the repository is only used to save the first time the entity or find it back after being outside a transaction (and re-entering a new one). For transaction principles, object synchronization with the DB and other details on @Transactional in components and in tests, see the lecture!