解决JPA会save null的坑

目录

问题

尝试解决

解决


问题

最近学校课程项目遇到了一个问题,有关于jpa的save方法

①jpa 会 把database 中非null的字段覆盖,比如,User表,有id, name, password,某一次你更新了name,但是password没改,save完后,db中相应的password会被覆盖成null,动到了数据库的数据,这是我们忍不了的

②在更新某数据的时候,jpa默认先select,然后update,这是一个非常鸡肋的设计,当访问量大时,这影响着性能。

先看一下SimpleJpaRepository的源码

@Transactional
    public <S extends T> S save(S entity) {
        Assert.notNull(entity, "Entity must not be null.");
        if (this.entityInformation.isNew(entity)) {
            this.em.persist(entity);
            return entity;
        } else {
            return this.em.merge(entity);
        }
    }

 简单解释一下,当传入一个实体,首先判断是否存在id,

this.entityInformation.isNew(entity)

如果不存在则执行persist,你可以理解成插入(insert),否则执行merge,但是merge会select然后判断是否相同再update

网上找了很久,基本上都是告诉你,先找出对象,变更,再存。

不错,是个办法,但是不好

尝试解决

@dynamicUpdate似乎可以解决第一个问题,这里我不做讨论了

我们的目标是两个问题都解决了,不用select就update并且不能覆盖null值,就像mybatis一样

有关merge会select那没办法了,只能重写save方法

@Transactional
    public <S extends T> S save(S entity) {
        Assert.notNull(entity, "Entity must not be null.");
        if (this.entityInformation.isNew(entity)) {
            this.em.persist(entity);
            return entity;
        } else {
            Session session = em.unwrap(Session.class);
            session.update(entity);
        }
    }

这样就不会select了,但是第二个问题怎么办?

后面想到了@Column(updatable = false)会在数据更新的时候跳过该字段

那我们每次把null值的字段加上这个注解不就好了?

动态加上@Column注解

对于怎么加上@Column(updatable = false),利用java的反射,我这里就不贴代码了,网上很多。

你会发现加上之后仍然没用,后面猜想是不是 .class 文件没改的问题

改一下 .class文件

        ClassPool aDefault = ClassPool.getDefault();
        addAnnotation(className,attributeName,typeName);
        //使用类的全类名
        CtClass ctClass = aDefault.get(className);
        //获取当前项目系统路径
        File file = new File(".");
        //找到class文件地址
        String canonicalPath = file.getCanonicalPath() + "target所在地址";
        System.out.println(canonicalPath);
        byte[] bytes = ctClass.toBytecode();
        //写入到文件夹 --->这样就可以修改编译之后的class文件了
        FileOutputStream fileOutputStream = new FileOutputStream(canonicalPath);
        fileOutputStream.write(bytes);
        fileOutputStream.close();

 嗯,成功了

还是

没用!!!

只有第一次有用,后面再改也不会变

我猜测可能是初始化的时候jpa已经固定下来了,做不到动态更新。。。

后面又想通过刷新bean容器使jpa改变,但是。。。我不会!

希望有大佬能提点意见

解决

转念一想,既然不想select就update那为毛不直接手写update Sql语句啊?

说干就干

对于一个待存对象

我们把它所有的字段遍历一遍,如果为null就不更新,否则加入update SQL语句中

见代码

package cn.edu.xmu.javaee.core.jpa;

import org.apache.ibatis.javassist.NotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.Table;
import java.lang.reflect.Field;
import java.util.Arrays;

public class UpdateHelper {
    private static Logger logger = LoggerFactory.getLogger(UpdateHelper.class);

