Chapter 3 Using Data

Spring combat

Chapter 3 Using Data

JDBC read and write data

  • When dealing with relational data, Java developers have a variety of options, the most common of which are JDBC and JPA. Spring supports these two abstract forms at the same time, which can make the use of JDBC or JPA easier. In this section, we will discuss how Spring supports JDBC, and then discuss Spring's support for JPA.
  • Spring's support for JDBC is due to the JdbcTemplate class. It allows us to avoid using boilerplate code when performing SQL operations.
  • Below is a simple query.
public Ingredient findOne(String id) {
    Connection connection = null;
    PreparedStatement statement = null;
    ResultSet resultSet = null;
    try {
        connection = dataSource.getConnection();
        statement = connection.prepareStatement(
                "select id, name, type from Ingredient where id = ?"
        );
        statement.setString(1, id);
        resultSet = statement.executeQuery();
        Ingredient ingredient = null;
        if(resultSet.next()) 
        {
            ingredient = new Ingredient(
                    resultSet.getString("id"),
                    resultSet.getString("name"),
                    Ingredient.Type.valueOf(resultSet.getString("type"))
            );
        }
        return ingredient;
    }
    catch (SQLException e)
    {
        // 无法解决问题
    }
    finally
    {
        if(resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
            }
        }
        if(statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
            }
        }
        if(connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
            }
        }
    }
    return null;
}
  • When creating links, creating statements, or executing queries, many errors may occur. This requires us to catch SQLException, which may be helpful or useless to find out where the problem occurred or how to solve the problem.
  • SQLException is a checked exception, which needs to be handled in the catch code block. However, for common problems, such as failure to create a connection to the database or an error in the input query, there is nothing that can be done in the catch code block, and it may continue to be thrown to facilitate upstream processing. For comparison, here is how to use JdbcTemplate.
private JdbcTemplate jdbc;
@Override
public Ingredient findOne(String id){
    return jdcb.queryForObject(
    	"select id, name, type from Ingredient where id = ?",
        this::mapRowToIngredient, id
    );
}

private Ingredient mapRowToIngredient(ResultSet rs, int rowNum) throws SQLException
{
    return new Ingredient(
    	rs.getString("id"),
        rs.getString("name"),
        Ingredient.Type.valueOf(rs.getString("type"))
    );
}

Adjust domain objects to adapt to persistence

  • When persisting an object to the database, it is usually best to have a field as the only identifier of the object. The Ingredient class now has an id field, but we also need to add the id field to the Taco and Order classes.
  • In addition, it may be useful to record when Taco and Order were created. Therefore, we will also add a field for each object to capture the date and time it was created.
package tacos;

import lombok.Data;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.sql.Date;
import java.util.List;

@Data
public class Taco {
    private Long id;
    private Date createdA;
    @NotNull
    @Size(min=5, message="Name must be ad least 5 characters long")
    private String name;
    @NotNull(message = "You must choose at least 1 ingredient")
    private List<String> ingredients;
}
  • The Order class also undergoes similar changes.

Use JdbcTemplate

  • Before you start using JdbcTemplate, you need to add it to the project's classpath. At the same time, add embedded database H2 (more convenient and simple).
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
Define JDBC repository
  • Our Ingredient repository needs to complete the following operations:
  • Query all ingredient information and put them in a collection of Ingredient objects
  • According to id, query a single Ingredient;
  • Save the Ing object.
  • The following IngredientRepository interface defines three operations in the form of method declarations:
package tacos.data;

import tacos.Ingredient;

public interface IngredientRepository {
    Iterable<Ingredient> finaAll();
    Ingredient findOne(String id);
    Ingredient save(Ingredient ingredient);
}
  • The following is to write an IngredientRepository implementation, using JdbcTemplate to query the database.
package tacos.data;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;

@Repository
public class JDBCIngredientRepository implements IngredientRepository{
    private JdbcTemplate jdbc;
    
