当前位置: 首页 > article >正文

Spring Data JPA(三) 原理分析

Spring Data JPA(三) 原理分析

1、方法命名规则解析

Spring Data JPA 中定义在 Repository 的接口方法在执行时会被 QueryExecuterMethodInterceptor 方法拦截器拦截,并根据QueryLookupStrategy 确定执行策略:

  • 解析方法带有@Query注解:
    • nativeQuery = true:即按照原生SQL,返回 NativeJpaQuery;
    • nativeQuery = false:即按照JPQL,返回 SimpleJpaQuery;
  • 解析方法不带@Query注解:即按照方法命名规则,返回 PartTreeJpaQuery;

PartTreeJpaQuery 用于对基于命名规则定义的方法进行解析和处理,包括方法名称解析、参数校验与绑定、属性映射、查询构建等;而其中方法名称的解析则是由 PartTree 来完成的,其对名称中的执行关键字(findBy、deleteBy等)、实体属性、逻辑连接词(And、Or)、谓词关键字(In、Like等)进行了拆分和保存,用于后续执行语句的构建。我们先来看一下 PartTree 的构造方法与静态属性源码:

public class PartTree implements Streamable<OrPart> {

	private static final String KEYWORD_TEMPLATE = "(%s)(?=(\\p{Lu}|\\P{InBASIC_LATIN}))";
	private static final String QUERY_PATTERN = "find|read|get|query|search|stream";
	private static final String COUNT_PATTERN = "count";
	private static final String EXISTS_PATTERN = "exists";
	private static final String DELETE_PATTERN = "delete|remove";
    // 预定义的执行关键词匹配正则表达式(方法必须以执行关键字为开始)
    // ^(find|read|get|query|search|stream|count|exists|delete|remove)((\p{Lu}.*?))??By
	private static final Pattern PREFIX_TEMPLATE = Pattern.compile( //
			"^(" + QUERY_PATTERN + "|" + COUNT_PATTERN + "|" + EXISTS_PATTERN + "|" + DELETE_PATTERN + ")((\\p{Lu}.*?))??By");

	/**
	 * The subject, for example "findDistinctUserByIdAndNameOrEmailAndPhoneOrderByAge" would have the subject "DistinctUser".
	 */
	private final Subject subject;

	/**
	 * The predicate, for example "findDistinctUserByIdAndNameOrderByAge" would have the predicate "IdAndNameOrEmailAndPhoneOrderByAge.
	 */
	private final Predicate predicate;

    /**
	 * @param source: methodName, Repository 中定义的方法名称
	 * @param domainClass: entity, Repository<T, ID>中的T
	 */
	public PartTree(String source, Class<?> domainClass) {

		Assert.notNull(source, "Source must not be null");
		Assert.notNull(domainClass, "Domain class must not be null");
        
		// ^(find|read|get|query|search|stream|count|exists|delete|remove)((\p{Lu}.*?))??By
		Matcher matcher = PREFIX_TEMPLATE.matcher(source);

		if (!matcher.find()) {
			this.subject = new Subject(Optional.empty());
			this.predicate = new Predicate(source, domainClass);
		} else {
            // 解析方法前半部分 Subject: 执行关键字内容 DistinctUser
			this.subject = new Subject(Optional.of(matcher.group(0)));
            // 解析方法后半部分 Predicate: 属性及行为内容 IdAndNameOrderByAge
			this.predicate = new Predicate(source.substring(matcher.group().length()), domainClass);
		}
	}
	
	//...
}

由上可以看出,对方法名称的解析划分为前后两部分,我们主要来看属性及行为内容(属性、逻辑连接词、谓词关键字)的解析 Predicate,其源码如下:

private static class Predicate implements Streamable<PartTree.OrPart> {

    private static final Pattern ALL_IGNORE_CASE = Pattern.compile("AllIgnor(ing|e)Case");
    private static final String ORDER_BY = "OrderBy";
    // 保存方法名中的属性部分,即实体类中的属性
    private final List<PartTree.OrPart> nodes;
    // 保存方法名中的排序部分 OrderBy
    private final OrderBySource orderBySource;
    // 是否忽略所有查询占位参数的大小写(注意非属性/字段名)
    private boolean alwaysIgnoreCase;
    
