字数:8716

1 起因

  • 在springboot的基础之上,引入spring-boot-starter-data-jpa后,简化数据层的开发,甚至不用再写HQL语句;
  • 另外,想解决统一的时间戳和逻辑删除的操作

2 问题

惨案发生在这里:
惨案

3 具体说明

文件结构如下:

├─src
│  ├─main
│  │  ├─java
│  │  │  └─com
│  │  │      └─xxx
│  │  │          └─qingdu
│  │  │              │  Starter.java #Main方式启动spring
│  │  │              │
│  │  │              ├─bean
│  │  │              │      AbsIdEntity.java #bean的基类,统一的ID/时间戳/假删除字段
│  │  │              │      User.java #案例bean
│  │  │              │
│  │  │              ├─config
│  │  │              │      IdGeneratorConfig.java #全局唯一主键bean
│  │  │              │      JpaConfig.java #spring data jpa的一些配置,包括扫描包、工厂类等 
│  │  │              │      RedisConfig.java #redis的template处理bean,跟本例无关
│  │  │              │
│  │  │              ├─controller
│  │  │              │      LibraryCardController.java #证件controller,跟本例无关
│  │  │              │      UserController.java
│  │  │              │
│  │  │              ├─repository
│  │  │              │  │  BaseRepositoryFactoryBean.java #repository实例的工厂
│  │  │              │  │  IBaseRepository.java #repository的基础接口,主要是定义了扩展另外的接口,这样在子接口就不用再定义
│  │  │              │  │  IUserRepository.java #user业务的repository接口,这里面也没什么方法好定义的
│  │  │              │  │
│  │  │              │  └─impl
│  │  │              │          BaseRepositoryImpl.java #repository接口的基础实现,不定义这个实现使用默认的也能操作数据
│  │  │              │
│  │  │              └─service
│  │  │                      UserService.java #user服务实现类
│  │  │                      UserServiceImpl.java #user服务接口类
│  │  │
│  │  ├─resources
│  │  │      application.properties #spring全局配置
│  │  │      application.yml #spring全局配置

问题在上面都描述过了,再组织一下:

(1)写了base repository interface,没什么内容,如下:

package com.xxx.qingdu.repository;

import java.io.Serializable;

import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.NoRepositoryBean;

import com.xxx.qingdu.bean.AbsIdEntity;

/**
* DAO基类,继承了另两个类,保证所有Repository都有基本的增删改查以及分页等方法
*
* 所有Repository接口都继承BaseDao
*
* 使用时只需要继承接口,不需要实现类,spring自动通过cglib生成实现类
*
*
* @param <T>
*/
@NoRepositoryBean
public interface IBaseRepository <T extends AbsIdEntity> extends
        CrudRepository<T, Serializable>, JpaSpecificationExecutor<T>{
}

(2)写了user repository interface,没什么内容

(3)写了base repository implement,出问题的地方,内容如下:

package com.xx.qingdu.repository.impl;

import java.io.Serializable;
import java.sql.Timestamp;

import javax.persistence.EntityManager;
import javax.transaction.Transactional;

import org.springframework.data.jpa.repository.support.SimpleJpaRepository;

import com.xxx.qingdu.bean.AbsIdEntity;
import com.xxx.qingdu.repository.IBaseRepository;

/**
* impl基类,使其具有Jpa Repository的基本方法
*
* JPA事务的使用:https://www.jianshu.com/p/c84a91992c32
* 加注解实现逻辑删除:https://www.liangzl.com/get-article-detail-3633.html
* @param <T>
*/
//@Transactional
public class BaseRepositoryImpl <T extends AbsIdEntity> extends SimpleJpaRepository<T, Serializable>
        implements IBaseRepository<T>  {
    @SuppressWarnings("unused")
    private final EntityManager entityManager;

    public BaseRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
        super(domainClass, entityManager);
        this.entityManager = entityManager;
    }

//    public BaseDaoImpl(JpaEntityInformation<T, Serializable> information, EntityManager entityManager) {
//        super(information, entityManager);
//        this.entityManager = entityManager;
//    }

    @Override
    public <S extends T> S save(S entity) {
        entity.setLastTime(new Timestamp(System.currentTimeMillis()));
        S s = super.save(entity); //save定义的是事务,内部先判断有无,有则更新无则插入
        return s;
    }

    /**
     * 只做逻辑删除
  
     * 或逻辑删除用注解定义到bean上
     */
    @Override
    public void delete(T entity) {
        entity.setIsDelete(1);
        this.save(entity);
    }
    @Transactional //声明了事务,才能Save,findOne是一个只读事务,另外,最好将注解定义到Service方法上
    @Override
    public void delete(Serializable id) {
        T entity = findOne(id);
        entity.setIsDelete(1);
        this.save(entity);
    }
}