    @Autowired
    public JDBCIngredientRepository(JdbcTemplate jdbc)
    {
        this.jdbc = jdbc;
    }
    
}
  • As you can see, JDBCIngredientRepository adds @Repository annotation, Spring defines a series of stereotype annotations, @Repository is one of them, other annotations also include @Controller and @Component. After adding the @Repository annotation to JDBCIngredientRepository, Spring's component scan will automatically find it and initialize it as a bean in the Spring application context.
  • When Spring creates the JdbcIngredientRepository bean, it will inject the JdbcTemplate into it through the constructor annotated by @Autowired. This constructor assigns JdbcTemplate to an instance variable, which will be used by other methods to perform database queries and insert operations. The following is the implementation of findAll() and findOne():
@Override
public Ingredient findOne(String id) {
    return jdbc.queryForObject(
        "select id, name, type from Ingredient where id = ?",
        this::mapRowToIngredient
    );
}

@Override
public Iterable<Ingredient> finaAll() {
    return jdbc.query("select id, name, type, from Ingredient", 
                      this::mapRowToIngredient);
}

private Ingredient mapRowToIngredient(ResultSet rs, int rowNum) throws SQLException
{
    return new Ingredient(
        rs.getString("id"),
        rs.getString("name"),
        Ingredient.Type.valueOf(rs.getString("type"))
    );
}
  • findAll() and findOne() use JdbcTemplate in the same way. The findAll() method uses the query() method of JdbcTemplate instead of returning a collection of objects. This method will accept the SQL to be executed and an implementation of Spring RowMapper (used to map each row of data in the result set to an object). This method can also accept the parameters required in the query in the form of final parameters. But no parameters are needed in this example.
  • The findOne() method is expected to only return an Ingredient object, so the queryForObject() method of JdbcTemplate is used. In this example, he accepts the query to be executed, RowMapper, and the id of the Ingredient to be obtained, which will replace the "?" in the query.
Insert a row of data
  • The update() method of JdbcTemplate can be used to execute query statements that write or update data to the database.
@Override
public Ingredient save(Ingredient ingredient) {
    jdbc.update(
        "insert into Ingredient (id, name, type) values (?, ?, ?)",
        ingredient.getId(),
        ingredient.getName(),
        ingredient.getType().toString()
    );
    return ingredient;
}
  • Because there is no need to map ResultSet data to objects, the update() method is much simpler than query() or queryForObject(). It only needs a String containing the SQL to be executed and the value corresponding to each query parameter.
  • After the JdbcIngredientRepository is written, we can inject it into DesignTacoController, and then use it to provide a list of Ingredient objects instead of using hard-coded values.
private final IngredientRepository ingredientRepo;

@Autowired
public DesignTacoController(IngredientRepository ingredientRepo)
{
    this.ingredientRepo = ingredientRepo;
}   

@GetMapping
public String showDesignForm(Model model){
    List<Ingredient> ingredients = new ArrayList<>();
    ingredientRepo.finaAll().forEach(i->ingredients.add(i));

    Type[] types = Ingredient.Type.values();
    for(Type type : types)
    {
        model.addAttribute(type.toString().toLowerCase(), filterByType(ingredients, type));
    }
    return "design";
}
  • It should be noted that the second line of the showDesignForm() method calls the findAll() method of the injected IngredientRepository. This method will get all ingredients from the database, filter them into different types and put them in the model.

Define patterns and preload data

In addition to the Ingredient table, we also need some other tables to store order and design information.

  • Ingredient: Save ingredient information
  • Taco: Save information related to Taco design
  • Taco_Ingredients: Each row of data in Taco corresponds to one or more rows, mapping taco and related ingredients together
  • Taco_Order: save the necessary order details
  • Taco_Order_Tacos: Each row of data in Taco_Order is a bet to win one or more rows, mapping the order and its related tacos together.

Below is the SQL to create the table

create table if not exists Ingredient(
    id varchar(4) not null ,
    name varchar(25) not null,
    type varchar(10) not null
);

create table if not exists Taco(
    id identity ,
    name varchar(50) not null,
    createdAt timestamp not null
);

create table if not exists Taco_Ingredients(
    taco bigint not null,
    ingredient varchar(4) not null
);

alter table Taco_Ingredients add foreign key (taco) references Taco(id);
alter table Taco_Ingredients add foreign key (ingredient) references Ingredient(id);

