Spring-boot 常见的使用数据库的方式有两种,一种是 JPA, 一种是 Mybatis
JPA
引入相关依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></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 有以下几个参数
none不对数据库做任何的改动update更新数据库,也就是说会根据给定的 @Entity 对象的改变而改变对应的数据库字段。如果这个时候,数据库没有表,那么就会自动新建表。create创建数据库,但是关闭的时候,不删除数据库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 的信息了!
一对多
一对多的关系用了两个关键词
- OneToMany
- 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(数据库连接会话)的生命周期没有结束,否则是无法抽取到数据的。
