SpringDataJPA笔记
第一章 为什么要学习jpa?
-
第一,大势所趋,大厂必备技能。
-
第二,提升开发效率。
-
第三,提高技术水平。
-
第四,求职加分项。
第二章 初识jpa
快速启动一个springboot项目集成springDataJPA。
1.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.5.0</version>
</dependency>
1.2 创建实体类
@Data
@Entity
public class User {
/**
* This field was generated by MyBatis Generator.
* This field corresponds to the database column user.id
*
* @mbg.generated Sun May 23 12:25:53 CST 2021
*/
@TableId(type = IdType.AUTO)
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Integer id;
/**
* This field was generated by MyBatis Generator.
* This field corresponds to the database column user.username
*
* @mbg.generated Sun May 23 12:25:53 CST 2021
*/
private String username;
/**
* This field was generated by MyBatis Generator.
* This field corresponds to the database column user.password
*
* @mbg.generated Sun May 23 12:25:53 CST 2021
*/
private String password;
/**
* This field was generated by MyBatis Generator.
* This field corresponds to the database column user.salt
*
* @mbg.generated Sun May 23 12:25:53 CST 2021
*/
private String salt;
}
1.3 创建UserRepository
~~~java
public interface UserRepository extends JpaRepository<User, Integer> {
}
~~~
-
Spring Data Common
2.1 是什么?
它定义了一套操作数据源的接口。CrudRepository、PageingAndSortingRepository、JpaRepository…
2.2 为什么能操作数据库?
Resposiory 是 Spring Data 里面进行数据库操作顶级的抽象接口,里面什么方法都没有,但是如果任何接口继承它,就能得到一个 Repository,还可以实现 JPA 的一些默认实现方法。Spring 利用 Respository 作为 DAO 操作的 Type,以及利用 Java 动态代理机制就可以实现很多功能,比如为什么接口就能实现 DB 的相关操作?这就是 Spring 框架的高明之处。
Spring 在做动态代理的时候,只要是它的子类或者实现类,再利用 T 类以及 T 类的 主键 ID 类型作为泛型的类型参数,就可以来标记出来、并捕获到要使用的实体类型,就能帮助使用者进行数据库操作。
2.3 Repository关系层次?
2.4 7 个大 Repository 接口:
- Repository(org.springframework.data.repository),没有暴露任何方法;
- CrudRepository(org.springframework.data.repository),简单的 Curd 方法;
- PagingAndSortingRepository(org.springframework.data.repository),带分页和排序的方法;
- QueryByExampleExecutor(org.springframework.data.repository.query),简单 Example 查询;
- JpaRepository(org.springframework.data.jpa.repository),JPA 的扩展方法;
- JpaSpecificationExecutor(org.springframework.data.jpa.repository),JpaSpecification 扩展查询;
- QueryDslPredicateExecutor(org.springframework.data.querydsl),QueryDsl 的封装。
根据方法名来定义查询条件。
/**
* 根据用户名来查询
*
* @param userName
* @return
*/
List<User> findByUsername(String userName);
3.1 Defining Query Method(DQM)语法
![img](https://s0.lgstatic.com/i/image/M00/51/31/Ciqc1F9ki9CAPfoLAAMOpmuNPDY563.png)
3.2 分页查询 排序查询
~~~java
//查询user里面的lastname=jk的第一页,每页大小是20条;并会返回一共有多少页的信息
Page<User> users = userRepository.findByLastname("jk",PageRequest.of(1, 20));
//查询user里面的lastname=jk的第一页的20条数据,不知道一共多少条
Slice<User> users = userRepository.findByLastname("jk",PageRequest.of(1, 20));
//查询出来所有的user里面的lastname=jk的User数据,并按照name正序返回List
List<User> users = userRepository.findByLastname("jk",new Sort(Sort.Direction.ASC, "name"))
//按照createdAt倒序,查询前一百条User数据
List<User> users = userRepository.findByLastname("jk",PageRequest.of(0, 100, Sort.Direction.DESC, "createdAt"));
~~~
具体调用:
//查询user里面的lastname=jk的第一页,每页大小是20条;并会返回一共有多少页的信息
Page<User> users = userRepository.findByLastname("jk",PageRequest.of(1, 20));
//查询user里面的lastname=jk的第一页的20条数据,不知道一共多少条
Slice<User> users = userRepository.findByLastname("jk",PageRequest.of(1, 20));
//查询出来所有的user里面的lastname=jk的User数据,并按照name正序返回List
List<User> users = userRepository.findByLastname("jk",Sort.by(Sort.Direction.ASC, "name"))
//按照createdAt倒序,查询前一百条User数据
List<User> users = userRepository.findByLastname("jk",PageRequest.of(0, 100, Sort.Direction.DESC, "createdAt"));
5.1 简单示例
~~~java
/**
* 测试@Query方法
*
* @param username
* @return
*/
@Query("from User where username=:username")
List<User> queryUserFromUsername(@Param("username") String username);
~~~
5.2 @Query里边的方法
package org.springframework.data.jpa.repository;
public @interface Query {
/**
* 指定JPQL的查询语句。(nativeQuery=true的时候,是原生的Sql语句)
*/
String value() default "";
/**
* 指定count的JPQL语句,如果不指定将根据query自动生成。
* (如果当nativeQuery=true的时候,指的是原生的Sql语句)
*/
String countQuery() default "";
/**
* 根据哪个字段来count,一般默认即可。
*/
String countProjection() default "";
/**
* 默认是false,表示value里面是不是原生的sql语句
*/
boolean nativeQuery() default false;
/**
* 可以指定一个query的名字,必须唯一的。
* 如果不指定,默认的生成规则是:
* {$domainClass}.${queryMethodName}
*/
String name() default "";
/*
* 可以指定一个count的query的名字,必须唯一的。
* 如果不指定,默认的生成规则是:
* {$domainClass}.${queryMethodName}.count
*/
String countName() default "";
}
5.3 动态sql
![img](https://s0.lgstatic.com/i/image/M00/56/57/CgqCHl9rKauAfQmmAAHMquqHBVg909.png)
/**
* 测试@Query多条件查询
*
* @param user
* @return
*/
@Query("select u from User u where (u.username is null or u.username = '' or u.username = :#{#user.username}) and (u.password is null or u.password = :#{#user.password})")
List<User> queryManyWrapper(@Param("user") User user);
SELECT
user0_.id AS id1_0_,
user0_.PASSWORD AS password2_0_,
user0_.salt AS salt3_0_,
user0_.username AS username4_0_
FROM
USER user0_
WHERE
(
user0_.username IS NULL
OR user0_.username = ''
OR user0_.username =?)
AND (
user0_.PASSWORD IS NULL
OR user0_.PASSWORD =?)
6.1 字段注解
注解要么全部加载属性上,要么全部加载get/set方法上。
@Column(length = 20, nullable = false)
public String getUserName(){
return userName;
}
@Column(length = 20, nullable = false)
private String userName;
6.2 注意事项
-
只要是在 @Entity 的实体里面被注解标注的字段,都会被映射到数据库中,除了使用 @Transient 注解的字段之外。
-
实体里面必须要有一个主键,主键标示的字段可以是单个字段,也可以是复合主键字段。
6.3 主键注解
public enum GenerationType {
//通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。
TABLE,
//通过序列产生主键,通过 @SequenceGenerator 注解指定序列名, MySql 不支持这种方式;
SEQUENCE,
//采用数据库ID自增长, 一般用于mysql数据库
IDENTITY,
//JPA 自动选择合适的策略,是默认选项;
AUTO
}
6.4常用注解
- @Entity 用于定义对象将会成为被 JPA 管理的实体,必填,将字段映射到指定的数据库表中,使用起来很简单,直接用在实体类上面即可,通过源码表达的语法如下:
- @Table 用于指定数据库的表名,表示此实体对应的数据库里面的表名,非必填,默认表名和 entity 名字一样。
- @Access 用于指定 entity 里面的注解是写在字段上面,还是 get/set 方法上面生效,非必填。在默认不填写的情况下,当实体里面的第一个注解出现在字段上或者 get/set 方法上面,就以第一次出现的方式为准。
- @Id 定义属性为数据库的主键,一个实体里面必须有一个主键,但不一定是这个注解,可以和 @GeneratedValue 配合使用或成对出现。
- @Enumerated 这个注解很好用,因为它对 enum 提供了下标和 name 两种方式,用法直接映射在 enum 枚举类型的字段上。请看下面源码。
- @Basic 表示属性是到数据库表的字段的映射。如果实体的字段上没有任何注解,默认即为 @Basic。也就是说默认所有的字段肯定是和数据库进行映射的,并且默认为 Eager 类型。
- @Transient 表示该属性并非一个到数据库表的字段的映射,表示非持久化属性。JPA 映射数据库的时候忽略它,与 @Basic 有相反的作用。也就是每个字段上面 @Transient 和 @Basic 必须二选一,而什么都不指定的话,默认是 @Basic。
- @Column 定义该属性对应数据库中的列名。
public @interface Column {
//数据库中的表的列名;可选,如果不填写认为字段名和实体属性名一样。
String name() default "";
//是否唯一。默认flase,可选。
boolean unique() default false;
//数据字段是否允许空。可选,默认true。
boolean nullable() default true;
//执行insert操作的时候是否包含此字段,默认,true,可选。
boolean insertable() default true;
//执行update的时候是否包含此字段,默认,true,可选。
boolean updatable() default true;
//表示该字段在数据库中的实际类型。
String columnDefinition() default "";
//数据库字段的长度,可选,默认255
int length() default 255;
}
- @Temporal 用来设置 Date 类型的属性映射到对应精度的字段,存在以下三种情况:
- @Temporal(TemporalType.DATE)映射为日期 // date (只有日期)
- @Temporal(TemporalType.TIME)映射为日期 // time (只有时间)
- @Temporal(TemporalType.TIMESTAMP)映射为日期 // date time (日期+时间)
6.5. 联合主键
-
如何通过@IdClass做联合主键?
~~~java package com.example.jpa.example1; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @Builder @AllArgsConstructor @NoArgsConstructor public class UserInfoID implements Serializable { private String name,telephone; } ~~~
@Entity
@Data
@Builder
@IdClass(UserInfoID.class)
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
private Integer ages;
@Id
private String name;
@Id
private String telephone;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Embeddable
public class UserInfoID implements Serializable {
private String name,telephone;
}
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
private Integer ages;
@EmbeddedId
private UserInfoID userInfoID;
@Column(unique = true)
private String uniqueNumber;
}
6.6 实体间的继承关系如何处理
关系可以分为以下几类:
-
纯粹的继承,和表没关系,对象之间的字段共享。利用注解 @MappedSuperclass,协议规定父类不能是 @Entity。
-
单表多态问题,同一张 Table,表示了不同的对象,通过一个字段来进行区分。利用
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
注解完成,只有父类有 @Table。package com.example.jpa.example1.book; import lombok.Data; import javax.persistence.*; @Entity(name="book") @Data @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name="color", discriminatorType = DiscriminatorType.STRING) public class Book { @Id @GeneratedValue(strategy= GenerationType.AUTO) private Long id; private String title; }
package com.example.jpa.example1.book; import lombok.Data; import lombok.EqualsAndHashCode; import javax.persistence.DiscriminatorValue; import javax.persistence.Entity; @Entity @Data @EqualsAndHashCode(callSuper=false) @DiscriminatorValue("blue") public class BlueBook extends Book{ private String blueMark; }
//红皮书 @Entity @DiscriminatorValue("red") @Data @EqualsAndHashCode(callSuper=false) public class RedBook extends Book { private String redMark; }
-
多表多态,每一个子类一张表,父类的表拥有所有公用字段。通过
@Inheritance(strategy = InheritanceType.JOINED)
注解完成,父类和子类都是表,有公用的字段在父表里面。 -
Object 的继承,数据库里面每一张表是分开的,相互独立不受影响。通过
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
注解完成,父类(可以是一张表,也可以不是)和子类都是表,相互之间没有关系。
实体与实体之间的关联关系一共分为四种,分别为 OneToOne、OneToMany、ManyToOne 和 ManyToMany;而实体之间的关联关系又分为双向的和单向的。
- oneToOne关联关系
public @interface OneToOne {
//表示关系目标实体,默认该注解标识的返回值的类型的类。
Class targetEntity() default void.class;
//cascade 级联操作策略,就是我们常说的级联操作
CascadeType[] cascade() default {};
//数据获取方式EAGER(立即加载)/LAZY(延迟加载)
FetchType fetch() default EAGER;
//是否允许为空,默认是可选的,也就表示可以为空;
boolean optional() default true;
//关联关系被谁维护的一方对象里面的属性名字。 双向关联的时候必填
String mappedBy() default "";
//当被标识的字段发生删除或者置空操作之后,是否同步到关联关系的一方,即进行通过删除操作,默认flase,注意与 CascadeType.REMOVE 级联删除的区别
boolean orphanRemoval() default false;
}
- mappedBy注意事项
只有关联关系的维护方才能操作两个实体之间外键的关系。被维护方即使设置了维护方属性进行存储也不会更新外键关联。
mappedBy 不能与 @JoinColumn 或者 @JoinTable 同时使用,因为没有意义,关联关系不在这里面维护。
==此外,mappedBy 的值是指另一方的实体里面属性的字段,而不是数据库字段,也不是实体的对象的名字。==也就是维护关联关系的一方属性字段名称,或者加了 @JoinColumn / @JoinTable 注解的属性字段名称。如上面的 User 例子 user 里面 mappedBy 的值,就是 UserInfo 里面的 user 字段的名字。
-
在 CascadeType 的用法中,CascadeType 的枚举值只有五个
- CascadeType.PERSIST 级联新建
- CascadeType.REMOVE 级联删除
- CascadeType.REFRESH 级联刷新
- CascadeType.MERGE 级联更新
- CascadeType.ALL 四项全选
-
orphanRemoval 属性用法
orphanRemoval 表示当关联关系被删除的时候,是否应用级联删除,默认 false。。
- @JoinCloumns & JoinColumn
public @interface JoinColumn {
//关键的字段名,默认注解上的字段名,在@OneToOne代表本表的外键字段名字;
String name() default "";
//与name相反关联对象的字段,默认主键字段
String referencedColumnName() default "";
//外键字段是否唯一
boolean unique() default false;
//外键字段是否允许为空
boolean nullable() default true;
//是否跟随一起新增
boolean insertable() default true;
//是否跟随一起更新
boolean updatable() default true;
//JPA2.1新增,外键策略
ForeignKey foreignKey() default @ForeignKey(PROVIDER_DEFAULT);
}
- ManyToOne& @OneToMany
public @interface ManyToOne {
Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default EAGER;
boolean optional() default true;
}
public @interface OneToMany {
Class targetEntity() default void.class;
//cascade 级联操作策略:(CascadeType.PERSIST、CascadeType.REMOVE、CascadeType.REFRESH、CascadeType.MERGE、CascadeType.ALL)
如果不填,默认关系表不会产生任何影响。
CascadeType[] cascade() default {};
//数据获取方式EAGER(立即加载)/LAZY(延迟加载)
FetchType fetch() default LAZY;
//关系被谁维护,单项的。注意:只有关系维护方才能操作两者的关系。
String mappedBy() default "";
//是否级联删除。和CascadeType.REMOVE的效果一样。两种配置了一个就会自动级联删除
boolean orphanRemoval() default false;
}
- jaskson注解
![Lark20201009-105051.png](https://s0.lgstatic.com/i/image/M00/5B/A6/CgqCHl9_0CiAWB2rAAL0pfxIviE487.png)
第三章 高级用法和实战
public interface QueryByExampleExecutor<T> {
//根据“实体”查询条件,查找一个对象
<S extends T> S findOne(Example<S> example);
//根据“实体”查询条件,查找一批对象
<S extends T> Iterable<S> findAll(Example<S> example);
//根据“实体”查询条件,查找一批对象,可以指定排序参数
<S extends T> Iterable<S> findAll(Example<S> example, Sort sort);
//根据“实体”查询条件,查找一批对象,可以指定排序和分页参数
<S extends T> Page<S> findAll(Example<S> example, Pageable pageable);
//根据“实体”查询条件,查找返回符合条件的对象个数
<S extends T> long count(Example<S> example);
//根据“实体”查询条件,判断是否有符合条件的对象
<S extends T> boolean exists(Example<S> example);
}
- 基本使用
@Test
public void testSpeQuery() throws JsonProcessingException {
User user = User.builder()
.username("wuhaiqing").gender(Gender.MAN)
.build();
// System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(address));
// 可以打印出来看看参数是什么
//创建匹配器,即如何使用查询条件
ExampleMatcher exampleMatcher = ExampleMatcher
.matching()
.withIgnoreNullValues()
.withIgnoreCase()
.withMatcher("username", ExampleMatcher.GenericPropertyMatchers.startsWith())
.withMatcher("gender",ExampleMatcher.GenericPropertyMatchers.startsWith());
Page<User> u = userRepository.findAll(Example.of(user, exampleMatcher), PageRequest.of(0, 2));
System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(u));
}
@Test
public void testSPE() {
// //模拟请求参数
// User userQuery = User.builder()
// .username("jack")
// .gender(Gender.MAN)
// .age("20")
// .build();
// //假设的时间范围参数
// Instant beginCreateDate = Instant.now().plus(-2, ChronoUnit.HOURS);
// Instant endCreateDate = Instant.now().plus(1, ChronoUnit.HOURS);
// //利用Specification进行查询
// Page<User> users = userRepository.findAll((Specification<User>) (root, query, cb) -> {
// List<Predicate> ps = new ArrayList<Predicate>();
// if (StringUtils.isNotBlank(userQuery.getUsername())) {
// //我们模仿一下like查询,根据name模糊查询
// ps.add(cb.like(root.get("name"), "%" + userQuery.getUsername() + "%"));
// }
// if (userQuery.getGender() != null) {
// //equal查询条件,这里需要注意,直接传递的是枚举
// ps.add(cb.equal(root.get("sex"), userQuery.getGender()));
// }
// if (userQuery.getAge() != null) {
// //greaterThan大于等于查询条件
// ps.add(cb.greaterThan(root.get("age"), userQuery.getAge()));
// }
// if (beginCreateDate != null && endCreateDate != null) {
// //根据时间区间去查询创建
// ps.add(cb.between(root.get(f"createDate"), beginCreateDate, endCreateDate));
// }
// return query.where(ps.toArray(new Predicate[ps.size()])).getRestriction();
// }, PageRequest.of(0, 2));
// System.out.println(users);
User queryWrapper = User.builder().username("wuhaiqing").password("wuhaiqing").gender(Gender.MAN).age("28").build();
//假设的时间范围参数
Instant beginCreateDate = Instant.now().plus(-2, ChronoUnit.HOURS);
Instant endCreateDate = Instant.now().plus(1, ChronoUnit.HOURS);
Page<User> all = userRepository.findAll((Specification<User>) (root, query, cb) -> {
List<Predicate> ps = Lists.newArrayList();
//模糊查询
if (StringUtils.isNotBlank(queryWrapper.getUsername())) {
ps.add(cb.like(root.get("username"), "%" + queryWrapper.getUsername() + "%"));
}
//精确匹配
if (StringUtils.isNotBlank(queryWrapper.getAge())) {
ps.add(cb.equal(root.get("age"), queryWrapper.getAge()));
}
//区间
// if (beginCreateDate != null && endCreateDate != null) {
// ps.add(cb.between(root.get("createDate"), beginCreateDate, endCreateDate));
// }
return query.where(ps.toArray(new Predicate[ps.size()])).getRestriction();
}, PageRequest.of(0, 1));
}
获得 EntityManager 的方式:通过 @PersistenceContext 注解。
@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class UserRepositoryTest {
//利用该方式获得entityManager
@PersistenceContext
private EntityManager entityManager;
@Autowired
private UserRepository userRepository;
/**
* 测试entityManager用法
*
* @throws JsonProcessingException
*/
@Test
@Rollback(false)
public void testEntityManager() throws JsonProcessingException {
//测试找到一个User对象
User user = entityManager.find(User.class,2L);
Assertions.assertEquals(user.getAddresses(),"shanghai");
//我们改变一下user的删除状态
user.setDeleted(true);
//merger方法
entityManager.merge(user);
//更新到数据库里面
entityManager.flush();
//再通过createQuery创建一个JPQL,进行查询
List<User> users = entityManager.createQuery("select u From User u where u.name=?1")
.setParameter(1,"jack")
.getResultList();
Assertions.assertTrue(users.get(0).getDeleted());
}
}
![image (5).png](https://s0.lgstatic.com/i/image/M00/62/8F/Ciqc1F-SoLyAODuaAADhS0Urg_0032.png)
- 语法注意事项。
- 回调函数都是和 EntityManager.flush 或 EntityManager.commit 在同一个线程里面执行的,只不过调用方法有先后之分,都是同步调用,所以当任何一个回调方法里面发生异常,都会触发事务进行回滚,而不会触发事务提交。
- Callbacks 注解可以放在实体里面,可以放在 super-class 里面,也可以定义在 entity 的 listener 里面,但需要注意的是:放在实体(或者 super-class)里面的方法,签名格式为“void ()”,即没有参数,方法里面操作的是 this 对象自己;放在实体的 EntityListener 里面的方法签名格式为“void (Object)”,也就是方法可以有参数,参数是代表用来接收回调方法的实体。
- 使上述注解生效的回调方法可以是 public、private、protected、friendly 类型的,但是不能是 static 和 finnal 类型的方法。
@PreUpdate
public void preUpdate() {
System.out.println("preUpdate::" + this.toString());
}
@PostUpdate
public void postUpdate() {
System.out.println("postUpdate::" + this.toString());
}
@PreRemove
public void preRemove() {
System.out.println("preRemove::" + this.toString());
}
@PostRemove
public void postRemove() {
System.out.println("postRemove::" + this.toString());
}
乐观锁在实际开发过程中很常用,它没有加锁、没有阻塞,在多线程环境以及高并发的情况下 CPU 的利用率是最高的,吞吐量也是最大的。jpa中使用@version实现乐观锁。
- 悲观锁的实现
![image-20210528164545123](/Users/wuhaiqing/Library/Application%20Support/typora-user-images/image-20210528164545123.png)
SELECT
user0_.id AS id1_1_,
user0_.age AS age2_1_,
user0_.create_date AS create_d3_1_,
user0_.gender AS gender4_1_,
user0_.PASSWORD AS password5_1_,
user0_.salt AS salt6_1_,
user0_.username AS username7_1_,
user0_.version AS version8_1_
FROM
USER user0_
WHERE
user0_.username =? FOR UPDATE
如何搞清楚事务、连接池的关系?正确配置是怎样的?
-
acid分别指什么?
- 原子性: 是指一个事务(Transaction)中的所有操作,要么全部完成,要么全部回滚,而不会有中间某个数据单独更新的操作。事务在执行过程中一旦发生错误,会被回滚(Rollback)到此次事务开始之前的状态,就像这个事务从来没有执行过一样。
- 原子性: 是指一个事务(Transaction)中的所有操作,要么全部完成,要么全部回滚,而不会有中间某个数据单独更新的操作。事务在执行过程中一旦发生错误,会被回滚(Rollback)到此次事务开始之前的状态,就像这个事务从来没有执行过一样。
- 持久性: 是指事务处理结束后,对数据的修改进行了持久化的永久保存,即便系统故障也不会丢失,其实就是保存到硬盘。
- 隔离性: 是指数据库允许多个连接,同时并发多个事务,又对同一个数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时,由于交叉执行而导致数据不一致的现象。而 MySQL 里面就是我们经常说的事务的四种隔离级别,即读未提交(Read Uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
-
四种 MySQL 事务的隔离级别
- Read Uncommitted(读取未提交内容):此隔离级别,表示所有正在进行的事务都可以看到其他未提交事务的执行结果。不同的事务之间读取到其他事务中未提交的数据,通常这种情况也被称之为脏读(Dirty Read),会造成数据的逻辑处理错误,也就是我们在多线程里面经常说的数据不安全了。在业务开发中,几乎很少见到使用的,因为它的性能也不比其他级别要好多少。
- Read Committed(读取提交内容): 此隔离级别是指,在一个事务相同的两次查询可能产生的结果会不一样,也就是第二次查询能读取到其他事务已经提交的最新数据。也就是我们常说的不可重复读(Nonrepeatable Read)的事务隔离级别。因为同一事务的其他实例在该实例处理期间,可能会对其他事务进行新的 commit,所以在同一个事务中的同一 select 上,多次执行可能返回不同结果。这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的隔离级别)。
- Repeatable Read(可重读): 这是 MySQL 的默认事务隔离级别,它确保同一个事务多次查询相同的数据,能读到相同的数据。即使多个事务的修改已经 commit,本事务如果没有结束,永远读到的是相同数据,要注意它与Read Committed 的隔离级别的区别,是正好相反的。这会导致另一个棘手的问题:幻读 (Phantom Read),即读到的数据可能不是最新的。这个是最常见的,我们举个例子来说明。
- Serializable(可串行化):这是最高的隔离级别,它保证了每个事务是串行执行的,即强制事务排序,所有事务之间不可能产生冲突,从而解决幻读问题。如果配置在这个级别的事务,处理时间比较长,并发比较大的时候,就会导致大量的 db 连接超时现象和锁竞争,从而降低了数据处理的吞吐量。也就是这个性能比较低,所以除了某些财务系统之外,用的人不是特别多。
-
MySQL 事务与连接的关系
- 事务必须在同一个连接里面的,离开连接没有事务可言;
- MySQL 数据库默认 autocommit=1,即每一条 SQL 执行完自动提交事务;
- 数据库里面的每一条 SQL 执行的时候必须有事务环境;
- MySQL 创建连接的时候默认开启事务,关闭连接的时候如果存在事务没有 commit 的情况,则自动执行 rollback 操作;
- 不同的 connect 之间的事务是相互隔离的。
-
MySQL 事务的两种操作方式
-
第一种:用 BEGIN、ROLLBACK、COMMIT 来实现。
- BEGIN开始一个事务
- ROLLBACK事务回滚
- COMMIT事务确认
-
第二种:直接用 SET 来改变 MySQL 的自动提交模式。
- SET AUTOCOMMIT=0禁止自动提交
- SET AUTOCOMMIT=1开启自动提交
-
-
@transaction参数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EjiyKkml-1623053169267)(https://s0.lgstatic.com/i/image/M00/6E/36/Ciqc1F-yC3-AP_fhAArdN5coRWQ007.png)]
-
spring事务的传播行为
- REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这个值是默认的。
- SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
- NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
- NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED。
第三章 原理与问题排查
- 常用配置参数
# 可以配置JPA的实现者的原始属性的配置,如:这里我们用的JPA的实现者是hibernate
# 那么hibernate里面的一些属性设置就可以通过如下方式实现,具体properties里面有哪些,本讲会详细介绍,我们先知道这里可以设置即可
spring.jpa.properties.hibernate.hbm2ddl.auto=none
#hibernate的persistence.xml文件有哪些,目前已经不推荐使用
#spring.jpa.mapping-resources=
# 指定数据源的类型,如果不指定,Spring Boot加载Datasource的时候会根据URL的协议自己判断
# 如:spring.datasource.url=jdbc:mysql://localhost:3306/test 上面可以明确知道是mysql数据源,所以这个可以不需要指定;
# 应用场景,当我们通过代理的方式,可能通过datasource.url没办法判断数据源类型的时候,可以通过如下方式指定,可选的值有:DB2,H2,HSQL,INFORMIX,MYSQL,ORACLE,POSTGRESQL,SQL_SERVER,SYBASE)
spring.jpa.database=mysql
# 是否在启动阶段根据实体初始化数据库的schema,默认false,当我们用内存数据库做测试的时候可以打开,很有用
spring.jpa.generate-ddl=false
# 和spring.jpa.database用法差不多,指定数据库的平台,默认会自己发现;一般不需要指定,database-platform指定的必须是org.hibernate.dialect.Dialect的子类,如mysql默认是用下面的platform
spring.jpa.database-platform=org.hibernate.dialect.MySQLInnoDBDialect
# 是否在view层打开session,默认是true,其实大部分场景不需要打开,我们可以设置成false,
# 22课时我们再详细讲解
spring.jpa.open-in-view=false
# 是否显示sql,当执行JPA的数据库操作的时候,默认是false,在本地开发的时候我们可以把这个打开,有助于分析sql是不是我们预期的
# 在生产环境的时候建议给这个设置成false,改由logging.level.org.hibernate.SQL=DEBUG代替,这样的话日志默认是基于logback输出的
# 而不是直接打印到控制台的,有利于增加traceid和线程ID等信息,便于分析
spring.jpa.show-sql=true
- 日志常用参数
### 日志级别的灵活运用
## hibernate相关
# 显示sql的执行日志,如果开了这个,show_sql就可以不用了
logging.level.org.hibernate.SQL=debug
# hibernate id的生成日志
logging.level.org.hibernate.id=debug
# hibernate所有的操作都是PreparedStatement,把sql的执行参数显示出来
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
# sql执行完提取的返回值
logging.level.org.hibernate.type.descriptor.sql=trace
# 请求参数
logging.level.org.hibernate.type=debug
# 缓存相关
logging.level.org.hibernate.cache=debug
# 统计hibernate的执行状态
logging.level.org.hibernate.stat=debug
# 查看所有的缓存操作
logging.level.org.hibernate.event.internal=trace
logging.level.org.springframework.cache=trace
# hibernate 的监控指标日志
logging.level.org.hibernate.engine.internal.StatisticalLoggingSessionEventListener=DEBUG
### 连接池的相关日志
## hikari连接池的状态日志,以及连接池是否完好 #连接池的日志效果:HikariCPPool - Pool stats (total=20, active=0, idle=20, waiting=0)
logging.level.com.zaxxer.hikari=TRACE
#开启 debug可以看到 AvailableSettings里面的默认配置的值都有哪些,会输出类似下面的日志格式
# org.hibernate.cfg.Settings : Statistics: enabled
# org.hibernate.cfg.Settings : Default batch fetch size: -1
logging.level.org.hibernate.cfg=debug
#hikari数据的配置项日志
logging.level.com.zaxxer.hikari.HikariConfig=TRACE
### 查看事务相关的日志,事务获取,释放日志
logging.level.org.springframework.orm.jpa=DEBUG
logging.level.org.springframework.transaction=TRACE
logging.level.org.hibernate.engine.transaction.internal.TransactionImpl=DEBUG
### 分析connect 以及 orm和 data的处理过程更全的日志
logging.level.org.springframework.data=trace
logging.level.org.springframework.orm=trace