create table if not exists Taco_Order(
    id identity ,
    deliveryName varchar(50) not null,
    deliveryStreet varchar(50) not null,
    deliveryCity varchar(50) not null,
    deliveryState varchar(2) not null,
    deliveryZip varchar(10) not null,
    ccNumber varchar(16) not null,
    ccCVV varchar(3) not null,
    placedAt timestamp not null
);

create table if not exists Taco_Order_Tacos(
    tacoOrder bigint not null,
    taco bigint not null
);

alter table Taco_Order_Tacos add foreign key (tacoOrder) references Taco_Order(id);
alter table Taco_Order_Tacos add foreign key (taco) references Taco(id);
  • The following is the sql that preloads the ingredient data. Both sql are stored in the "src/main/resources" file
delete from Taco_Order_Tacos;
delete from Taco_Ingredients;
delete from Taco;
delete from Taco_Order;
delete from Ingredient;


insert into Ingredient (id, name, type) values ('FLTO', 'Flour Tortilla', 'WRAP');
insert into Ingredient (id, name, type) values ('COTO', 'Corn Tortilla', 'WRAP');
insert into Ingredient (id, name, type) values ('GRBF', 'Ground Beef', 'PROTEIN');
insert into Ingredient (id, name, type) values ('CARN', 'Carnitas', 'PROTEIN');
insert into Ingredient (id, name, type) values ('TMTO', 'Diced Tomatoes', 'VEGGIES');
insert into Ingredient (id, name, type) values ('LETC', 'Lettuce', 'VEGGIES');
insert into Ingredient (id, name, type) values ('CHED', 'Cheddar', 'CHEESE');
insert into Ingredient (id, name, type) values ('JACK', 'Monterrey Jack', 'CHEESE');
insert into Ingredient (id, name, type) values ('SLSA', 'Salsa', 'SAUCE');
insert into Ingredient (id, name, type) values ('SRCR', 'Sour Cream', 'SAUCE');

Insert data

  • Now you have roughly seen how to use JdbcTemplate to write data into the database. The save() method of JDBCIngredientRepository uses the update() method of JdbcTemplate to save the Ingredient object in the database.
  • With the help of JdbcTemplate, there are two ways to save data
  • Use the update() method directly
  • Use SimpleJdbcInsert wrapper class.
Use JdbcTemplate to save data
  • Now, the only thing that taco and order repositories need to do is save the corresponding objects. In order to save Taco objects, TacoRepository declares a save() method.
package tacos.data;

import tacos.Taco;

public interface TacoRepository {
    Taco save(Taco design);
}
  • In order to implement TacoRepository, we need to use the save() method to first save the necessary taco design details (such as name and creation time), and then insert a row of data into Taco_Ingredients for each ingredient in the Taco object.
package tacos.data;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.PreparedStatementCreatorFactory;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import tacos.Ingredient;
import tacos.Taco;

import java.util.Date;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Arrays;

@Repository
public class JdbcTacoRepository implements TacoRepository{
    private JdbcTemplate jdbc;

    public JdbcTacoRepository(JdbcTemplate jdbc)
    {
        this.jdbc = jdbc;
    }

    @Override
    public Taco save(Taco taco) {
        long tacoId = saveTacoInfo(taco);
        taco.setId(tacoId);
        for(Ingredient ingredient : taco.getIngredients())
        {
            saveIngredientToTaco(ingredient, tacoId);
        }
        return taco;
    }

    private long saveTacoInfo(Taco taco)
    {
        taco.setCreatedAt(new Date());
        PreparedStatementCreator psc = new PreparedStatementCreatorFactory(
                "insert into Taco (name, createdAt) values (?, ?)",
                Types.VARCHAR, Types.TIMESTAMP
        ).newPreparedStatementCreator(
                Arrays.asList(
                        taco.getName(),
                        new Timestamp(taco.getCreatedAt().getTime())
                )
        );
        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbc.update(psc, keyHolder);
        return keyHolder.getKey().longValue();
    }