    // @param predicate: 属性及行为语句,比如 IdAndNameOrEmailAndPhoneOrderByAge
    public Predicate(String predicate, Class<?> domainClass) {
        // 按OrderBy拆分语句: IdAndNameOrEmailAndPhone + OrderByAge
        String[] parts = split(detectAndSetAllIgnoreCase(predicate), ORDER_BY);

        if (parts.length > 2) {
            throw new IllegalArgumentException("OrderBy must not be used more than once in a method name");
        }
        // 解析非排序部分(属性信息): 先按 Or 切分为 OrPart 集合,OrPart 中再按 And 切分为属性
        // nodes = [IdAndName, EmailAndPhone]
        this.nodes = Arrays.stream(split(parts[0], "Or")) // 1、按照 Or 拆分语句
                .filter(StringUtils::hasText) // 2、过滤空串
                .map(part -> new PartTree.OrPart(part, domainClass, alwaysIgnoreCase)) // 3、每个拆分部分构建 OrPart 对象
                .collect(Collectors.toList());
        // 解析排序部分
        this.orderBySource = parts.length == 2 ? new OrderBySource(parts[1], Optional.of(domainClass))
                : OrderBySource.EMPTY;
    }
    
    // 检测是否设置忽略【所有占位参数】大小写,并截取忽略关键字
    private String detectAndSetAllIgnoreCase(String predicate) {
        // AllIgnoreCase/AllIgnoringCase 匹配
        Matcher matcher = ALL_IGNORE_CASE.matcher(predicate);

        if (matcher.find()) {
            alwaysIgnoreCase = true;
            // 截取关键字
            predicate = predicate.substring(0, matcher.start()) + predicate.substring(matcher.end(), predicate.length());
        }

        return predicate;
    }
    
    //...
}

Predicate 中对方法名后半部分的属性及行为内容进行了拆分,总体拆分顺序是OrderBy -> Or -> And,其中 nodes 存储了按 Or 拆分后的属性关联信息即 OrPart 对象列表,因此我们继续来看一下 OrPart 的实现源码:

public static class OrPart implements Streamable<Part> {
    // 存储拆分的属性信息 Part
    private final List<Part> children;

    OrPart(String source, Class<?> domainClass, boolean alwaysIgnoreCase) {
        // 按照 And 逻辑连接词拆分属性关联信息: EmailAndPhone -> Email + Phone
        String[] split = split(source, "And");
        // 构建属性信息列表 children
        this.children = Arrays.stream(split)
                .filter(StringUtils::hasText)
                .map(part -> new Part(part, domainClass, alwaysIgnoreCase)) // 构造属性信息对象 Part
                .collect(Collectors.toList());
    }
    
    //...
}

OrPart 类的实现非常简单,就是对根据 Or 连接词拆分后的属性关联语句进一步按照 And 连接词进行拆分(先Or后And),拆分后的属性信息除了包含实体属性,还有可能包含属性谓词(比如AgeLessThan、NameLike、IdIn等),因此拆分后的属性信息会进一步解析并封装为最细粒度的 Part 对象,存储在 children 列表中;我们接下来看一下 Part 类的实现源码是如何处理属性信息的:

public class Part {
    
	private static final Pattern IGNORE_CASE = Pattern.compile("Ignor(ing|e)Case");
    // 实体属性
	private final PropertyPath propertyPath;
    // 属性修饰谓词: LessThan、Like、Between...
	private final Part.Type type;

	private IgnoreCaseType ignoreCase = IgnoreCaseType.NEVER;


	public Part(String source, Class<?> clazz) {
		this(source, clazz, false);
	}


	public Part(String source, Class<?> clazz, boolean alwaysIgnoreCase) {

		Assert.hasText(source, "Part source must not be null or empty");
		Assert.notNull(clazz, "Type must not be null");
        // 检测是否设置忽略当前占位参数大小写,并截取忽略关键字
		String partToUse = detectAndSetIgnoreCase(source);

		if (alwaysIgnoreCase && ignoreCase != IgnoreCaseType.ALWAYS) {
			this.ignoreCase = IgnoreCaseType.WHEN_POSSIBLE;
		}
        // 解析属性修饰谓词
		this.type = Type.fromProperty(partToUse);
        // 解析实体属性
		this.propertyPath = PropertyPath.from(type.extractProperty(partToUse), clazz);
	}
    
    // 检测是否设置忽略【当前占位参数】大小写,并截取忽略关键字
	private String detectAndSetIgnoreCase(String part) {
        // IgnoringCase/IgnoreCase 匹配
		Matcher matcher = IGNORE_CASE.matcher(part);
		String result = part;

		if (matcher.find()) {
			ignoreCase = IgnoreCaseType.ALWAYS;
            // 截取关键字
			result = part.substring(0, matcher.start()) + part.substring(matcher.end(), part.length());
		}

		return result;
	}
    
    //...
	
}

Part 类中的两个关键方法分别是对属性修饰谓词进行解析对实体属性进行解析。首先对属性修饰谓词的解析逻辑很简单,就是遍历所有的修饰谓词(枚举类型),判断当前属性信息语句是否以某个谓词字符串结尾(endsWith),谓词的枚举类如下:

public static enum Type {

