Spring-boot 常见的使用数据库的方式有两种,一种是 JPA, 一种是 Mybatis

JPA

引入相关依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-jpa</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>mysql</groupId>
  7. <artifactId>mysql-connector-java</artifactId>
  8. <scope>runtime</scope>
  9. </dependency>

配置

spring:
  datasource:
    url: jdbc:mysql://<ip>:3307/jpa
    username: root
    password: xxx
  hikari:
    maximum-pool-size: 20
    minimum-idle: 5
  jpa:
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    show-sql: true
    hibernate:
      ddl-auto: update

根据 spring-boot 文档,这里 ddl-auto 有以下几个参数

  1. none 不对数据库做任何的改动
  2. update 更新数据库,也就是说会根据给定的 @Entity 对象的改变而改变对应的数据库字段。如果这个时候,数据库没有表,那么就会自动新建表。
  3. create 创建数据库,但是关闭的时候,不删除数据库
  4. create-drop 创建数据库,但是应用关闭的时候,自动删除数据库

在一些内置的数据库中,比如 H2 数据库,其默认的值是 create-drop 而在 mysql 这样的数据库中,其默认值为 none 。这里比较常设置的值应该为 update 这个必须要设置,不设置的话,就不会有对应的数据库的表生成。

创建 Entity 模块

@Data
@Entity
@Table(name = "Customer")
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column(length = 50, nullable = false, columnDefinition = "unique comment '姓名'" )
    private String name;

    @Column(length = 20)
    private String email;

}

@Data 是 lombok 的一个方法,用于生成 get / set 方法,以及toString / equals 等等
@Entity 是jpa 的一个注解,用于告诉数据库,这是映射表的对象
@Table 是 JPA 的一个注解,用于声明表名
@Id 是 JPA 的一个注解,表示表字段常见的 Id 属性
@GeneratedValue(strategy = GenerationType.AUTO) 是 JPA 的一个注解,用于表示 ID 字段自增长
@Column(length=50, nullable=false, columnDefinition="unique comment '姓名' ") :是 JPA 的一个注解,长度是 50, 如果没有指定,则为默认的 varchar(255), nullable 表示是否为空

测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomerRepoTest {
    @Autowired
    private CustomerRepository customerRepository;

    @Before
    public void before(){
        Customer customer = new Customer();
        customer.setName("test");
        customer.setEmail("test@ehi.com");
        customerRepository.save(customer);

        customer = new Customer();
        customer.setName("test2");
        customerRepository.save(customer);
    }
    @Test
    public void testQuery(){
        Customer customer = customerRepository.findAllById(1);
        assert(customer.getName().equals("test"));
    }
    @Test
    public void testAddUserFailed(){
        Customer customer = new Customer();
        customer.setEmail("email");
        try{
            customerRepository.save(customer);
        }catch (Exception e){
            assert(e instanceof DataIntegrityViolationException);
        }
    }
}

数据库关系

数据库中的关系分为单向关联和双向关联,单项关联就是通过管理关系的表查询到另一个表的内容,而另一个表是查询不到这个表的内容的。
双向关联指的是,两个表都能互相查询得到内容

一对一

首先我们假设有两张表,People 表和 address 表。 单向关联关系在 people 表里面维护
people 表

@Data
@Entity
@Table( name = "people")
public class People {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column(length = 50)
    private String name;


    @OneToOne(cascade = {CascadeType.ALL},fetch = FetchType.LAZY)
    // 新增一个字段 address_id, 指向的是 address表的 id
    // 这里的 referencedColumnName 也可以是 address表的其他字段
    @JoinColumn(name = "address_id",referencedColumnName = "id")
    private Address address;
}

address 表

@Data
@Entity
@Table(name = "address")
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    @Column(length = 50)
    private String name;
}

最后我们写一个 testcase

@RunWith(SpringRunner.class)
@SpringBootTest
public class PeopleAndAddressTest {
    @Autowired
    PeopleRepository peopleRepository;

    @Test
    public void testAddPeople(){
        People p = new People();
        p.setName("john");

        Address address = new Address();
        address.setName("fugong....");

        p.setAddress(address);

        peopleRepository.save(p);
    }
}

然后是双向关联操作。 双向管理意味着双方都知道对方的存在,而不只是管理关系的表知道。我们在 address 表增加如下内容