    private void saveIngredientToTaco(Ingredient ingredient, long tacoId)
    {
        jdbc.update(
                "insert into Taco_Ingredients (taco, ingredient)"
                + "values (?, ?)",
                tacoId, ingredient.getId()
        );
    }
}
  • As you can see, the save() method first calls the private saveTacoInfo() method, and then uses the taco ID returned by the method to call saveIngredientToTaco(). The last method saves each ingredient. The problem here lies in the details of the saveTacoInfo() method.
  • When inserting a row of data into Taco, we need to know the ID generated by the database so that we can reference it in each ingredient information. The update() method used when saving ingredient data cannot help us get the generated ID, so here we need a different update() method.
  • The update() method here needs to accept a PreparedStatementCreator and a KeyHolder. KeyHolder will provide us with the generated taco ID. But in order to use this method, we must create a PreparedStatementCreator.
  • Creating PreparedStatementCreator is not simple. First, we need to create a PreparedStatementCreatorFactory, pass the SQL we want to execute to him, and include the type of each query parameter. Then, you need to call the newPreparedStatementCreator() method of the factory class and pass in the required values ​​of the query parameters, so that the PreparedStatementCreator can be generated.
  • With PreparedStatementCreator, you can call the update() method, and you need to pass in PreparedStatementCreator and KeyHolder (that is, the GeneratedKeyHolder instance). After the update() call is completed, you can return the taco ID through KeyHolder.getKey().longValue().
  • Back to the save() method, each Ingredient in Taco will be polled and saveIngredientToTaco() will be called. saveIngredientToTaco() uses a simpler update() form to save the reference to the ingredients in the Taco_Ingredients table.
  • For TacoRepository, the remaining thing is to inject it into DesignTacoController and call it when saving taco.
private final IngredientRepository ingredientRepo;
private TacoRepository designRepo;

@Autowired
public DesignTacoController(IngredientRepository ingredientRepo, TacoRepository designRepo)
{
    this.ingredientRepo = ingredientRepo;
    this.designRepo = designRepo;
}
  • That is, the constructor accepts both IngredientRepository and TacoRepository objects. And assign it to the instance variable.
@ModelAttribute(name="order")
public Order order()
{
    return new Order();
}

@ModelAttribute(name="taco")
public Taco taco()
{
    return new Taco();
}   

@PostMapping
public String processDesign(@Valid @ModelAttribute("design") Taco design, Errors errors, @ModelAttribute Order order){
    if(errors.hasErrors())
        return "design";

    Taco saved = designRepo.save(design);
    order.addDesign(saved);

    log.info("Processing design:" + design);
    return "redirect:/orders/current";
}
  • The DesignTacoController class has added the @SessionAttributes("order") annotation, and there is a new method annotated with @ModelAttribute, the order() method. Similar to the taco() method, the @ModelAttribute annotation on the order() method ensures that an Order object will be created in the model. But unlike the Taco object in the model, we need the order information to appear in multiple requests, so that we can create multiple tacos and add them to the order. Class-level @SessionAttributes can specify that model objects (such as order attributes) should be stored in the session so that they can be used across requests.
  • The processing of taco design is in the processDesign() method. This method accepts the Order object as a parameter, and also includes Taco and Errors objects. The Order parameter is annotated with @ModelAttribute, indicating that its value should come from the model, and Spring MVC will not try to bind request parameters to it.
  • After checking the verification errors, processDesign() uses the injected TacoRepository to save the taco. Then, it saves the Taco object to the Order in the session.
  • In fact, before the user completes the operation and submits the order form, the Order object will always be saved in the session, and not saved in the database. At that time, OrderController needs to call the implementation of OrderRepository to save the order. The following is the implementation class.
Use SimpleJdbcInsert to insert data
  • Taco ID is obtained through KeyHolder and PreparedStatementCreator.
  • When saving an order, a similar situation exists. Not only need to save the order data in the Taco_Order table, but also save the order's reference to each taco in the Taco_Order_Tacos table. However, at this time, the cumbersome PreparedStatementCreator is no longer used, but SimpleJdbcInsert is introduced, which wraps the JdbcTemplate and can insert data into the table more easily.
package tacos.data;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;

@Repository
public class JdbcOrderRepository implements OrderRepository{
    private SimpleJdbcInsert orderInserter;
    private SimpleJdbcInsert orderTacoInserter;
    private ObjectMapper objectMapper;
    