(4)写了bean factory,内容简单,如下:

package com.xx.qingdu.repository;

import java.io.Serializable;

import javax.persistence.EntityManager;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;

import com.xxx.qingdu.bean.AbsIdEntity;
import com.xxx.qingdu.repository.impl.BaseRepositoryImpl;

/**
* https://yq.aliyun.com/articles/465404
* 另一种做法直接改spring data的代码,包括查询排除掉逻辑删除的内容 https://blog.csdn.net/Gavid0124/article/details/79063999
*
* 代替默认的RepositoryFactoryBean,负责返回一个Factory,spring data jpa使用其来创建repository的具体实现
* @param <R> repository的类型
* @param <T> the domain type the repository manages
*/
public class BaseRepositoryFactoryBean<R extends JpaRepository<T, Serializable>, T extends AbsIdEntity>
        extends JpaRepositoryFactoryBean<R, T, Serializable>  {

    public BaseRepositoryFactoryBean(Class<? extends R> repositoryInterface) {
        super(repositoryInterface);        
    }

    @Override
    protected RepositoryFactorySupport createRepositoryFactory(final EntityManager entityManager) {
        return new JpaRepositoryFactory(entityManager) {

            protected SimpleJpaRepository<T, Serializable> getTargetRepository(
                    RepositoryInformation information,    EntityManager entityManager) {
                Class<T> domainClass = (Class<T>) information.getDomainType(); //获得repository接口关联的Bean类
                //根据bean类是否继承了基类,来返回不同的Repository
                if(AbsIdEntity.class.isAssignableFrom(domainClass)) {
                    return new BaseRepositoryImpl(domainClass, entityManager); //这些repository的实现在springboot启动时即初始化
                } else {
                    return new SimpleJpaRepository(domainClass, entityManager);
                }
            }

            @Override
            protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
                return metadata.getDomainType().isAssignableFrom(AbsIdEntity.class) ?
                        BaseRepositoryImpl.class : SimpleJpaRepository.class;
            }
        };
    }
}

(5)写了user service interface,就定义几个方法
(6)写了user service implement,出问题的地方,内容如下:

package com.xxx.qingdu.service;

import java.sql.Timestamp;
import java.util.List;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import com.xxx.qingdu.bean.User;
import com.xxx.qingdu.repository.IUserRepository;
import com.xxx.tools.ObjectUtil;
import com.xxx.tools.SnowflakeIdWorker;

@Service
@EnableCaching //启用缓存
public class UserServiceImpl implements UserService{
    @Autowired private SnowflakeIdWorker idGenerator;
    @Autowired private IUserRepository userRepository;
  
    /**
     * 更新用户信息
     *
     * https://blog.csdn.net/Soulmate_Min/article/details/82777293
     */
    @Override
    @Transactional //JPA更新需要使用事务机制,即加锁同步,否则update无效
    public User updateUser(User u) {
        //这里调用的是org.springframework.data.jpa.repository.support.SimpleJpaRepository<T, Serializable> 
        //而该方法的事务沿用的是注解在类上的readOnly属性?再使用com.xxx.qingdu.repository.impl.BaseRepositoryImpl.save无效。在方法上注解即可
        User u_exists = userRepository.findOne(u.getId());

        if (u_exists != null) {
            // 仅将参数对象里有的内容覆盖到库里的对象
ObjectUtil.combineObject(User.class, u, u_exists);
        } else {
            u.setCreateTime(new Timestamp(System.currentTimeMillis()));
            u_exists = u;
        }
        return userRepository.save(u_exists);
    }
    
    /**
     * 首次创建用户信息
     */
    @Override
    public User createUser(User u) {
        //u.setId(idGenerator.nextId());          
        u.setCreateTime(new Timestamp(System.currentTimeMillis()));
        return userRepository.save(u); //调用com.xxx.qingdu.repository.impl.BaseRepositoryImpl.save(S)
    }

    //@Transactional
    @Override
    public void deleteUser(Long id_user) {
        userRepository.delete(id_user);
    }
}

4 结论

  • jpa的更新操作需要注解为事务

  • 注解不要写在接口方法上

  • 注解可以定义到repository实现类的类上,或方法上,方法上优先

  • 提倡注解定义到service的方法上,调用者注解优先

  • 还遇到过一个比较匪夷所思的问题,但是同一个@Transactional方法里update操作有效,create操作无效。 改成后者解决问题 ——>

JPA事务注解相互影响.jpg

问题的根源在于,顶层Service方法上必须要注解@Transactional

以上内容,在《spring data jpa从入门到精通》里也说明的不全面啊,真可谓不踩坑不知坑的深浅,另外,读书的方法是不是这样,先找到问题,先去尝试解决问题才能求得甚解。