文章目录

  • 1. JPA自定义sql的三种方式
  • 1.1 在repository接口上注解@Query参数
  • 1.2 实现Specification可以用来做一些需要过滤条件的查询
  • 1.3 使用entityManager完全自定义的拼接sql
  • 1.3.1 两种类型的EntityManager对象
  • 2. JPA在使用时要特别注意的地方
  • 2.1 Save
  • 2.2 Delete
  • 2.3 Set属性时自动持久化
  • 3. JPA缓存
  • 3.1 简介
  • 3.2 一级缓存
  • 3.3 二级缓存
  • 4. JPA事务管理
  • 4.1 默认事务
  • 4.2 Dao层代码
  • 4.3 Service层代码


1. JPA自定义sql的三种方式

1.1 在repository接口上注解@Query参数

/**
     * 原生SQL查询
     * @param name
     * @return
     */
    @Query(value = "select ht.* from `act_hi_taskinst` ht WHERE ht.`NAME_` = ?1",
           nativeQuery = true)
    List<HiTaskinst> findHiTaskinstByName(String name);
    /**
     * jpql方式
     * @param l
     * @return
     */
    @Query( "from LinkMan l where l.lkmId =?1")
    LinkMan findById2(long l);

有nativeQuery = true和没有的区别

  • 有nativeQuery = true时,是可以执行原生sql语句,所谓原生sql,也就是说这段sql拷贝到数据库中,然后把参数值给一下就能运行了,比如:
    @Query(value = “select ht.* from act_hi_taskinst ht WHERE ht.NAME_ = ?1”,nativeQuery = true)
    List findHiTaskinstByName(String name);
    这个时候,把select ht.* from act_hi_taskinst ht WHERE ht.NAME_ = ?1 拷贝到数据库中,给NAME_赋一个值,
    那么这段sql就可以运行。其中数据库表在数据库中的表名就是act_hi_taskinst,字段NAME_在数据库中也是真实存在的字段名。
  • 没有nativeQuery = true时,就不是原生sql,而其中的select * from 的xxx中xxx也不是数据库对应的真正的表名,而是对应的实体名,并且sql中的字段名也不是数据库中真正的字段名,而是实体的字段名。例如:
    @Query( “select l from LinkMan l where l.lkmId =?1”)
    LinkMan findById2(long l);
    此中,select l from LinkMan中的LinkMan为实体名,不是真正的数据库表名,真正的数据库表名是cst_linkman,而查询条件中的lkmId 在数据库中真正的名字是lkm_id。

1.2 实现Specification可以用来做一些需要过滤条件的查询

实现动态条件查询(过滤条件)

//动态查询

        Specification<HiTaskinst> specification = new Specification<HiTaskinst>() {
            @Override
            public Predicate toPredicate(Root<HiTaskinst> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                Predicate p = null;
                //判断是否为空或者null,进行动态的查询
                if(!hiTaskinstVo.getName().equals("")&&hiTaskinstVo.getName()!=null){
                    //不为空或者null就进行模糊查询条件设置
                    p = criteriaBuilder.like(root.get("name"),"%"+hiTaskinstVo.getName()+"%");
                }else {
                    //空或者null就进行全部查询条件设置
                    p = criteriaBuilder.like(root.get("name"),"%%");
                }
                criteriaQuery.where(p);
                return null;
            }
        };

pecification.class是Spring Data JPA提供的一个查询规范,而你只需围绕这个规范来设置你的查询条件便可。
toPredicate,而其中的参数都是JPA规范中的:

  • Root 查询中的条件表达式
  • CriteriaQuery 条件查询设计器
  • CriteriaBuilder 条件查询构造器

而我们在使用复杂对象查询时,实现该方法用JPA去构造对象查询便可。

1.3 使用entityManager完全自定义的拼接sql

在了解EntityManager对象之前,首先要分清楚两个概念,即Java EE环境与J2SE环境。

  • Java EE环境,包括EJB容器和Web容器。
    – Web容器:只运行Web应用的容器,例如Tomcat就是开源的Web容器,它可以运行JSP、Servlet等。
    – EJB容器:运行在EJB组件的容器,提供EJB组件的状态管理、事务管理、线程管理、远程数据资源访问、连接管理和安全性管理等系统级服务。例如JBoss为EJB容器和Web容器(Web容器是集成了Tomcat)结合。
    – 部署在EJB容器中的JAR包都可以认为是运行在EJB容器中。但JBoss中的Web应用,比如war包中的类就不是运行在EJB容器中,而是运行在Web容器中。
  • J2SE环境
    – 最普通Java运行环境,例如一个HelloWorld的Java程序就是运行在J2SE的环境中,通常使用main入口方法作为程序启动的触发。