    @Autowired
    public JdbcOrderRepository(JdbcTemplate jdbc)
    {
        this.orderInserter = new SimpleJdbcInsert(jdbc)
                .withTableName("Taco_Order")
                .usingGeneratedKeyColumns("id");
        this.orderTacoInserter = new SimpleJdbcInsert(jdbc)
                .withTableName("Taco_Order_Tacos");
        this.objectMapper = new ObjectMapper();
    }
}
  • JdbcOrderRepository injects JdbcTemplate into it through the constructor. But here, instead of assigning JdbcTemplate directly to instance variables, two SimpleJdbcInsert instances are constructed using it. The first one is assigned to the orderInserter instance variable, which is configured to cooperate with the Taco_Order table and assumes that the id attribute will be provided or generated by the database. The second instance is assigned to the orderTacoInserter instance variable, configured to cooperate with the Taco_Order_Tacos table, but it does not declare how the ID in the table is generated.
  • The constructor also creates an instance of the ObjectMapper class in Jackson and assigns it to an instance variable. Although Jackson's original intention is to process JSON, here can help us save orders and associated tacos.
  • The following is the implementation of the save() method and an example of how to use SimpleJdbcInsert.
@Override
public Order save(Order order) {
    order.setPlacedAt(new Date());
    long orderId = saveOrderDetails(order);
    order.setId(orderId);
    List<Taco> tacos = order.getTacos();
    for(Taco taco : tacos)
    {
        saveTacoToOrder(taco, orderId);
    }
    return order;
}

private long saveOrderDetails(Order order)
{
    @SuppressWarnings(value = "unchecked")
    Map<String, Object> values = ObjectMapper.convertValue(order, Map.class);
    values.put("placedAt", order.getPlacedAt());

    long orderId = orderInserter.executeAndReturnKey(values)
        .longValue();
    return orderId;
}

private void saveTacoToOrder(Taco taco, long orderId)
{
    Map<String, Object> values = new HashMap<>();
    values.put("tacoOrder", orderId);
    values.put("taco", taco.getId());
    orderTacoInserter.execute(values);
}
  • The save() method does not actually save anything, it just defines the process of saving the Order and its associated Taco objects, and delegates the actual persistence tasks to saveOrderDetails() and saveTacoToOrder().
  • SimpleJdbcInsert has two very useful methods to perform data insertion operations: execute() and executeAndReturnKey(). They all accept Map<String, Object> as parameters, where the key of the Map corresponds to the column name, and the value corresponds to the actual value to be inserted.
  • Order has many attributes, which have the same names as the corresponding columns. In view of this, we use ObjectMapper and its convertValue() method to facilitate the conversion of Order to Map. The reason for this is because ObjectMapper converts the Date attribute to long, which leads to incompatibility with the placedAt field in the Taco_Order table.
  • Now you can inject OrderRepository into OrderController and start using it. Below is the complete OrderController
package tacos.web;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import tacos.Order;
import tacos.data.OrderRepository;

import javax.validation.Valid;


@Slf4j
@Controller
@RequestMapping("/orders")
@SessionAttributes("order")
public class OrderController {
    private OrderRepository orderRepo;

    public OrderController(OrderRepository orderRepo)
    {
        this.orderRepo = orderRepo;
    }

    @GetMapping("/current")
    public String orderForm()
    {
        return "orderForm";
    }

    @PostMapping
    public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus)
    {
        if(errors.hasErrors())
            return "orderForm";

        orderRepo.save(order);
        sessionStatus.setComplete();
        log.info("Order submitted: "+order);
        return "redirect:/";
    }
}
  • In addition to injecting OrderRepository into the controller, the only obvious change in OrderController is the processOrder() method. In this method, the Order object submitted through the form will be saved through the save() method of the injected OrderRepository.
  • After the order is saved, there is no need to hold it in the session. In fact, if it is not cleaned up, it will continue to be associated with the next taco requested.