		BETWEEN(2, "IsBetween", "Between"), IS_NOT_NULL(0, "IsNotNull", "NotNull"), 
		IS_NULL(0, "IsNull", "Null"), LESS_THAN("IsLessThan", "LessThan"),
		LESS_THAN_EQUAL("IsLessThanEqual", "LessThanEqual"), 
		GREATER_THAN("IsGreaterThan","GreaterThan"), 
		GREATER_THAN_EQUAL("IsGreaterThanEqual", "GreaterThanEqual"), 
		BEFORE("IsBefore","Before"), AFTER("IsAfter", "After"), 
		NOT_LIKE("IsNotLike", "NotLike"), LIKE("IsLike","Like"), 
		STARTING_WITH("IsStartingWith", "StartingWith", "StartsWith"), 
		ENDING_WITH("IsEndingWith","EndingWith", "EndsWith"), IS_NOT_EMPTY(0, "IsNotEmpty", "NotEmpty"),
		IS_EMPTY(0, "IsEmpty","Empty"), NOT_CONTAINING("IsNotContaining", "NotContaining", "NotContains"),
		CONTAINING("IsContaining", "Containing", "Contains"), NOT_IN("IsNotIn", "NotIn"), 
		IN("IsIn","In"), NEAR("IsNear", "Near"), WITHIN("IsWithin", "Within"), 
		REGEX("MatchesRegex","Matches", "Regex"), EXISTS(0, "Exists"), 
		TRUE(0, "IsTrue", "True"), FALSE(0,"IsFalse", "False"), "Not"), SIMPLE_PROPERTY("Is", "Equals");

		// Need to list them again explicitly as the order is important
		private static final List<Part.Type> ALL = Arrays.asList(IS_NOT_NULL, IS_NULL, BETWEEN, LESS_THAN,
						LESS_THAN_EQUAL,GREATER_THAN, GREATER_THAN_EQUAL, BEFORE, AFTER, NOT_LIKE, LIKE, 
						STARTING_WITH, ENDING_WITH, IS_NOT_EMPTY,IS_EMPTY, NOT_CONTAINING, CONTAINING, 
						NOT_IN, IN, NEAR, WITHIN, REGEX, EXISTS, TRUE, FALSE,NEGATING_SIMPLE_PROPERTY, 
						SIMPLE_PROPERTY);
    
    	// ...
}

对实体属性进行解析是通过PropertyPath.from(type.extractProperty(partToUse), clazz)实现的,其中对实体属性进行转换的关键方法是decapitalize,其源码如下:

/**
* Java 属性命名转换:
* 	- 通常情况: 将第一个字符从大写转换为小写
*	- 特殊情况: 当有多个字符且第一和第二字符都是大写时,则无需处理
* Example:
*	- “FooBah”变成“FooBah”、“X”变成“X”
*	- “URL”保持为“URL”、“FName”保持为“FName”
**/
public static String decapitalize(String name) {
	if (name == null || name.length() == 0) {
		return name;
	}
    // 连续两个大写字符开头则无需处理
	if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && 
					Character.isUpperCase(name.charAt(0)){
		return name;
	}
    // 否则首字母转换为小写
	char chars[] = name.toCharArray();
	chars[0] = Character.toLowerCase(chars[0]);
	return new String(chars);
}

到此为止,按照命名规则自定义方法名的解析就基本完成了,若要使用命名规则定义方法的方式进行查询,则实体类中的属性名称最好是以小写字符开始,按照小驼峰方式命名。除了解析,还需要将解析属性跟实体属性进行映射和绑定,这一步是在构建查询中完成的,同时在这个过程中会检查已保存的解析属性是否与真正的实体属性一致,其相关源码如下:

/**
* 根据拆分属性名获取实体类对应属性
* @param name: 命名规则方法拆分的属性名称
**/
public PersistentAttributeDescriptor<? super J, ?> getAttribute(String name) {
    // declaredAttributes 中保存了实体类的属性信息(HashMap)
	PersistentAttributeDescriptor<? super J, ?> attribute = declaredAttributes.get( name );
	if ( attribute == null && getSuperType() != null ) {
		attribute = getSuperType().getAttribute( name );
	}
    // 检查实体类对应属性是否存在(解析属性是否与真正实体属性一致)
	checkNotNull( "Attribute ", attribute, name );
	return attribute;
}

// 检查实体类对应属性是否存在(解析属性是否与真正实体属性一致)
private void checkNotNull(String attributeType, Attribute<?,?> attribute, String name) {
    // 不一致则抛出异常 IllegalArgumentException
	if ( attribute == null ) {
		throw new IllegalArgumentException(String.format("Unable to locate %s with the the given name [%s] on this ManagedType [%s]",attributeType,name,getTypeName()));
	}
}

在这里插入图片描述

在此例中,实体类真正属性为 AddressInfo,而按照规则解析属性名为 addressInfo,因此会抛出异常信息

Reason: Failed to create query for method public abstract com.study.springdatajpademo.pojo.DemoInfo com.study.springdatajpademo.dao.IDemoInfoDao.findByNameAndAddressInfo(java.lang.String,java.lang.String)!