1.3.1 两种类型的EntityManager对象

  • 容器托管的(container-managed)EntityManager对象
    – 容 器托管的EntityManager对象最简单,程序员不需要考虑EntityManager连接的释放,以及事务等复杂的问题,所有这些都交 给容器去管理。容器托管的EntityManager对象必须在EJB容器中运行,而不能在Web容器和J2SE的环境中运行。
  • 应用托管的(application-managed)EntityManager对象
    – 应 用托管的EntityManager对象,程序员需要手动地控制它的释放和连接、手动地控制事务等。但这种获得应用托管的 EntityManager对象的方式,不仅可以在EJB容器中应用,也可以使 JPA脱离EJB容器,而与任何的Java环境集成,比如说Web容器、J2SE环境等。所以从某种角度上来说,这种方式是JPA能够独立于EJB环境运 行的基础。

理想状态下,最好是选用容器托管的EntityManager对象的方式,但在特殊的环境下,还是需要使用应用托管的EntityManager对象这种方式。

  1. 容器托管的(container-managed)EntityManager对象
    在EJB容器中获得EntityManager对象主要有两种方式,即@PersistenceContext注释注入和JNDI方式获得。
    通过@PersistenceContext 注解注入
@PersistenceContext(unitName = "jpaUnit")
    private EntityManager entityManager;
    /**
     * 自定义拼接sql查询
     */
    @Test
    public void demo05(){
        String sql = "select ht from Customer ht WHERE ht.custId = 9";
        TypedQuery<Customer> query = entityManager.createQuery(sql, Customer.class);
        List<Customer> resultList = query.getResultList();
        for (Customer customer : resultList) {
            System.out.println(customer);
        }
        System.out.println(resultList.size());
    }
  1. 应用托管的(application-managed)EntityManager对象
    应 用托管的EntityManager对象,不仅可以在Java EE环境中获得,也可以应用在J2SE的环境中。但无论是在什么情况下获得的EntityManager对象,都是通过实体管理器工厂 (EntityManagerFactory)对象创建的。所以如何获得应用托管的EntityManager对象关键是 EntityManagerFactory对象如何获得。
    EJB容器中获得
    在EJB容器中,EntityManagerFactory对象可以通过使用注入@PersistenceUnit注释获得。
@PersistenceUnit(unitName="jpaUnit")  
    private EntityManagerFactory emf;  
    public List<CustomerEO> findAllCustomers() {  
        /**创建EntityManager对象*/  
        EntityManager em = emf.createEntityManager();  
          Query query = em.createQuery("SELECT c FROM CustomerEO c");  
          List<CustomerEO> result = query.getResultList();  
        for (CustomerEO c : result) {  
              System.out.println(c.getId()+","+c.getName());  
        }  
        /**关闭EntityManager */  
        em.close();  
        return result;  
    }

2. JPA在使用时要特别注意的地方

2.1 Save

  1. 先select,再update
    save方法在更新DB时一个最大的特点是:默认每次都会先去查一遍DB,对查出来的DB数据与要保存的数据进行比较,若有变化,才会将数据持久化至DB(执行数据的update),否则就不会进行数据的持久化。
    这很有可能是因为底层Hibernate的Session生命周期限制导致的,对象实例有transient瞬时、persistent持久、detached游离三种状态。Hibernate要求实例对象在被更新或删除前,必须要为persitent状态,所以要先查一遍 在对性能有较高要求的场景下如批量保存更新时,save方式可能要慎重考虑,还需评估其对性能的影响。方案:
    改为使用@Query方式,即直接写update hql/sql语句来更新数据。
  2. 默认更新所有字段
    save方式下的更新数据,会默认更新该条记录的所有字段,即使只更改了一个字段值,JPA仍会对该条记录的所有字段进行更新。这个特性可能会造成2个后果:
    ①并发更新下数据不一致及产生不必要的性能浪费。对于DB来说,只更新一个字段,肯定是比更新十个字段的性能消耗小很多
    ②可能会把空值更新到DB
    方案: 使用@DynamicUpdate注解可以只更新所更改的字段,未更改的字段并不会被更新。使用@DynamicInsert注解可以避免插入null值

2.2 Delete

  1. 先select在delete
    同理,方案:
    改为使用@Query方式,即直接写delete hql/sql语句来删除数据
  2. 批量删除
    见源码可知JPA的delete(Iterable entities)为假的批量删除,内部实现其实还是for循环删除;deleteInBatch(Iterable entities)才是真正的批量删除,拼接ID参数一条SQL删除

2.3 Set属性时自动持久化

  • Hibernate 查出某个对象后,就进入了持久状态,处于Hibernate的缓存管理之中,此时对对象的任何属性改动,都会自动保存到数据库中
  • 方案:
    new(拷贝)一个新的对象出来进行操作,瞬时态
    清除或关闭session
    使该对象为游离态
@PersistenceContext
private EntityManager entityManager;