Use Spring Data JPA to persist data

  • Spring Data is a very large umbrella project consisting of multiple sub-projects, most of which focus on data persistence for different database types. Several popular Spring Data projects include:
  • Spring Data JPA: JPA persistence based on relational database
  • Spring Data MongoDB: Persistence to the Mongo document database
  • Spring Data Neo4j: Persist to Neo4j graph database
  • Spring Data Redis: Persistence to Redis Key-value storage
  • Spring Data Cassandra: Persistence to Cassandra database
  • Spring Data provides one of the most interesting and useful features for all projects, which is to automatically generate the repository function based on the repository specification interface.
  • To understand how Spring Data works, we need to start over and replace the previous JDBC-based repository with a Spring Data JPA repository. First, you need to add Spring Data JPA to the build file of the project.

Add Spring Data JPA to the project

  • Spring Data applications can add Spring Data JPA through the JPA starter. This starter will not only introduce Spring Data JPA, but also transitively introduce Hibernate as a JPA implementation.
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Mark domain objects as entities

  • In terms of creating a repository, Spring Data has done a lot of useful things for us. However, the use of JPA mapping annotations to annotate domain objects does not provide much help. We need to open the Ingredient, Taco and Order classes and add some annotations to them. The following is the Ingredient class:
package tacos;

import javax.persistence.Entity;
import javax.persistence.Id;

import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;

@Data
@RequiredArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
@Entity
public class Ingredient {

    @Id
    private final String id;
    private final String name;
    private final Type type;

    public static enum Type {
        WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
    }
}
  • In order to declare Ingredient as a JPA entity, the @Entity annotation must be added. Its id attribute needs to be annotated with @Id so that it can be designated as the attribute that uniquely identifies the entity in the database.
  • In addition to JPA-specific annotations, we also added @NoArgsConstructor annotations at the class level. JPA requires entities to have a parameterless constructor. Lombok's @NoArgsConstructor annotation can help us achieve this. But I don't want to use it directly, so it becomes private by setting the access property to AccessLevel.PRIVATE. Because there are final attributes that must be set, we set force to true so that the constructor generated by Lombok will set them to null.
  • We also added a @RequiredArgsConstructor annotation. The @Data annotation will add a parameterized constructor for us, but after using @NoArgsConstructor, this constructor will be removed. We explicitly add the @RequiredArgeConstructor annotation to ensure that there is another constructor in addition to the private parameterless constructor. Parameter constructor.
  • Below is the Taco class
package tacos;

import lombok.Data;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Date;
import java.util.List;

@Data
@Entity
public class Taco {
    
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    
    private Date createdAt;
    
    @NotNull
    @Size(min=5, message="Name must be ad least 5 characters long")
    private String name;
    
    @ManyToMany(targetEntity = Ingredient.class)
    @NotNull(message = "You must choose at least 1 ingredient")
    private List<Ingredient> ingredients;
    
    @PrePersist
    void createdAt(){
        this.createdAt = new Date();
    }
}
  • Similar to Ingredient, the Taco class now has an @Entity annotation and an @Id annotation for its id attribute. Because we need to rely on the database to automatically generate the ID value, we also set @GeneratedValue for the id attribute and set its strategy to AUTO
  • In order to declare the relationship between Taco and its associated Ingredient list, we added @ManyToMany annotations to ingredients. Each Taco can have multiple Ingredients, and each Ingredient can be multiple Taco components.
  • There is a new method createdAt() with @PrePersist annotation. Before Taco persists, we will use this method to set createdAt to the current date and time. Finally, we will mark the Order object as an entity. Below is the Order class
package tacos;

import lombok.Data;
import org.hibernate.validator.constraints.CreditCardNumber;

import javax.persistence.*;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import java.io.Serializable;
import java.util.Date;
import java.util.ArrayList;
import java.util.List;