package com.example.demo.dao;

import lombok.Data;

import javax.persistence.*;

/**
 * @DATA: 2020/11/16
 */

@Data
@Entity
@Table(name = "address")
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(length = 50)
    private String name;

    @OneToOne(mappedBy = "address", cascade = {CascadeType.ALL}, fetch = FetchType.LAZY,optional = false)
    private People people;

}

然后看一下测试用例

    @Test
    public void testQuery(){
        Address address = addressRepository.findAllById(38);
        People p = address.getPeople();
        assert(p.getName().equals("john"));

    }

双向关联,也能从 address 表中得出 People 的信息了!

一对多

一对多的关系用了两个关键词

  1. OneToMany
  2. ManyToOne

接下来我们创建两个类, article 类和 comment 类,一篇文章有多个评论,但是一个评论只能属于一篇文章

@Data
@Entity
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column
    private String title;

    @Column
    private String content;

    @OneToMany(mappedBy = "article",cascade = {CascadeType.ALL},fetch = FetchType.LAZY)
    private List<Comment> comment;
}

comment 类

@Data
@Entity
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column
    private String content;

    @ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH},optional = false)
    @JoinColumn(name="article_id")
    private Article article;

}

测试

@Test
    public void testArticleAdd(){
        Article article = new Article();
        article.setTitle("test");
        article.setContent("this is article");
        articleRepository.save(article);

        Comment comment = new Comment();
        comment.setContent("comment1");
        comment.setArticle(article);
        commentRepository.save(comment);
    }

注意,因为是一对多,所以在 的那一放,需要保留 的这边的 ID,所以我们需要用 的那一端来添加 的id,并且要保证 的那一端已经添加到了数据库中。

同时需要注意:
假设文章是One,评论是Many。
cascade = CascadeType.ALL 只能写在 One 端,只有One端改变Many端,不准Many端改变One端。
特别是删除,因为 ALL 里包括更新,删除。
如果删除一条评论,就把文章删了,这是不合理的现象。

多对多

假设一个user 有多个 role,一个 role 也对应多个 user

@Getter
@Setter
@Table(name = "user")
@Entity
public class UserDAO {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private long id;

    @Column(name = "name")
    private String name;


    @ManyToMany
    @JoinTable(
            name = "user_role",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private List<Role> roleList;
}

然后是 role

@Data
@Entity
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String name;
    @ManyToMany(mappedBy = "roleList")
    private List<UserDAO> userDAO;
}

测试用例

    @Test
    public void testRole(){
        UserDAO userDAO = new UserDAO();
        userDAO.setName("test2");
        userRepository.save(userDAO);
        Role role = new Role();
        role.setName("admin2");
        roleRepository.save(role);

         List<Role> list = new ArrayList<>();
        list.add(role);
        userDAO.setRoleList(list);
        userRepository.save(userDAO);

        // 失败
        //List<UserDAO> list = new ArrayList<>();
        //list.add(userDAO);
        //role.setUserDAO(list);
        //roleRepository.save(role);
        // 在  mappedBy 的那一端,不能更新数据
    }

CascadeType 类别

CascadeType包含的类别 级联:给当前设置的实体操作另一个实体的权限
CascadeType.ALL 级联所有操作
CascadeType.PERSIST 级联持久化(保存)操作
CascadeType.MERGE 级联更新(合并)操作
CascadeType.REMOVE 级联删除操作
CascadeType.REFRESH 级联刷新操作
CascadeType.DETACH 级联分离操作,如果你要删除一个实体,但是它有外键无法删除,这个级联权限会撤销所有相关的外键关联。

分页和排序

分页

@Repository
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
    Page<Employee> findAll(Pageable pageable);
    Page<Employee> findByFirstName(String firstName, Pageable pageable);
    Slice<Employee> findByFirstNameAndLastName(String firstName, String lastName, Pageable pageable);
}

排序

Spring Data JPA提供了一个 Sort 对象以提供排序机制。我们来看看排序方式。

