Spring Boot data访问

1. Spring Boot data访问overview

Spring Boot providing了 many 种data访问方式, including:

  • Spring Data JPA - 基于 JPA ORM framework
  • Spring Data JDBC - 轻量级 JDBC operation
  • MyBatis - 流行 SQL mapframework
  • NoSQL support - Redis, MongoDB, Elasticsearch etc.

in 本章节in, 我们将重点介绍 Spring Data JPA and MyBatis using.

2. Spring Data JPA configuration

Spring Data JPA is Spring ecosystemin用于简化 JPA Development framework.

2.1 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

2.2 configurationdatalibrary连接

in application.properties fileinconfigurationdatalibrary连接:

# datasourcesconfiguration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA configuration
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

2.3 creation实体class

package com.example.demo.entity;

import jakarta.persistence.*;
import lombok.Data;

@Entity
@Table(name = "users")
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String username;
    
    @Column(nullable = false, unique = true)
    private String email;
    
    @Column(nullable = false)
    private String password;
    
    private String fullName;
    private boolean active = true;
}

2.4 creation Repository interface

package com.example.demo.repository;

import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
    Optional<User> findByEmail(String email);
    boolean existsByUsername(String username);
    boolean existsByEmail(String email);
}

3. Spring Data JPA basicoperation

Spring Data JPA providing了丰富 CRUD operationmethod, 无需writing SQL 语句.

3.1 注入 Repository

package com.example.demo.service;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    // 业务method...
}

3.2 CRUD operationexample

// creationuser
User user = new User();
user.setUsername("admin");
user.setEmail("admin@example.com");
user.setPassword("password");
user.setFullName("Administrator");
userRepository.save(user);

// queryuser
Optional<User> optionalUser = userRepository.findById(1L);
optionalUser.ifPresent(System.out::println);

// updateuser
optionalUser.ifPresent(u -> {
    u.setFullName("Updated Admin");
    userRepository.save(u);
});

// deleteuser
userRepository.deleteById(1L);

3.3 自定义querymethod

// 按user名query
Optional<User> userByUsername = userRepository.findByUsername("admin");

// 按邮箱query
Optional<User> userByEmail = userRepository.findByEmail("admin@example.com");

// checkuser名 is 否存 in 
boolean exists = userRepository.existsByUsername("admin");

4. Spring Data JPA advancedquery

Spring Data JPA support many 种advancedquery方式.

4.1 JPQL query

package com.example.demo.repository;

import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    @Query("SELECT u FROM User u WHERE u.active = ?1")
    List<User> findByActiveStatus(boolean active);
    
    @Query("SELECT u FROM User u WHERE u.fullName LIKE %?1%")
    List<User> findByFullNameContaining(String keyword);
}

4.2 原生 SQL query

@Query(value = "SELECT * FROM users WHERE created_at > ?1", nativeQuery = true)
List<User> findByCreatedAtAfter(Date date);

4.3 分页query

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    
    @Autowired
    private UserRepository userRepository;
    
    @GetMapping("/users")
    public Page<User> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        Pageable pageable = PageRequest.of(page, size);
        return userRepository.findAll(pageable);
    }
}

5. MyBatis 集成

MyBatis is 另一个流行 data访问framework, 它providing了强 big SQL mapfunctions.

5.1 添加依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.2</version>
</dependency>

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

5.2 configuration MyBatis