@Data
@Entity
@Table(name = "Taco_Order")
public class Order implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private Date placedAt;
    @NotBlank(message = "Name is required")
    private String deliveryName;

    @NotBlank(message = "Street is required")
    private String deliveryStreet;

    @NotBlank(message = "City is required")
    private String deliveryCity;

    @NotBlank(message = "State is required")
    private String deliveryState;

    @NotBlank(message = "Zip code is required")
    private String deliveryZip;

    @CreditCardNumber(message = "Not a valid credit card number")
    private String ccNumber;

    @Pattern(regexp = "^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$",
        message = "Must be formatted MM/YY")
    private String ccExpiration;

    @Digits(integer = 3, fraction = 0, message = "Invalid CVV")
    private String ccCVV;
    
    @ManyToMany(targetEntity = Taco.class)
    private List<Taco> tacos = new ArrayList<>();
    
    public void addDesign(Taco design) {
        this.tacos.add(design);
    }
    
    @PrePersist
    void placedAt(){
        this.placedAt = new Date();
    }
}
  • The Order class is basically the same as the Taco change, but there is a new annotation at the class level, namely @Table. Indicates that the Order entity should be persisted to a table named Taco_Order in the database. Mainly because if not, JPA will persist the entity to the table named Order by default, but order is a reserved word in SQL.

Declare the JPA repository

  • In the JDBC version of the repository, we explicitly declare the methods we want the repository to provide. However, with Spring Data, we can extend the CrudRepository interface. For example, below is the new IngredientRepository interface.
package tacos.data;

import org.springframework.data.repository.CrudRepository;
import tacos.Ingredient;

public interface IngredientRepository 
       extends CrudRepository<Ingredient, String>{

}
  • CrudRepository defines many methods for CRUD (create, read, update, delete) operations. It is parameterized. The first parameter is the type of questions to be persisted in the repository, and the second parameter is the type of the entity ID attribute. For IngredientRepository, the parameters should be Ingredient and String.
  • Below is TacoRepository
package tacos.data;

import org.springframework.data.repository.CrudRepository;
import tacos.Taco;

public interface TacoRepository
        extends CrudRepository<Taco, Long>{
}
  • Below is OrderRepository
package tacos.data;

import org.springframework.data.repository.CrudRepository;
import tacos.Order;

public interface OrderRepository extends CrudRepository<Order, Long> {
    Order save(Order order);
}
  • Now there are 3 repositories. The advantage of Spring Data JPA is that we don't need to write their implementation classes. When the application starts, Spring Data JPA will automatically generate the implementation class at runtime. This means that we can use these repositories now. Just inject them into the controller as you would with a JDBC-based implementation.
  • The methods provided by CrudRepository are very useful for general-purpose persistence of entities. But what if our needs are not limited to basic persistence. The following is how to customize the repository to perform queries in a specific field.

Custom JPA repository

  • Assume that in addition to the basic CRUD operations provided by CrudRepository, we also need to obtain orders delivered to the specified zip code (Zip). In fact, we only need to add the following method declaration to OrderRepository:
List<Order> findByDeliveryZip(String deliveryZip);
  • When the repository implementation is created, Spring Data will check all the methods of the repository interface, resolve the name of the method, and try to guess the purpose of the method based on the persisted object. Essentially, Spring Data defines a set of small Domain-Specific Language (DSL), where the details of persistence are described by the signature of the repository method.
  • Spring Data can know that this method is to find Order, because we use Order to parameterize the CrudRepository. The method name findByDeliveryZip() determines that the method needs to find the Order based on the deliveryZip attribute matching, and the value of deliveryZip is passed to the method as a parameter.
  • For example, List<Order> readOrderByDeliveryZipAndPlaceAtBetween(String deliveryZip, Date startDate, Date endDate);Spring Data parses and understands the method name in this way when generating the repository implementation: read can also use get and find to read the data, by starting to declare the attributes to be matched, deliveryzip to match the .delivery.zip attribute, and table side by side , Placedat matches the .placeAt or .placed.at attributes, and between indicates that the value must be within a given range.
  • There are many operators similar to between. In addition, you can also add AllIgnoringCase or AllIgnoreCase to ignore the case of all String comparisons.
  • You can add OrderBy at the end to sort the result set according to a certain column, for example, according to the deliveryTo attribute:List<Order> findByDeliveryCityOrderByDeliveryTo(String city);
  • Although method name conventions are useful for relatively simple queries, it is not difficult to imagine that method names for more complex queries face the risk of getting out of control. In this method, you can define the method as any name and add @Query annotations to it to indicate the query to be executed when the method is called, as shown below:
@Query("from Order o where o.deliveryCity='Seattle'")
List<Order> readOrderDeliveryInSeattle();
  • In this example, by using @Query, it is stated that only all orders delivered to Seattle will be queried.