一、基本介绍
JPA诞生的缘由是为了整合第三方ORM框架,建立一种标准的方式,百度百科说是JDK为了实现ORM的天下归一,目前也是在按照这个方向发展,但是还没能完全实现。在ORM框架中,Hibernate是一支很大的部队,使用很广泛,也很方便,能力也很强,同时Hibernate也是和JPA整合的比较良好,我们可以认为JPA是标准,事实上也是,JPA几乎都是接口,实现都是Hibernate在做,宏观上面看,在JPA的统一之下Hibernate很良好的运行。
我们都知道,在使用持久化工具的时候,一般都有一个对象来操作数据库,在原生的Hibernate中叫做Session,在JPA中叫做EntityManager,在MyBatis中叫做SqlSession,通过这个对象来操作数据库。我们一般按照三层结构来看的话,Service层做业务逻辑处理,Dao层和数据库打交道,在Dao中,就存在着上面的对象。那么ORM框架本身提供的功能有什么呢?答案是基本的CRUD,所有的基础CRUD框架都提供,我们使用起来感觉很方便,很给力,业务逻辑层面的处理ORM是没有提供的,如果使用原生的框架,业务逻辑代码我们一般会自定义,会自己去写SQL语句,然后执行。在这个时候,Spring-data-jpa的威力就体现出来了,ORM提供的能力他都提供,ORM框架没有提供的业务逻辑功能Spring-data-jpa也提供,全方位的解决用户的需求。使用Spring-data-jpa进行开发的过程中,常用的功能,我们几乎不需要写一条sql语句。
二、入门示例
1、配置相关
首先maven依赖如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
然后配置文件
spring:
# 测试的时候可以不用mysql
# datasource:
# url: jdbc:mysql://127.0.0.1:3306/jpa_demo?characterEncoding=utf-8&useSSL=true
# username: root
# password: root
# driver-class-name: com.mysql.jdbc.Driver
h2: # 使用H2 控制台 访问页面http://localhost:8080/h2-console/,
console:
enabled: true
jpa:
show-sql: true
hibernate:
ddl-auto: update
在测试阶段可以使用H2数据库,其管理页面如下(http://localhost:8080/h2-console/ ),默认的数据库为jdbc:h2:mem:testdb,如果启用mysql,把mysql注释放开即可。
2、创建实体类
实体类中常用注解:
- @Entity :声明这个类是一个实体类
- @Table:指定映射到数据库的表格
- @Id :映射到数据库表的主键属性,一个实体只能有一个属性被映射为主键
- @GeneratedValue:主键的生成策略
- @Column配置单列属性
@Entity//标识该为一个实体
@Table(name = "user")//关联数据库中的user表
public class User {
@Id//标识该属性为主键
private Integer id;
private String name;
private String address;
private String phone;
//省略get和set
}
3、Repository接口
- Repository: 最顶层的接口,是一个空接口,目的是为了统一所有的Repository的类型,且能让组件扫描时自动识别
- CrudRepository: Repository的子接口,提供CRUD 的功能。
- PagingAndSortingRepository:CrudRepository的子接口, 添加分页排序。
- JpaRepository: PagingAndSortingRepository的子接口,增加批量操作等。
- JpaSpecificationExecutor: 用来做复杂查询的接口。
由图来看,一般使用JpaRepository这个接口做查询即可.这个接口拥有如下方法:
- delete删除或批量删除
- findOne查找单个
- findAll查找所有
- save保存单个或批量保存
- saveAndFlush保存并刷新到数据库
知道这些我们就可以创建repository 了:
//User表示该Repository与实体User关联,主键类型为Integer
public interface UserRepository extends JpaRepository<User,Integer> {
}
这样就完成了一个基本Repository的创建,可以直接使用其中的方法,而不需要去写实现类。
4、测试
关于测试这里,我把测试案例写到test文件夹的话,总是报实体类未被JPA管理,所以改写到java文件夹,具体原因未知.
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
UserRepository userRepository = (UserRepository) applicationContext.getBean("userRepository");
System.out.println(userRepository.findAll());
System.out.println(userRepository.findOne(1));
System.out.println(userRepository.findAll(new Sort(new Sort.Order(Sort.Direction.ASC,"id"))));
}
三、CRUD操作
1、增加
增加可以使用JpaRepository接口里面的save方法.查看源码可以发现实际上是使用了em.persist(entity)来使对象进入持久化状态,最后提交事务的时候再一起更新到数据库:
User user = new User();
user.setId(99);
user.setAddress("上海");
user.setName("张三");
user.setPhone("110");
//保存单个
userRepository.save(user);
//保存或更新
userRepository.saveAndFlush(user);
List<User> users = new ArrayList<>();
users.add(user);
//保存多个
userRepository.save(users);
这里还可以批量插入,对于mysql支持INSERT user VALUES (20,'王二','111','111'),(21,'王二','111','111');类似这样的sql语句,具体实现就需要自己去写实现了,这样可以一次插入多条记录,效率很高.至于一次插入多少条就可以根据你的业务量来自己制定。
2、删除
删除都是根据主键来删除的,区别就是多条sql和单条sql :
User user = new User();
user.setId(21);
user.setName("王二");
/**
* 删除都是根据主键删除
*/
//删除单条,根据主键值
userRepository.delete(20);
//删除全部,先findALL查找出来,再一条一条删除,最后提交事务
userRepository.deleteAll();
//删除全部,一条sql
userRepository.deleteAllInBatch();
List<User> users = new ArrayList<>();
users.add(user);
//删除集合,一条一条删除
userRepository.delete(users);
//删除集合,一条sql,拼接or语句 如 id=1 or id=2
userRepository.deleteInBatch(users);
3、修改
修改也是根据主键来更新的 :
User user = new User();
user.setId(1);
user.setName("张三");
/**
* 更新也是根据主键来更新 update XX xx where id=1
*/
userRepository.saveAndFlush(user);
批量更新的话,就调用entityManager的merge函数来更新.
//首先在service层获取持久化管理器:
@PersistenceContext
private EntityManager em;
//批量更新方法,同理插入,删除也都可以如此做.
@Transactional
public void batchUpateCustom(List<User> users) {
// TODO Auto-generated method stub
for(int i = 0; i < users.size(); i++) {
em.merge(users.get(i));
if(i % 30== 0) {
em.flush();
em.clear();
}
}
}
4、简单查询
单表查询,大部分都可以使用下面的方法解决,多表联合查询的话,下面方法就不是很实用,下一节分析多表查询。
1.使用JpaRepository方法
//查找全部
userRepository.findAll();
//分页查询全部,返回封装了分页信息
Page<User> pageInfo = userRepository.findAll(new PageRequest(1, 3, Sort.Direction.ASC,"id"));
//查找全部,并排序
userRepository.findAll(new Sort(new Sort.Order(Sort.Direction.ASC,"id")));
User user = new User();
user.setName("小红");
//条件查询,可以联合分页,排序
userRepository.findAll(Example.of(user));
//查询单个
userRepository.findOne(1);
2.解析方法名创建查询
规则:find+全局修饰+By+实体的属性名称+限定词+连接词+ …(其它实体属性)+OrderBy+排序属性+排序方向。例如:
//分页查询出符合姓名的记录,同理Sort也可以直接加上
public List<User> findByName(String name, Pageable pageable);
全局修饰: Distinct, Top, First 关键词: IsNull, IsNotNull, Like, NotLike, Containing, In, NotIn, IgnoreCase, Between, Equals, LessThan, GreaterThan, After, Before… 排序方向: Asc, Desc 连接词: And, Or And — 等价于 SQL 中的 and 关键字,比如 findByUsernameAndPassword(String user, Striang pwd); Or — 等价于 SQL 中的 or 关键字,比如 findByUsernameOrAddress(String user, String addr); Between — 等价于 SQL 中的 between 关键字,比如 findBySalaryBetween(int max, int min); LessThan — 等价于 SQL 中的 “<”,比如 findBySalaryLessThan(int max); GreaterThan — 等价于 SQL 中的”>”,比如 findBySalaryGreaterThan(int min); IsNull — 等价于 SQL 中的 “is null”,比如 findByUsernameIsNull(); IsNotNull — 等价于 SQL 中的 “is not null”,比如 findByUsernameIsNotNull(); NotNull — 与 IsNotNull 等价; Like — 等价于 SQL 中的 “like”,比如 findByUsernameLike(String user); NotLike — 等价于 SQL 中的 “not like”,比如 findByUsernameNotLike(String user); OrderBy — 等价于 SQL 中的 “order by”,比如 findByUsernameOrderBySalaryAsc(String user); Not — 等价于 SQL 中的 “! =”,比如 findByUsernameNot(String user); In — 等价于 SQL 中的 “in”,比如 findByUsernameIn(Collection userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数; NotIn — 等价于 SQL 中的 “not in”,比如 findByUsernameNotIn(Collection userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数; |
嵌套实体:
主实体中子实体的名称+ _ +子实体的属性名称 List findByAddress_ZipCode(ZipCode zipCode) 表示查询所有 Address(地址)的zipCode(邮编)为指定值的所有Person(人员) |
3.JPQL查询
一个类似HQL的语法,在接口上使用@Query标识:
@Query("select a from user a where a.id = ?1")
public User findById(Long id);
使用@Modifying标识修改
@Modifying
@Query("update User a set a.name = ?1 where a.id < ?2")
public int updateName(String name, Long id);
携带分页信息:
@Query("select u from User u where u.name=?1")
public List<User> findByName(String name, Pageable pageable);
除此之外也可以使用原生sql,只需要@Query(nativeQuery=true)标识即可。
创建查询顺序:
Spring Data JPA 在为接口创建代理对象时,如果发现同时存在多种上述情况可用,它该优先采用哪种策略呢?为此, 提供了 query-lookup-strategy 属性,用以指定查找的顺序。它有如下三个取值:
|
4.计数
计数就直接使用JpaRepository的count方法:
//查找总数量
userRepository.count();
User user = new User();
user.setName("小红");
//条件计数
userRepository.count(Example.of(user));
5.判断是否存在
计数就直接使用JpaRepository的exists方法:
//根据主键判断是否存在
userRepository.exists(1);
User user = new User();
user.setName("小红");
//根据条件判断是否存在
userRepository.exists(Example.of(user));
6.自定义查询
首先自定义一个接口,用于定义自定义方法,如UserRepositoryCustom,然后让UserRepository实现该接口,这样的话就可以使用其中的方法。然后写UserRepositoryImpl实现UserRepositoryCustom接口。
最后设置jpa:repositories的repository-impl-postfix="Impl",这样的话JPA会查找自定义实现类命名规则,这样的话JPA在相应UserRepository包下面查找实现类,找到则会使用其中的实现方法,而不去自己实现。具体可以看项目demo,或者下一节的复杂查询。
5、动态条件查询
1.使用Specification查询
首先在Dao中我们需要多继承一个JpaSpecificationExecutor接口,代码如下:
//请忽略DictionaryRepository,这只是我给自己的Dao层起的名字,主要是要实现JpaSpecificationExecutor这个接口
public interface DictionaryRepository extends JpaRepository<SchoolEntity,Long> ,JpaSpecificationExecutor<SchoolEntity> {
}
下面我们就来写我们Service层了
public RestResult list(DailyDictionaryListParams params, Pageable pageable) {
Specification<DailyDictionary> specification = new Specification<DailyDictionary>() {
@Override
public Predicate toPredicate(Root<DailyDictionary> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//创建一个条件的集合
List<Predicate> predicates = new ArrayList<Predicate>();
//发送日期:这里的createDate和实体属性对应,不是和数据库字段对应
if (params.getSendDate() != null){
predicates.add(cb.between(root.<Date>get("createDate"), DateUtil.getMinDayDateTime(params.getSendDate()), DateUtil.getMaxDayDateTime(params.getSendDate())));
}
//类目名称
if (!StringUtil.isEmptyOrNvl(params.getCategoryName())){
Join<DailyDictionary, ContentCategory> join = root.join("dictionary", JoinType.LEFT)
.join("category", JoinType.LEFT);
predicates.add(cb.like(join.get("category.name"), "%" + params.getCategoryName()));
}
//状态
if (!StringUtil.isEmptyOrNvl(params.getStatus())){
predicates.add(cb.equal(root.get("status"), params.getStatus()));
}
//创建一个条件的集合,长度为上面满足条件的个数
Predicate[] pre = new Predicate[predicates.size()];
//将上面拼接好的条件返回去
return query.where(predicates.toArray(pre)).getRestriction();
}
};
//这里我们按照返回来的条件进行查询,就能得到我们想要的结果
Page<DailyDictionary> page = dailyDictionaryRepository.findAll(specification, pageable);
return RestResult.ok(page);
}
2.对Specification查询进行封装
上面我们看到了,JPA中的动态SQL查询太复杂了,而动态查询使用是非常频繁的,这点的确是比mybatis差远了。这里对Specification查询进行了简单封装(基于CriteriaBuilder),来简化查询操作,代码已经放到码云上了(https://gitee.com/getoutshen/jpa-dao/tree/master)。
下面来介绍一下使用方式,其实很简单,构造好QueryParams就好了(所以查询字段都是JPQL字段,并不是原生sql)
public Page<User> list(UserListParams params, Pageable pageable) {
QueryParams<User> queryParams = new QueryParams<>();
queryParams
.and(
Filter.between("createDate", params.getBeginDate(), params.getEndDate()),
Filter.eq("mobile", false, params.getMobile()),//默认启用动态SQL,如果不需要动态SQL,设置为false
Filter.like("nickName", params.getNickName()),
Filter.eq("mail", params.getMail())//比较的可以是对象
).or(
Filter.eq("status", "0"),
Filter.eq("isDeleted", "1")
)
.orderDESC("createDate");
Page<User> page = userRepository.findAll(queryParams, pageable);
}
四、项目实战
1、模型创建
@Entity//表面是一个实体
@Data//lombok 插件,免得写setget方法
@Table(name = "t_user")//对于数据库t_user表
public class User {
//主键
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY) // 自增长策略
private Long id; // 用户的唯一标识
//创建时间
@NotNull
@Column(nullable = false, updatable = false)
@ApiModelProperty("创建时间")
private Date createDate;
//创建人
@NotNull
@ApiModelProperty("创建人Id")
private Long createdUserId;
//姓名
@NotEmpty(message = "姓名不能为空")
@Size(min=2, max=20)
@Column(length = 20)
private String name;
//昵称
@Column(length = 50)
private String nickName;
//用户名
@NotEmpty(message = "用户名不能为空")
@Size(min=3, max=20)
@Column(nullable = false, length = 50, unique = true)//不为空,唯一
private String username;
//登录密码
@NotEmpty(message = "密码不能为空")
@Size(max = 100)
private String password;
//状态
@Column(length = 2)
@ApiModelProperty("状态【0:可用;1:不可用】")
private String status;
}
上面一个简单的User模型就创建好了,但是我们知道实际上在模型类设计的时候往往很多模型类都有相同的属性。比如我们这里的id,创建时间,创建人等等,一般每个模型类都有这些属性,所以这里我们在来一个封装,封装个BaseModel来。
@Data
@MappedSuperclass//不被映射到表
@EntityListeners(EntityListener.class)//下面讲
public abstract class BaseVO implements Serializable {
private static final long serialVersionUID = 2828418936767521019L;
//主键id
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ApiModelProperty("主键Id")
private Long id;
//创建时间
@Column
@ApiModelProperty("创建时间")
private Date createDate;
//创建人
@Column
@ApiModelProperty("创建人Id")
private Long createdUserId;
//更改时间
@Column
@ApiModelProperty("更新时间")
private Date updateDate;
//更改人
@Column
@ApiModelProperty("更新人Id")
private Long updateUserId;
//删除状态
@Column
@ApiModelProperty(hidden = true)
private DeleteStatus isDeleted;
}
相同的属性被封装好了,但是每次更改或者新增记录的时候,对于上面这些创建人,创建时间等属性来讲,我们都要手动填充一下,如果可以自行填充,那就更好了。我们上面也看到了@EntityListeners注解。它就是jpa提供给我们的对实体属性变化的跟踪的监听器,它提供了保存前,保存后,更新前,更新后,删除前,删除后等状态,就像是拦截器一样,你可以在拦截方法里重写你的个性化逻辑。
回过头来,再看看我们的EntityListener实现:
public class EntityListener {
/*
* 功能描述:保存前处理
* 创建时间:2018/10/12 18:05
* 入参:[entity 基类]
* 返回值:void
*/
@PrePersist
public void prePersist(BaseVO entity) {
//这里我用了spring security安全框架,所以使用SecurityContextHolder来获取用户,可以根据自己的项目执行修改
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null){
Object principal = authentication.getPrincipal();
if (principal instanceof TinUserDetails){
Long userId = ((TinUserDetails) principal).getId();
// 信息需要根据需要写入真实信息
entity.setCreatedUserId(userId);
entity.setUpdateUserId(userId);
}else {
entity.setCreatedUserId(new Long(0));
entity.setUpdateUserId(new Long(0));
}
}else {
entity.setCreatedUserId(new Long(0));
entity.setUpdateUserId(new Long(0));
}
// 设置创建时间更新时间
entity.setCreateDate(DateUtil.getSystemTime());
entity.setUpdateDate(DateUtil.getSystemTime());
entity.setIsDeleted(DeleteStatus.NO);
}
/*
* 功能描述:更新前处理
* 创建时间:2018/10/12 18:05
* 入参:[entity 基类]
* 返回值:void
*/
@PreUpdate
public void preUpdate(BaseVO entity) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
if (principal instanceof TinUserDetails){
Long userId = ((TinUserDetails) principal).getId();
// 信息需要根据需要写入真实信息
entity.setUpdateUserId(userId);
}else {
entity.setUpdateUserId(new Long(0));
}
entity.setUpdateDate(DateUtil.getSystemTime());
}
}
这样在实体更新时,对于这些属性来讲我们就可以不再关心了。
2、持久化层
直接继承JpaRepository和JpaSpecificationExecutor接口
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User>{
}
不过在写的时候,Jpa**这两个接口并不好记,那就在抽取出来一个父接口吧
@NoRepositoryBean//表示不是Repository对象
public interface BaseRepository<T> extends JpaRepository<T, Long>, JpaSpecificationExecutor<T> {
}
//UserRepository继承BaseRepository
@Repository
public interface UserRepository extends BaseRepository<User> {
}
3、控制层
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserRepository userRepository;//service就省了
/**
* 查询所用用户
*/
@GetMapping
public ModelAndView list(Model model) {
model.addAttribute("userList", userRepository.findAll());
model.addAttribute("title", "用户管理");
return new ModelAndView("users/list", "userModel", model);
}
/**
* 根据id查询用户
*/
@GetMapping("{id}")
public ModelAndView view(@PathVariable("id") Long id, Model model) {
User user = userRepository.findOne(id);
model.addAttribute("user", user);
model.addAttribute("title", "查看用户");
return new ModelAndView("users/view", "userModel", model);
}
/**
* 新建用户
*/
@PostMapping
public ModelAndView create(User user) {
userRepository.save(user);
return new ModelAndView("redirect:/users");
}
/**
* 删除用户
*/
@DeleteMapping("{id}")
public ModelAndView delete(@PathVariable("id") Long id, Model model) {
userRepository.delete(id);
model.addAttribute("title", "删除用户");
return new ModelAndView("users/list", "userModel", model);
}
/**
* 修改用户
*/
@PutMapping
public ModelAndView modifyForm(User user, Model model) {
userRepository.save(user);
model.addAttribute("user", user);
model.addAttribute("title", "修改用户");
return new ModelAndView("users/form", "userModel", model);
}
}