# datasourcesconfiguration
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# MyBatis configuration
mybatis.mapper-locations=classpath:mappers/*.xml
mybatis.type-aliases-package=com.example.demo.entity
mybatis.configuration.map-underscore-to-camel-case=true

5.3 creation实体class

package com.example.demo.entity;

import lombok.Data;

@Data
public class Product {
    private Long id;
    private String name;
    private Double price;
    private Integer stock;
    private String description;
}

5.4 creation Mapper interface

package com.example.demo.mapper;

import com.example.demo.entity.Product;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface ProductMapper {
    List<Product> findAll();
    Product findById(@Param("id") Long id);
    int insert(Product product);
    int update(Product product);
    int delete(@Param("id") Long id);
    List<Product> findByPriceRange(@Param("min") Double min, @Param("max") Double max);
}

5.5 creation XML Mapper file

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.demo.mapper.ProductMapper">
    
    <resultMap id="ProductResultMap" type="Product">
        <id column="id" property="id" />
        <result column="name" property="name" />
        <result column="price" property="price" />
        <result column="stock" property="stock" />
        <result column="description" property="description" />
    </resultMap>
    
    <select id="findAll" resultMap="ProductResultMap">
        SELECT * FROM products
    </select>
    
    <select id="findById" parameterType="Long" resultMap="ProductResultMap">
        SELECT * FROM products WHERE id = #{id}
    </select>
    
    <insert id="insert" parameterType="Product" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO products (name, price, stock, description)
        VALUES (#{name}, #{price}, #{stock}, #{description})
    </insert>
    
    <update id="update" parameterType="Product">
        UPDATE products
        SET name = #{name}, price = #{price}, stock = #{stock}, description = #{description}
        WHERE id = #{id}
    </update>
    
    <delete id="delete" parameterType="Long">
        DELETE FROM products WHERE id = #{id}
    </delete>
    
    <select id="findByPriceRange" resultMap="ProductResultMap">
        SELECT * FROM products WHERE price BETWEEN #{min} AND #{max}
    </select>
</mapper>

5.6 using MyBatis Mapper

package com.example.demo.service;

import com.example.demo.entity.Product;
import com.example.demo.mapper.ProductMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ProductService {
    
    @Autowired
    private ProductMapper productMapper;
    
    public List<Product> getAllProducts() {
        return productMapper.findAll();
    }
    
    public Product getProductById(Long id) {
        return productMapper.findById(id);
    }
    
    public void addProduct(Product product) {
        productMapper.insert(product);
    }
    
    public void updateProduct(Product product) {
        productMapper.update(product);
    }
    
    public void deleteProduct(Long id) {
        productMapper.delete(id);
    }
    
    public List<Product> getProductsByPriceRange(Double min, Double max) {
        return productMapper.findByPriceRange(min, max);
    }
}

6. transactionmanagement

Spring Boot providing了声明式transactionmanagement, using @Transactional 注解即可.

6.1 basictransactionconfiguration

package com.example.demo.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {
    
    @Transactional
    public void createOrder(Order order) {
        // 保存订单
        // updatelibrary存
        // 发送notification
        // such as果任何一步失败, 所 has operation都会rollback
    }
}

6.2 transactionpropertyconfiguration

@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.READ_COMMITTED,
    timeout = 30,
    readOnly = false,
    rollbackFor = Exception.class
)
public void complexBusinessLogic() {
    //  complex 业务逻辑
}

6.3 transaction传播behavior

  • REQUIRED - such as果当 before 存 in transaction, 则加入该transaction; 否则, creation一个 new transaction
  • SUPPORTS - such as果当 before 存 in transaction, 则加入该transaction; 否则, 以非transaction方式执行
  • MANDATORY - such as果当 before 存 in transaction, 则加入该transaction; 否则, 抛出exception
  • REQUIRES_NEW - creation一个 new transaction, such as果当 before 存 in transaction, 则挂起该transaction
  • NOT_SUPPORTED - 以非transaction方式执行, such as果当 before 存 in transaction, 则挂起该transaction
  • NEVER - 以非transaction方式执行, such as果当 before 存 in transaction, 则抛出exception
  • NESTED - such as果当 before 存 in transaction, 则 in 嵌套transaction in 执行; 否则, creation一个 new transaction

7. datavalidation

Spring Boot 集成了 Hibernate Validator, 可以方便地fordatavalidation.

7.1 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

7.2 in 实体classin添加validation注解

package com.example.demo.entity;

import jakarta.validation.constraints.*;
import lombok.Data;

@Data
public class User {
    private Long id;
    
    @NotBlank(message = "user名不能 for 空")
    @Size(min = 3, max = 20, message = "user名 long 度必须 in  3-20 个字符之间")
    private String username;
    
    @NotBlank(message = "邮箱不能 for 空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @NotBlank(message = "password不能 for 空")
    @Size(min = 6, message = "password long 度不能 few 于 6 个字符")
    private String password;
    
    @NotBlank(message = "姓名不能 for 空")
    private String fullName;
    
    private boolean active = true;
}

7.3 in Controller inusingvalidation

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @PostMapping
    public ResponseEntity<User> createUser(@Validated @RequestBody User user) {
        User createdUser = userService.createUser(user);
        return ResponseEntity.ok(createdUser);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(
            @PathVariable Long id,
            @Validated @RequestBody User user) {
        User updatedUser = userService.updateUser(id, user);
        return ResponseEntity.ok(updatedUser);
    }
}

7.4 全局exceptionprocessing

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.Exceptionprocessingr;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionprocessingr {
    
    @Exceptionprocessingr(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}

8. datalibrarymigration

using Flyway or Liquibase fordatalibraryversionmanagement and migration.

8.1 Flyway configuration

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>
# Flyway configuration
spring.flyway.url=jdbc:mysql://localhost:3306/mydb
spring.flyway.user=root
spring.flyway.password=password
spring.flyway.baseline-on-migrate=true

8.2 creationmigration脚本

in src/main/resources/db/migration Table of Contents under creation SQL migration脚本:

-- V1__Create_users_table.sql
CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(20) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password VARCHAR(100) NOT NULL,
    full_name VARCHAR(100) NOT NULL,
    active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

8.3 runmigration

启动 Spring Boot application时, Flyway 会自动执行未执行 migration脚本.