employeeRepository.findAll(Sort.by("fistName"));
employeeRepository.findAll(Sort.by("fistName").ascending().and(Sort.by("lastName").descending());

参考

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference
https://spring.io/guides/gs/accessing-data-jpa/
https://zhuanlan.zhihu.com/p/57545677
https://blog.csdn.net/johnf_nash/article/details/80642581
https://blog.csdn.net/qq330983778/article/details/100026196
https://blog.csdn.net/pan_junbiao/article/details/105239426

附录

JPA的常用注解

注解 说明
@Entity 声明类为实体。
@Table 声明表名,@Entity和@Table注解一般块使用,如果表名和实体类名相同,那么@Table可以省略。
@Basic 指定非约束明确的各个字段。
@Embedded 用于注释属性,表示该属性的类是嵌入类( @embeddable 用于注释Java类的,表示类是嵌入类)。
@ld 指定的类的属性,一个表中的主键。
@GeneratedValue 指定如何标识属性可以被初始化,如@GeneratedValue(strategy = GenerationType.SEQUENCE,generator = “repair_seq”):表示主键生成策略是sequence,还有Auto、
ldentity、 Native 等。
@Transient 表示该属性并非一个数据库表的字段的映射,ORM框架将忽略该属性。如果一个属性并非数据库表的字段映射,就务必将其标示为@Transient,即它是不持久的,为虚拟字段。
@Column 指定持久属性,即字段名。如果字段名与列名相同,则可以省略。使用方法如:
@Column(length=11,name=”phone”,nullable=false,columnDefinition = “varchar(11) unique comment’电话号码’”)
@SequenceGenerator 指定在 @SequenceGenerator 注解中指定的属性的值。它创建一个序列。
@TableGenerator 在数据库生成一张表来管理主键生成策略。
@AccessType 这种类型的注释用于设置访问类型。如果设置@AccessType(FIELD),则可以直接访问变量,并且不需要使用Gtter和Stter方法,但必须为public属性。如果设置@AccessType(PROPERTY),则通过Getter和Setter方法访问Entity的变量。
@UniqueConstraint 指定的字段和用于主要或辅助表的唯一约束。
@ColumnResult 可以参考使用select子句的SQL查询中的列名。
@NamedQueries 指定命名查询的列表。
@NamedQuery 指定使用静态名称的查询。
@Basic 指定实体属性的加载方式,如@Basic(fetch=FetchType.LAZY)。
@Jsonlgnore 作用是JSON序列化时将Java Bean中的一些属性忽略掉,序列化和反序列化都受影响。

映射关系的注解

注解 说明
@JoinColumn 指定一个实体组织或实体集合。用在“多对一”和“一对多”的关联中。
@OneToOne 定义表之间“一对一”的关系。
@OneToMany 定义表之间“一对多”的关系。
@ManyToOne 定义表之间“多对一”的关系。
@ManyToMany 定义表之间“多对多”的关系。

映射关系的属性

属性名 说明
targetEntity 表示默认关联的实体类型,默认为当前标注的实体类。
cascade 表示与此实体一对一关联的实体的级联样式类型,以及当对实体进行操作时的策略。在定义关系时经常会涉及是否定义Cascade(级联处理)属性,如果担心级联处理容易造成负面影响,则可以不定义。它的类型包括CascadeType.PERSIST(级联新建)、CascadeType.REMOVE (级联删除)、CascadeType.REFRESH(级联刷新)、CascadeType.MERGE(级联更新)、CascadeType.ALL (级联新建、更新、删除、刷新)。
fetch 该实体的加载方式,包含 LAZY 和 EAGER。
optional 表示关联的实体是否能够存在null值。默认为true,表示可以存在null值。如果为false,则要同时配合使用@JoinColumn标记。
mappedBy 双向关联实体时使用,标注在不保存关系的实体中。
JoinColumn 关联指定列。该属性值可接收多个@JoinColumn。用于配置连接表中外键列的信息。@JoinColumn配置的外键列参照当前实体对应表的主键列。
JoinTable 两张表通过中间的关联表建立联系时使用,即多对多关系。
PrimaryKeyJoinColumn 主键关联。在关联的两个实体中直接使用注解@PrimaryKeyJoinColumn注释

懒加载和实时加载

懒加载LAZY和实时加载EAGER的目的是,实现关联数据的选择性加载。 懒加载是在属性被引用时才生成查询语句,抽取相关联数据。 实时加载则是执行完主查询后,不管是否被引用,都会马上执行后续的关联数据查询。 使用懒加载来调用关联数据,必须要保证主查询的Session(数据库连接会话)的生命周期没有结束,否则是无法抽取到数据的。