Unable to locate Attribute with the the given name [addressInfo] on this ManagedType [com.study.springdatajpademo.pojo.DemoInfo];

2、实体生命周期与状态转换

Spring Data JPA 只提供了一套基于 JPA 规范的接口,其底层仍是基于 Hibernate ORM 框架技术实现的。Hibernate 通过 Session 来建立数据库会话、实现数据交互,并引入了生命周期的概念来管理实体状态;在此基础之上,EntityManager 是 Spring Data JPA 完成数据持久化操作的核心对象,用来管理实体类与底层数据源之间的映射及生命周期状态:

在这里插入图片描述

Spring Data JPA 规范中定义实体生命周期有四种状态(Hibernate中是三种):

  • 新建状态/瞬时状态(New): 对象刚被创建(NEW)时,即处于新建状态,尚未拥有持久性主键;该对象数据与数据库没有任何关联,也不会被 EntityManager 所感知,如果没有变量引用这个对象,则其会被JVM的垃圾回收机制回收(对象信息将丢失)。
  • 托管状态/持久状态(Managed): 当对象被 EntityManager 管理(与Session产生关联)时,则成为持久化状态;此时,持久态对象的实例数据在数据库中有对应的记录,并且拥有数据库持久化标识(主键ID)。注意:对持久态对象的任何修改和变更都将同步到数据库(但不会马上同步到数据库,直到事务提交或主动flush)。
  • 游离状态(Datached): 处于持久状态的对象,脱离与其关联的 EntityManager 的管理(或者说Session的关联)后即进入于游离状态(比如会话关闭后、事务提交后等)。处于游离状态的对象已无法保证与数据库记录的一致性,也已无法执行有关数据库的操作(Hibernate已无法感知),但游离状态对象仍保有主键ID和曾经的数据库记录数据。
  • 删除状态(Removed): 执行删除的对象,仍保有主键ID和记录数据,但是其对应记录已从数据库中删除(Hibernate 生命周期中将其归类为瞬时态)。

具体来说,实体管理器 EntityManager 管理的目标是 PersistenceContext (持久化上下文),其是由一组受托管的实体对象实例所构成的集合,是从本地代码区到数据库之间的过渡区域;换句话说,可以将其看作是 Spring Data JPA 的一级缓存。EntityManager追踪保存在 PersistenceContext 中所有对象的修改和更新情况,并根据指定的 flush 模式将这些修改同步保存到数据库中;PersistenceContext一旦被关闭,所有关联的实体对象实例都会脱离 EntityManager 而成为非托管对象,不再受EntityManager管理,因此任何对此对象的状态变更也将不会被同步到数据库,除非重新回到 PersistenceContext 中。对应于实体的生命周期状态转换,实体管理器 EntityManager 提供了几种状态转换的接口方法及其状态转换图如下:

ReturnMethodDescription
voidpersist(Object entity)用于将对象实例entity转换为持久状态,交由 EntityManager 管理(调用时需要处于事务):
- entity 对象已处于瞬时状态:交由 EntityManager 管理,转换为持久状态;
- entity 对象已处于持久状态:则该操作什么都不做;
- entity 对象已处于删除状态:重新转换为持久状态;
- entity 对象已处于游离状态:会抛出 EntityExistException 异常(也有可能是在flush或事务提交时抛出异常);
<T> Tmerge(T entity)用于处理对象实例状态的更新与同步,并产生一个新的持久状态状态对象(调用时需要处于事务):
- entity 对象已处于瞬时状态:系统会执行数据库持久化操作,同时返回一个处于托管状态的复制实体;
- entity 对象已处于持久状态:实体状态不会发生任何改变;
- entity 对象已处于删除状态:方法调用将抛出异常 IllegalArgumentException
- entity 对象已处于游离状态:系统将实体的修改持久化到数据库,同时返会一个处于托管状态的实体;
voidremove(Object entity)用于将实体转换为删除状态,并在 flush() 或提交事务后同步删除数据库中的数据记录(调用时需要处于事务):
- entity 对象已处于瞬时状态:实体状态不会发生任何改变;
- entity 对象已处于持久状态:实体状态转化为删除状态;
- entity 对象已处于删除状态:实体状态不会发生任何改变;
- entity 对象已处于游离状态:方法调用将抛出 IllegalArgumentException 异常;
<T> Tfind(Class<T> entityClass, Object primaryKey)按主键查找实体,如果实体实例已包含在持久化上下文 PersistenceContext 中则直接返回;否则将创建并返回新的实体Entity(或null),同时将其交由EntityManager 管理
voiddetach(Object entity)从 PersistenceContext 中删除给定的实体entity,将其转换为游离状态;此时对实体所做的未提交更改(包括删除实体)将不会同步到数据库
voidflush()将 PersistenceContext 中所有未保存实体的状态信息同步到数据库中(调用时需要事务),可以通过 setFlushMode() 和 getFlushMode()来设置和获取持久上下文的Flush 模式:
- FlushModeType.AUTO:自动更新实体到数据库;
- FlushModeType.COMMIT:直到提交事务时才更新实体到数据库;
voidrefresh(Object entity)从数据库中同步数据来刷新实体entity的状态,覆盖对实体所做的更改(调用时需要处于事务):
- 若实体非托管状态/持久化状态:抛出IllegalArgumentException异常;
- 若实体在数据库不存在记录:抛出EntityNotFoundException异常;
voidclear()清除 PersistenceContext,使所有托管实体进入游离状态,对尚未提交更新到数据库的实体更改将不会进行持久化
booleancontains(Object entity)检查该实例entity是否是属于当前 PersistenceContext 的托管实体实例(托管状态)