    /**
     * 获取update的sql语句
     * @author jslsb
     * @param bo
     * @return
     * @throws NotFoundException
     * @throws IllegalAccessException
     */
    public static String getSql(Object bo) throws NotFoundException, IllegalAccessException {
        Class<?> target = bo.getClass();
        StringBuffer stringBuffer = new StringBuffer("update ");
        logger.debug("getSql : className = {}",target.getName());
        //获取表名
        String table = target.getAnnotation(Table.class).name();
        logger.debug("getSql : table = {}",table);
        stringBuffer.append(table).append(" set ");
        //获取所有属性
        Field[] field = target.getDeclaredFields();
        //id列
        StringBuffer id = new StringBuffer("");
        //判断属性是否为null
        Arrays.stream(field).distinct().forEach(pp->{
            pp.setAccessible(true);
            boolean Is_String = pp.getType().getSimpleName().equals("String")||pp.getType().getSimpleName().equals("LocalDateTime");
            Object object = getValueByPropertyName(bo, pp.getName());
            logger.debug("getSql : pp = {}, object  = {}",pp,object);
            String column = getColumn(pp.getName());
            if(column.equals("id"))id.append(object);
            logger.debug("getSql : columns = {}",column);
            if(null != object && !column.equals("id")){
                stringBuffer.append(column).append(" = ");
                StringBuffer s = new StringBuffer();
                if(Is_String)s.append("'").append(object.toString()).append("'").append(", ");
                else s.append(object.toString()).append(", ");
                stringBuffer.append(s.toString());
            }
        });
        stringBuffer.deleteCharAt(stringBuffer.length()-2).append("where id = ").append(id.toString()).append(";");
        logger.info("getSql : sql = {}",stringBuffer.toString());

        return stringBuffer.toString();
    }

    /**
     * 获取列
     * @param name
     * @return
     */
    public static String getColumn(String name){
        StringBuffer stringBuffer = new StringBuffer("");
        for(int i = 0; i<name.length(); i++){
            char ch = name.charAt(i);
            if(Character.isUpperCase(ch))stringBuffer.append('_');
            stringBuffer.append(Character.toLowerCase(ch));
        }
        return stringBuffer.toString();
    }

    /**
     * 获取值
     * @param obj
     * @param propertyName
     * @return
     */
    public static Object getValueByPropertyName(Object obj, String propertyName) {
        StringBuffer stringBuffer = new StringBuffer("get");
        stringBuffer.append(Character.toUpperCase(propertyName.charAt(0))).append(propertyName.substring(1));
        logger.info("getValueByPropertyName : {} = {}","method",stringBuffer.toString());
        try {
            return obj.getClass().getMethod(stringBuffer.toString()).invoke(obj);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

save方法重写

package cn.edu.xmu.javaee.core.jpa;

import cn.edu.xmu.javaee.core.jpa.UpdateHelper;
import org.apache.ibatis.javassist.NotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import javax.persistence.EntityManager;
import javax.persistence.Query;
/**
 * 重写save方法
 * @param <T>
 * @param <ID>
 */
public class SimpleJpaRepositoryImpl<T, ID> extends SimpleJpaRepository<T, ID> {

    private final JpaEntityInformation<T, ?> entityInformation;
    private final EntityManager em;

    @Autowired
    public SimpleJpaRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.entityInformation = entityInformation;
        this.em = entityManager;
    }

    /**
     * 通用save方法 :新增/选择性更新
     */
    @Override
    @Transactional
    public <S extends T> S save(S entity) {
        Assert.notNull(entity, "Entity must not be null.");
        if (entityInformation.isNew(entity)) {
            em.persist(entity);
        } else {
            String sql = null;
            try {
                sql = UpdateHelper.getSql(entity);
            } catch (NotFoundException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
            Query x = em.createNativeQuery(sql);
            x.executeUpdate();
        }
        return entity;
    }
}

最后,记得在启动类更改!!!!!!!! 

@EnableJpaRepositories(value = "类的位置", repositoryBaseClass = SimpleJpaRepositoryImpl.class)
public class ShopApplication {

	public static void main(String[] args) {
		SpringApplication.run(ShopApplication.class, args);
	}

}

测试

@Test
    public void JapTest(){
        ShopPo po = new ShopPo();
        po.setId(30L);
        po.setName("j2slsb");
        po.setCreatorName("下次一定");
        Common.putGmtFields(po,"modified");
        shopPoMapper.save(po);
        Optional<ShopPo> newPo = shopPoMapper.findById(30L);
        assertThat(newPo.get().getName().equals("j2slsb"));
        assertThat(newPo.get().getDeposit().equals(5000000L));
    }

测试通过

这样我们就实现了不先select 后 update,并且不覆盖null。

当然想法很简单,效率是否高也是一个问题。

大佬如果有什么好的办法,欢迎讨论 

创作不易,给个赞叭


版权声明:本文为QMZQDF原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。