User u = userService.findByUsername("username");
// 方式一 清除持久上下文环境 避免后面语句导致持久化
entityManager.clear();
// 方式二 使Session中的该对象变为游离态
entityManager.detach(u);
u.setPassword(null); // 持久化状态时此处会自动执行update语句更新至数据库中

3. JPA缓存

3.1 简介

对于JPA2.0,缓存分为一级缓存和二级缓存(JPA1.0只支持一级缓存)。二级缓存通常是用来提高应用程序性能的,它可以避免访问以已经从数据库加载的数据,提高访问未被修改数据对象的速度。

3.2 一级缓存

持久化上下文就是JPA的一级缓存,通过在持久化上下文中存储持久化状态实体的快照,既可以进行脏检测,还可以当做持久化实体的缓存。一级缓存属于请求范围级别的缓存,如下:

spring data jpa集成clickhouse spring data jpa 教程_jpa


JPA默认情况下和MyBatis一样开启一级缓存。JPA是针对与entityManager,Mybatis是针对于namespace。

3.3 二级缓存

JPA二级缓存是跨越持久化上下文的,是真正意义上的全局应用缓存,如下:

spring data jpa集成clickhouse spring data jpa 教程_JPA_02


如果二级缓存激活,JPA会先从一级缓存中寻找实体,未找到再从二级缓存中寻找。当二级缓存有效时,就不能依靠事务来保护并发的数据,而是依靠锁策略,如在确认修改后,需要手工处理乐观锁失败等。

注意:二级缓存只能缓存通过EntityManager的find或getReference查询到的实体,以及通过实体的getter方法获取到的关联实体;而不能缓存通过JPQL查询获得的数据。

二级缓存通常用来提高性能,同时,使用二级缓存可能会导致提取到“陈旧”数据,也会出现并发写的问题。所以二级缓存最好是用在经常阅读数据,比较少更新数据的情况,而不应该对重要数据使用二级缓存。

4. JPA事务管理

4.1 默认事务

Spring Data 提供了默认的事务处理方式,即所有的查询均声明为只读事务。确保了单个请求过程数据的一致性。

  • 对于自定义的方法,如需改变 SpringData 提供的事务默认方式,可以在方法上注解@Transactional声明进行多个 Repository操作时,也应该使它们在同一个事务中处理,按照分层架构的思想,这部分属于业务逻辑层,因此,需要在Service 层实现对多个 Repository的调用,并在相应的方法上声明事务。
    Repository概念:按照最初提出者的介绍,它是衔接数据映射层和域之间的一个纽带,作用相当于一个在内存中的域对象集合。客户端对象把查询的一些实体进行组合,并把它们提交给Repository。对象能够从Repository中移除或者添加,就好比这些对象在一个Collection对象上进行数据操作,同时映射层的代码会对应的从数据库中取出相应的数据。从概念上讲,Repository是把一个数据存储区的数据给封装成对象的集合并提供了对这些集合的操作。 广义上可以理解为我们常说的DAO

4.2 Dao层代码

@Modifying
@Query(value="UPDATE hr_employee_contract t SET t.deleteStatus=1 WHERE t.id=?1",nativeQuery = true)
void delete(Long id);

说明:@Modifying注解
①在@Query注解中,编写JPQL实现DELETE和UPDATE操作的时候,必须加上@modifying注解,以通知Spring Data 这是一个DELETE或UPDATE操作。
②UPDATE或者DELETE操作需要使用事务,此时需要定义Service层,在Service层的方法上添加事务操作。
③注意JPQL不支持INSERT操作。

4.3 Service层代码

使用@Transactional手动开启事务管理

@Transactional
@Override
public void delete(Long id) {
    employeeContractDao.delete(id);
}

@Transactional注解支持9个属性的设置,其中使用较多的三个属性:readOnly、propagation、isolation。其中propagation属性用来枚举事务的传播行为,isolation用来设置事务隔离级别,readOnly进行读写事务控制。

① readOnly

从这一点设置的时间点开始(时间点a)到这个事务结束的过程中,其他事务所提交的数据,该事务将看不见!(查询中不会出现别人在时间点a之后提交的数据)

应用场合:

NO.1、如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;
NO.2、如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。
【注意是一次执行多次查询来统计某些信息,这时为了保证数据整体的一致性,要用只读事务】

怎样设置:

对于只读查询,可以指定事务类型为readonly,即只读事务。
由于只读事务不存在数据的修改,因此数据库将会为只读事务提供一些优化手段,例如Oracle对于只读事务,不启动回滚段,不记录回滚log。

(1)在JDBC中,指定只读事务的办法为: connection.setReadOnly(true);

(2)在Hibernate中,指定只读事务的办法为: session.setFlushMode(FlushMode.NEVER);
此时,Hibernate也会为只读事务提供Session方面的一些优化手段