在这里插入图片描述

public class UserRepositoryImpl {
    // @PersistenceContext 用于注入 EntityManager
    @PersistenceContext    
    private EntityManager entityManager;
    
    @Transactional    
    public void add(User user) {
        entityManager.persist(user);
        System.out.println(user.getId()); // 已生成主键标识
    }
    
    @Transactional    
    public User update(User user) {
        User userUpdate = entityManager.find(User.class, user.getId());
        userUpdate.setAddress(user.getAddress());
        userUpdate.setName(user.getName());
        userUpdate.setPhone(user.getPhone());    
        return userUpdate; //事务提交时同步更新到数据库
    }
    
    @Transactional    
    public User addOrUpdate(User user) { 
        // 数据库存在记录则update,否则执行insert
        return entityManager.merge(user);
    }
    
    @Transactional    
    public void delete(User user) {
        entityManager.remove(user);
    }    
    
    public User findOne(Integer id) {   
        return entityManager.find(User.class, id);
    }    
    
    public List<User> findAll() {
        String queryString = "select u from User u"; //JPQL
        Query query = entityManager.createQuery(queryString);        
        return query.getResultList();
    }
}

2.1 JPA 接管方法与实体状态

Spring Data JPA 中的保留方法基于命名规则的方法以及 @Query 自定义查询方法(包括JPQL与SQL) 所关联的实体均由 JPA 完全接管,在持久化上下文 PersistenceContext 中存储(所谓的JPA一级缓存)并由 EntityManager 实体管理器管理以参与上述状态转换。比如 JPA 自动更新的问题,是将 PersistenceContext 中持久化状态的实体,在事务提交后自动同步到数据库(默认为全字段更新):

@Transactional
public void updateDesInfoById(String desInfo, Long id) {
    // 基于命名规则的查询方法 -> 结果由EntityManager管理并转化为持久态
	UserInfo userInfo = userInfoDao.queryById(id);
	System.out.println(userInfo);
	// 更新持久化状态实体 -> 事务提交后自动同步实体变更到数据库(默认为全字段更新)
	userInfo.setEmail("testEmail@test.com");
}
[info] Hibernate: select userinfo0_.id as id1_1_, userinfo0_.age as age2_1_, userinfo0_.des_info as des_info3_1_, userinfo0_.email as email4_1_, userinfo0_.name as name5_1_ from user_info userinfo0_ where userinfo0_.id=?
[info] UserInfo(id=2, name=user_02, age=18, email=222222@test.com, description=user02 des info test)
[info] Hibernate: update user_info set age=?, des_info=?, email=?, name=? where id=?

注意: 如果想在查询持久化状态的实体后,对实体结果进行更新或其他变更操作以用于业务逻辑,为了避免引起意料之外的数据库变更,需要对实体进行深拷贝,然后基于深拷贝对象进行操作

2.2 @Modifying 自定义方法与实体状态

@Modifying 跟上述系列方法是两套不同的体系,@Modifying 所引起的数据库变更(包括 JPQL与 SQL) 并不会纳入 PersistenceContext 及 EntityManager 的管理,所不会影响到持久化上下文(JPA一级缓存)中原有的实体;换句话说,使用其它工具或框架所引起的数据库变更,也不能及时反应到 JPA 持久化上下文中来,这可能就会产生数据不一致的问题。 比如@Modifying方法与JPA接管的方法混用时:

public interface IUserInfoDao extends JpaRepository<UserInfo, Long> {
    // 基于命名规则的查询方法
    public UserInfo queryById(Long Id);
    // @Query自定义更新方法
    @Modifying
    @Query(value = "update user_info set des_info = ?1 where id = ?2")
    public int updateDesInfoById(String desInfo, Long id);
}

