SpringDataJPA到目前为止已经解决了大量的问题,但依然还有一些比较重要的问题没有解决:
- 批量删除和更新的操作
- 事务处理问题
Spring最大的一个优点就是声明式的事务,在原来的开发环境中我们需要在beans.xml中配置事务在哪些类上有作用,现在SpringBoot已经帮助我们完成了这些配置,我们仅仅需要加几个Annotation就可以解决问题。
我们假设我们的开发环境是这样的:有两个类,一个是Classroom,一个是Student,两者之间有相应的关联,这里个人依然强烈不建议使用对象的方式进行关联(在Student中创建一个Classroom的对象),个人比较推崇就是在Student中加入一个Classroom的id即可。
@Entity
@Table(name="t_classroom")
public class Classroom {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
private String grade;
//省略了getter和setter
}
@Entity
@Table(name="t_student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
private String address;
private int age;
/**
*classroom对象的外键,不建议使用对象的方式关联
*/
private int cid;
//省略了getter和setter
}
此时我们如果需要删除班级信息,应该先要把学生信息删除,或者在删除时检查是否存在学生信息,面对这样的问题我们就必须手动来实现这个方法,不能直接用SpringDataJpa提供的方法,所以此时我们首先需要创建Classroom自己的工厂,在第四讲中详细介绍了这个知识。这里首先要思考的第一个问题就是,为什么我们原来的操作都没有加任何事务处理的代码却可以完成修改,因为SpringDataJPA的CrudRepository
,JPARepository
等默认接口的实现类是SimpleJpaRepository
,大家可以看一下这个代码,我们会发现这些方法已经自动添加了事务处理。
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID extends Serializable>
implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
@Transactional
public void delete(ID id) {
Assert.notNull(id, ID_MUST_NOT_BE_NULL);
T entity = findOne(id);
if (entity == null) {
throw new EmptyResultDataAccessException(
String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1);
}
delete(entity);
}
@Transactional
public void delete(T entity) {
Assert.notNull(entity, "The entity must not be null!");
em.remove(em.contains(entity) ? entity : em.merge(entity));
}
@Transactional
public void delete(Iterable<? extends T> entities) {
Assert.notNull(entities, "The given Iterable of entities not be null!");
for (T entity : entities) {
delete(entity);
}
}
@Override
public T getOne(ID id) {
Assert.notNull(id, ID_MUST_NOT_BE_NULL);
return em.getReference(getDomainClass(), id);
}
}
以上我摘取了SimpleJpaRepository的几个代码,我们看到在class上面有个@Transactional(readOnly = true)
这说明该类中的所有方法都是加入到事务中的,而且都是只读的,所以类中的默认方法都是事务只读,对于删除修改等操作,我们只要在该方法上添加@Transactional
,就说明该事务是可以修改的。SpringDataJPA已经帮助我们把事务处理得如此的简单。这确实很大程度上减少了开发人员的工作量。但是我们自己的方法需要自己加入事务处理。
我们依然有一些问题需要解决
- 要根据班级id批量删除学生该如何处理
- 哪些自定义的方法需要加入事务
- Classroom的自定方法的写法
第一个问题有两种解决方案,一种是在StudentRepository
中添加一个方法,通过@Query
来解决,代码如下
public interface StudentRepository extends BaseRepository<Student,Integer>,JpaSpecificationExecutor<Student> {
@Modifying //说明该操作是修改类型操作,删除或者修改
@Transactional //因为默认是readOnly=true的,这里必须自己进行声明
@Query("delete from Student where cid=?1") //删除的语句
public void deleteByCla(int cid);
}
一定要注意@Modifying
和@Transactional
必须添加到接口的方法上,当调用这个方法就可以通过班级来删除学生了,但这样我们如果有任何批量操作的方法都得在相应的Repository
接口中声明。调用代码如下:
@Test
public void testDeleStu() {
studentRepository.deleteByCla(2);
}
第二种方式是直接在第四讲中的BaseRepository
中添加基于HQL和SQL的update操作,这样就可以直接使用SQL或者HQL来处理,能够满足所有的要求。
@NoRepositoryBean
@Transactional(readOnly=true)
public interface BaseRepository<T,ID extends Serializable> extends JpaRepository<T,ID> {
public List<Object[]> listBySQL(String sql);
@Transactional
public void updateBySql(String sql,Object...args);
@Transactional
public void updateByHql(String hql,Object...args);
}
public class BaseRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T,ID>
implements BaseRepository<T,ID> {
private final EntityManager entityManager;
//父类没有不带参数的构造方法,这里手动构造父类
public BaseRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
super(domainClass, entityManager);
this.entityManager = entityManager;
}
//通过EntityManager来完成查询
@SuppressWarnings("unchecked")
@Override
public List<Object[]> listBySQL(String sql) {
return entityManager.createNativeQuery(sql).getResultList();
}
@Override
public void updateBySql(String sql,Object...args) {
Query query = entityManager.createNativeQuery(sql);
int i = 0;
for(Object arg:args) {
query.setParameter(++i,arg);
}
query.executeUpdate();
}
@Override
public void updateByHql(String hql,Object...args) {
Query query = entityManager.createQuery(hql);
int i = 0;
for(Object arg:args) {
System.out.println(arg);
query.setParameter(++i,arg);
}
query.executeUpdate();
}
}
以上代码是在BaseRepository中创建的,由于在实际的开发环境中,我们都要实现这个接口,所以updateByHql和updateBySql是所有接口共有的,我们可以直接调用,注意的是这些需要进行修改的方法我们都要加上@Transactional
的事务声明(我们都是在BaseRepository的接口上声明的)。调用代码如下所示
@Test
public void testDeleStu2() {
studentRepository.updateByHql("delete from Student where cid=?", 2);
}
现在我们只要如何执行批量更新和删除的方法了,也同样知道哪些方法该加入事务信息了,下一步我们要解决ClassroomRepository的写法,这个和第四讲完全一致,需要先创建ClassroomRepositoryCustom接口和ClassroomRepositoryImpl实现类,注意这两个名称必须是固定的。然后让ClassroomRepository实现ClassroomRepositoryCustom接口即可。代码如下:
@Transactional(readOnly = true)
public interface ClassroomRepositoryCustom {
@Transactional
public void delete(int cla);
}
public class ClassroomRepositoryImpl implements ClassroomRepositoryCustom {
@Autowired
@PersistenceContext
private EntityManager entityManager;
@Autowired
private StudentRepository studentRepository;
@Override
public void delete(int cid) {
Classroom cla = entityManager.find(Classroom.class, cid);
studentRepository.updateByHql("delete from Student where cid=?",cid);
//强行抛出异常,验证声明式事务是否起作用
if(cid>1) throw new RuntimeException();
entityManager.remove(cla);
}
}
以上代码就是班级删除的所有操作,这样强行抛出了一个RuntimeException以此验证声明式事务。我们发现一切都工作正常了,到这里批量修改和事务的操作都已经完成。
本文的源代码在这里:源代码