(3)在Spring的Hibernate封装中,指定只读事务的办法为: bean配置文件中,prop属性增加“readOnly”
或者用注解方式@Transactional(readOnly=true)
【 if the transaction is marked as read-only, Spring will set the Hibernate Session’s flush mode to FLUSH_NEVER,
and will set the JDBC transaction to read-only】也就是说在Spring中设置只读事务是利用上面两种方式
② propagation

//支持当前事务,如果当前没有事务,就新建一个事务。Spring默认事务级别。

int PROPAGATION_REQUIRED = 0;  

//支持当前事务,如果当前没有事务,就以非事务方式执行。

int PROPAGATION_SUPPORTS = 1;  

//支持当前事务,如果当前没有事务,就抛出异常。

int PROPAGATION_MANDATORY = 2;  

//新建事务,如果当前存在事务,把当前事务挂起。执行新事务后,再激活当前事务。

int PROPAGATION_REQUIRES_NEW = 3;  

//以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

int PROPAGATION_NOT_SUPPORTED = 4;  

//以非事务方式执行,如果当前存在事务,则抛出异常。

int PROPAGATION_NEVER = 5;

//如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

//嵌套时由外部事务决定,子事务是否是commit还是rollback。

//一般在外部事务是使用try{}catch(嵌套事务方法){}进行编码。

int PROPAGATION_NESTED = 6;

案例分析1:

@Service
class A{
    @Autowired
    B b;
 
    @Transactional(propagation = Propagation.REQUIRED)    
    void call(){
        try{
            b.call();
        } catch(Exception e){
            //doSomething....
            //不抛异常,则A无法提交
        }
        //doSomething....
    }
}
 
@Service
class B{
    @Transactional(propagation = Propagation.REQUIRED)
    void call(){}
}

A和B共用事务,如果B异常。A未使用try…catch…捕获,则AB一起回滚。

如果B异常。A捕获,但并未抛出。则A最终也无法提交,因为B的事务已经被设置为rollback-only了。
案例分析2:

@Service
class A{
    @Autowired
    B b;
 
    @Transactional(propagation = Propagation.REQUIRED)    
    void call(){
        try{
            b.call();
        } catch(Exception e){
            throw e; //或者不抛
        }
        //doSomething....
    }
}
 
@Service
class B{
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void call(){}
}

执行b.call()时A事务挂起,此时如果B执行异常。被A捕获,如果抛出异常,则AB回滚;如果A捕获未抛异常,则A继续执行不回滚。

执行b.call()时A事务挂起,此时如果B正常执行,而在A中出现异常。则B不回滚,A回滚。
案例分析3:

@Service
class A{
    @Autowired
    B b;
 
    @Transactional(propagation = Propagation.REQUIRED)    
    void call(){
        try{
            b.call();
        } catch(Exception e){
            throw e; //或者不抛
        }
        //doSomething....
    }
}
 
@Service
class B{
    @Transactional(propagation = Propagation.NESTED)
    void call(){}
}

执行b.call()时A事务挂起,B新起事务并设置SavePoint。如果B正常执行,A出现异常,则AB一起回滚。

如果B失败异常,此时A如果捕获但未抛出,后续A正常执行的话,A可以提交,而B已经回滚。

如果B失败异常,此时A如果捕获且抛出,则AB一起回滚。

以上案例,我们可以得出第1种和第3种模式的区别,第3种在嵌套模式下,可以在内部异常下执行其它业务且外部正常提交,而第1种不可以这么操作。

③ isolation

@Override
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void test() {
        User user = userMapper.getOne(1L);
        System.out.println(user.getName());
 
        userMapper.updateUser();
        try {
            Thread.sleep(10000); // 10 s
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end");
    }

@Transactional

默认值

spring data jpa集成clickhouse spring data jpa 教程_二级缓存_03


@Transactional 注解的属性信息

属性名

说明

name

当在配置文件中有多个 TransactionManager , 可以用该属性指定选择哪个事务管理器。

propagation

事务的传播行为,默认值为 REQUIRED。

isolation

事务的隔离度,默认值采用 DEFAULT。

timeout

事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。

read-only

指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。

rollback-for

用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。

no-rollback- for

抛出 no-rollback-for 指定的异常类型,不回滚事务。

此外,

① @Transactional 注解也可添加在类级别上。当把@Transactional 注解放在类级别时,表示所有该类的公共方法都配置相同的事务属性信息。如下面类,EmployeeService 的所有方法都支持事务并且是只读。

② 当类级别配置了@Transactional,方法级别也配置了@Transactional,应用程序会以方法级别的事务属性信息来管理事务,换言之,方法级别的事务属性信息会覆盖类级别的相关配置信息。

@Transactional 注解的类级别支持

@Transactional(propagation= Propagation.SUPPORTS,readOnly=true)
@Service(value ="employeeService")
public class EmployeeService