@Transactional
public void updateDesInfoById(String desInfo, Long id) {
    // 1.基于命名规则的方法查询实体 -> 查询结果进入持久化上下文管理并转为持久态
    // Hibernate: select userinfo0_.id as id1_1_, userinfo0_.age as age2_1_, userinfo0_.des_info as des_info3_1_, userinfo0_.email as email4_1_, userinfo0_.name as name5_1_ from user_info userinfo0_ where userinfo0_.id=?
    // UserInfo(id=2, name=user_02, age=18, email=222222@test.com, description=user02 des info test)
    UserInfo userInfo = userInfoDao.queryById(id);
    System.out.println(userInfo);
    // 2.@Modifying自定义方法更新实体 -> @Modifying独立于JPA的管理所以不影响持久化上下文的内容(事务未提交暂不会更新到数据库)
    // Hibernate: update user_info set des_info = ? where id = ?
    int cnt = userInfoDao.updateDesInfoById(desInfo, id);
    // 3.基于命名规则的方法查询实体 -> 查询结果来自持久化上下文的管理(内容不变)
    // Hibernate: select userinfo0_.id as id1_1_, userinfo0_.age as age2_1_, userinfo0_.des_info as des_info3_1_, userinfo0_.email as email4_1_, userinfo0_.name as name5_1_ from user_info userinfo0_ where userinfo0_.id=?
    // UserInfo(id=2, name=user_02, age=18, email=222222@test.com, description=user02 des info test)
    userInfo = userInfoDao.queryById(id);
    System.out.println(userInfo);
    // 4.自动更新持久化状态实体,事务提交后同步到数据库
    // WARN: 这会覆盖掉@Query自定义方法的更新,产生数据不一致问题!
    // Hibernate: update user_info set age=?, des_info=?, email=?, name=? where id=?
    userInfo.setEmail("testEmail@test.com");
}

@Modifying 引起的数据库变更 EntityManager 并不能发现,因此在updateDesInfoById方法更新后,后续的userInfoDao.queryById查询方法直接从持久化上下文中返回了数据(持久化上下文中的实体并未发生变化),并且最后对持久化实体属性的更新(全字段更新)覆盖掉了@Modifying提交到数据库的更新,导致数据不一致和数据丢失问题。对此 @Modifying 提供了两个属性值:

  • clearAutomatically = true:避免脏数据。在每次执行完modifying query之后会显式清理持久化上下文(clear),从而在下次查询时就可以读取到数据库中的最新数据(包括同一事务中);
  • flushAutomatically = true:避免数据丢失。在每次执行modifying query之前会先显式调用flush操作将持久化上下文中的实体变更同步到数据库中,从而避免数据丢失问题;

在更新方法上重新添加@Modifying(clearAutomatically = true)后,上述代码片段的执行结果如下:可以看到在更新后的第二次查询中已经可以正确查询到最新数据,并且数据库的最终结果也没有出现数据丢失,其细节如下:

  • 事务未提交为何能查询到最新数据: 首先更新方法执行后,通过注解的clear清空了持久化上下文内容,第二次查询就需要到数据库中去查询最新数据。然而在同一事务中,数据库连接是共享的;因此即使事务尚未提交,同一事务的查询操作仍然可以看到未提交的数据更改,从而返回最新数据;
  • 为何能够避免数据丢失: 第二次查询返回的是更新后的最新数据,并且由于是JPA接管的方法所以直接交给了持久化上下文管理,在事务提交后即使自动进行了全字段更新,但也是基于之前最新查询数据的更新,因此最终数据库的结果仍是正确的;
Hibernate: select userinfo0_.id as id1_1_, userinfo0_.age as age2_1_, userinfo0_.des_info as des_info3_1_, userinfo0_.email as email4_1_, userinfo0_.name as name5_1_ from user_info userinfo0_ where userinfo0_.id=?
UserInfo(id=2, name=user_02, age=18, email=222222@test.com, description=user02 des info test)
Hibernate: update user_info set des_info=? where id=?
Hibernate: select userinfo0_.id as id1_1_, userinfo0_.age as age2_1_, userinfo0_.des_info as des_info3_1_, userinfo0_.email as email4_1_, userinfo0_.name as name5_1_ from user_info userinfo0_ where userinfo0_.id=?
UserInfo(id=2, name=user_02, age=18, email=222222@test.com, description=update test)
Hibernate: update user_info set age=?, des_info=?, email=?, name=? where id=?

在实际运行中,clearflush操作都可能需要消耗一定的时间,要根据系统实际情况可以选择使用其中的一个或两个属性,以保证系统的正确性。

3、JPA与事务

Spring Data JPA 同样支持事务,比如其保留方法中的save以及delete全系列方法默认实现上都标注了@Transactional事务注解,这就说明使用save方法或delete方法时会单独开启事务。接下来,我们先来简单回忆一下事务所遵循的原则:

  • 在同一事务中: 事务首先具有原子性,即事务作为一个整体,若存在部分失败则整个事务都将回滚;事务还具有一致性,在同一事务中可以读取所做的任何更改,即使这些更改尚未提交
  • 在不同事务间: 不同事务之间遵循隔离性传递性。传递性是指事物之间的加入行为,Spring 默认的事务传播方式为PROPAGATION_REQUIRED,即如果当前存在事务则加入当前事务,否则就新建一个事务;而隔离性是为了解决并发事务之间出现的数据问题(脏读、不可重复读、幻读等),MYSQL InnoDB 默认的事务隔离级别为 REPEATABLE_READ,即事务在开启期间多次重复执行同一查询的返回结果均相同,即使其他事务已提交了修改。

基于上述理论,Spring Data JPA 的不同数据变更行为在事务中的表现存在不同,本节将进行验证和说明如下:

(1)@Modifying 修改方法(包括JPQL与SQL): 由上述分析中可知,@Modifying 所引起的数据库变更并不会经由PersistenceContext 处理,所以其执行语句(UPDATEDELETE)是直接发送到数据库的,并等待事务提交时更新数据;按照数据库行锁理论,此时对应的数据记录已被加锁,若在此期间其他事务同时修改同一记录则会阻塞等待;

// SQL
@Modifying
@Query(value = "update demo_info set des_info = ?1 where id = ?2", nativeQuery = true)
int updateDemoInfoById(String des, Long id);

// JPQL
@Modifying
@Query(value = "update DemoInfo demo set demo.desInfo = ?1 where demo.id = ?2")
int updateDemoInfoById(String des, Long id);
@Service
public class DemoInfoService {
    @Autowired
    IDemoInfoDao demoInfoDao;

    // 1.开启事务,执行 Native SQL 后挂起线程等待
    @Transactional
    public String updateByNative(Long Id) {
        System.out.println("执行本地SQL...");
        int cnt = demoInfoDao.updateDemoInfoById("native update test", Id);
        try {
            TimeUnit.SECONDS.sleep(60);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return "SUCCESS";
    }

    // 2.开启事务,执行SAVE更新并提交事务
    @Transactional
    public DemoInfo updateBySave(Long Id) {
        System.out.println("执行save方法...");
        DemoInfo demoInfo = new DemoInfo(Id,"save test","JPA update test"); //数据库中Id已存在
        DemoInfo saveEntity = demoInfoDao.save(demoInfo);
        System.out.println(saveEntity);
        return "SUCCESS";
    }
}

这里只展示了最重要的部分,接下来启动项目后,通过浏览器分别先后请求updateByNativeupdateBySave的接口地址(修改相同数据),可以发现在updateByNative方法挂起过程中,updateBySave也会进入阻塞等待,直到updateByNative提交事务或抛出行锁等待超时异常: Lock wait timeout exceeded; try restarting transaction;这就说明,@Modifying 修改方法的执行是直接提交到数据库的,而不经过持久化上下文的过渡;若在此期间存在其他事务修改同行记录则会阻塞等待,直到事务提交后更新数据。

(2)保留方法savesaveAndFlush 保留方法的数据变更由持久化上下文 PersistenceContext 及 EntityManager 实体管理器接管,其区别如下:

  • save:实体首先更新并存储在持久化上下文 PersistenceContext 中,默认事务提交时才自动执行flush()方法将实际执行语句发送到数据库,事务提交之前不会发送UPDATE语句;
  • saveAndFlush:实体更新并存储在持久化上下文中,同时将主动调用flush()方法立即将执行语句发送到数据库,并等待事务提交时更新数据,与@Modifying 的变更一致;
// 查询单数据
@Query(value = "SELECT * FROM demo_info WHERE id = ?1",nativeQuery = true)
public DemoInfo findRawMapByObject(Long id);
@Service
public class DemoInfoService {
    @Autowired
    IDemoInfoDao demoInfoDao;

    @Transactional
    public DemoInfo queryById(Long Id) {
        // 默认隔离级别可重复读
        // 其他事务更新未提交时,更新加数据库行锁之后,其他事务仍可以读取该记录(事务提交之前的旧数据,但不能更新\删除)
        DemoInfo queryObject = demoInfoDao.findRawMapByObject(Id);
        System.out.println(queryObject);
        try {
            // 睡一会,断点会阻塞请求
            TimeUnit.SECONDS.sleep(80);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        // 其他事务提交后,看到的仍是旧数据(可重复读)
        queryObject = demoInfoDao.findRawMapByObject(Id);
        System.out.println(queryObject);
        return queryObject;
    }

    // 1.开启事务,执行 Native SQL 并提交事务
    @Transactional
    public DemoInfo updateByNative(Long Id) {
        System.out.println("执行本地SQL...");
        int cnt = demoInfoDao.updateDemoInfoById("update native test", Id);
        System.out.println(cnt);
        return new DemoInfo();
    }

    // 2.开启事务,执行 SAVE 方法后挂起
    @Transactional
    public String updateBySave(Long Id) {
        System.out.println("执行save方法...");
        DemoInfo demoInfo = new DemoInfo(Id,"save test","save update test");
        DemoInfo saveEntity = demoInfoDao.save(demoInfo);
        try {
            TimeUnit.SECONDS.sleep(60);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(saveEntity);
        return "SUCCESS";
    }
    
    // 3.开启事务,执行 SAVEANDFLUSH 方法后挂起
    @Transactional
    public DemoInfo updateBySaveAndFlush(Long Id) {
        System.out.println("执行saveAndFlush方法...");
        // 保留方法/命名规则方法: 根据是否flush来发送到数据库,默认事务提交时才flush;首先修改的是持久化上下文中的实体数据,事务提交后再同步到数据库
        DemoInfo demoInfo = new DemoInfo(Id,"save test","update save test");
        // 1.save: 事务提交才flush,因此执行时不会发送update,只会更新持久化上下文数据,不会加数据库行锁

        // 2.saveAndFlush: 同native sql,执行时立即发送update(主动调用flush),会加数据库行锁,等待事务提交后更新数据(持久化上下文也会更新)
        DemoInfo saveEntity = demoInfoDao.saveAndFlush(demoInfo);
        try {
            // 睡一会,断点会阻塞请求
            TimeUnit.SECONDS.sleep(120);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(saveEntity);
        return saveEntity;
    }
    /**
     * 读未提交(Read Uncommitted):在这种最低的隔离级别下,事务可以看到其他事务尚未提交的数据。这意味着即使另一个事务还没有提交更新,当前事务也能看到更新后的数据。
     * 读已提交(Read Committed):这是大多数数据库系统的默认隔离级别。在此级别下,事务只能看到已经提交的数据。因此,如果另一个事务对记录进行了更新但尚未提交,当前事务查询到的数据仍然是更新前的状态。
     * 可重复读(Repeatable Read):innodb默认级别。在这一隔离级别下,一旦事务开始读取数据,它在整个事务期间看到的数据都是一致的,即第一次读取的结果。这意味着,即使其他事务提交了对同一记录的更新,当前事务在后续的读取中仍会看到最初读取的数据版本。
     * 序列化(Serializable):这是最高的隔离级别,事务完全隔离。在这一级别下,事务之间完全互不干扰,每个事务都会按照顺序执行,就像它们在一个串行队列中一样。这通常通过锁定整个范围来实现,可能会导致较高的锁争用。
     */


    // 注意: 更新加数据库行锁之后,其他事务仍可以读取该记录(事务提交之前的旧数据,但不能更新\删除),由于是默认可重复读隔离级别,即使其他事务提交了也仍然是旧数据
}

因此:

  • 在使用save的时候,当前事务不提交则不会发送到数据库执行,因此如果有更新之后发送消息的场景,使用save需谨慎(回滚后事务不一致);除此之外当前事务不提交,这时候其他事务过来进行修改数据,也可能会产生数据覆盖的情况(数据不一致)。
  • 在使用native sql或saveAndFlush的时候,当前事务不提交,其他事务过来的时候修改同一行记录,会产生锁等待现象,如果使用saveAndFlush建议将saveAndFlush操作放在最后执行,最大粒度的减少锁等待时间。

http://www.kler.cn/a/418669.html

相关文章:

  • unity中添加预制体及其基本设置
  • 自回归(Autoregressive)模型概述
  • 排序学习整理(1)
  • 接口测试工具:reqable
  • docker快速部署gitlab
  • Mybatis:CRUD数据操作之单个条件(动态SQL)
  • 科研学习|论文解读——基于旅游知识图谱的游客偏好挖掘和决策支持
  • 网络安全究竟是什么? 如何做好网络安全
  • 第十三周:密集嵌入算法(skip-gram)(word2vec)和嵌入语义特性
  • 【无标题】你的 github 项目,用的什么开源许可证
  • 【VUE】el-table表格内输入框或者其他控件规则校验实现
  • 学习笔记:黑马程序员JavaWeb开发教程(2024.11.28)
  • 前端js面试知识点思维导图(脑图)
  • TCP/IP网络协议栈
  • 题解:CF416C Booking System
  • 基于 Flask 和 RabbitMQ 构建高效消息队列系统:从数据生成到消费
  • leetcode 841.钥匙和房间
  • 【GESP】c++四级备考(含真题传送门)
  • 目标检测之学习路线(本科版)
  • 【SSM】mybatis的增删改查
  • 智能产品综合开发 - 智能家居(智能语音机器人)
  • 网安瞭望台第6期 :XMLRPC npm 库被恶意篡改、API与SDK的区别
  • Css、less和Sass(SCSS)的区别详解
  • 华为ACL应用笔记
  • 07.ES11 08.ES12
  • 设备内存指纹