JPA
版本:
spring-boot-starter-data-jpa-2.7.2
[2022-08-09 18:18:03]
前言:
作为 MyBatis 的重度用户,当新项目用到新的
JPA
的持久化层技术时,完全没接触过而又没有拿来即用的参考文档时怎么办呢?下面的文章应该会对你有帮助注:以下出现的单测均通过上级验证(建表语句见文末)
熟悉
MyBatis
的知道,MyBatis
是强大的,它是一款优秀的 ORM 框架,支持原生的 SQL 语句等等
JPA
听说过最出名的就是“不用写 sql 语句”,那么它是怎么做到的。回顾起 mapper 文件,会自然想到,那它是怎么实现 ① mapper 的实体类和基类的一一映射关系、② Bean属性和表字段之间的映射、查询方法的…
跟着这个框架走可以帮助我们更快速地认识
JPA
的语法
1.实体类
过去需要继承一个基类,但是不适合测试,现在利用注解@Entity
代替了
2.对象关系映射
可以像 MyBatis
一样用专门的配置文件 xml 配置,也可以用注解的方法配置 [从字段到属性] 之间的映射关系,也就是 jpa 中的 @Column
3.其它属性描述
注解@Id
和@GeneratedValue
是告诉JPA主键和自动递增的信息,既可以加在get方法上也可以加在字段上,建议加在字段上
依赖:
spring-boot-starter-data-jpa
com.h2database.h2 # (创建基于内存的数据库/测试用)
lombok # 缩写setter/getter用
1.取出查询实体
repository.findById(1).get()
从Optional<T>
里取出实体类
Optional<ProductCategory> byId = repository.findById(1);
ProductCategory productCategory = repository.findById(1).get();
2.正确的修改姿势
先查出来,然后对结果按需设置,相当于MyBatis
的按需更新。
// [2022-08-05 10:02:05]
@Test
public void testUpdate() {
ProductCategory productCategory = repository.findById(1).get();
productCategory.setCategoryName("热销磅单///");
repository.save(productCategory);
}
附:实体类的元素
以一张商品类别表举例
@Data
@AllArgsConstructor // 构造对象用
@NoArgsConstructor // 构造对象用
@Entity // JPA
@Table(name="product_category") // 绑定表,也方便查看映射表(非必填)如果命名严格按照驼峰的话
public class ProductCategory implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY) // 自动递增的字段
@Column(name="category_id") // (非必填)如果命名严格按照驼峰的话
private Integer categoryId;
@Column(name="category_type")
private Integer categoryType;
/**
* 类目名字
*/
@Column(name="category_name") // (非必填)
private String categoryName;
/**
* create_time
*/
@Column(name="create_time") // (非必填)
private Date createTime;
/**
* update_time
*/
@Column(name="update_time") // (非必填)
private Date updateTime;
}
3.常用方法
1.count()
@Test
public void testCount() {
long count = repository.count();
}
2.existById()
@Test
public void testExistById() {
boolean b = repository.existsById(2);
}
3.saveAll()
@Test
public void testSaveAll() {
List<ProductCategory> list = mockList();
repository.saveAll(list);
List<ProductCategory> all = repository.findAll();
all.forEach(System.out::println);
}
4.mybatis没有的findAllById(List)
@Test
public void testFindAllById() {
Integer[] ints = {1,3,4,5,6,7};
List<Integer> list = new ArrayList<>(Arrays.asList(ints));
List<ProductCategory> allById = repository.findAllById(list);
allById.forEach(System.out::println);
}
4.+非主键字段的in查询
主键字段:category_id
- findAllById()
非主键:category_type
也有对应的智能匹配生成
repository
/**
* 非主键版 findAllById()
*
* @param typeIdList
* @return java.util.List<com.imooc.domain.ProductCategory>
*/
List<ProductCategory> findByCategoryTypeIn(List<Integer> typeIdList);
Test
/**
* 测试根据非主键字段的 in 查询
*
*/
@Test
public void testFindByCategoryTypeIn() {
Integer[] ints = {1, 10};
List<ProductCategory> byCategoryIn = repository.findByCategoryTypeIn(Arrays.asList(ints));
System.out.println(byCategoryIn);
}
1.按照findByFieldNameBy(List<fieldType> list)
的规则定义抽象方法
2.使用
相当于 MyBatis
的
<!--item: 元素代词-->
<select id="selectUserByPwdErrIn" parameterType="list" resultMap="UserResult">
select * from t_user
<where>
<if test="timeList != null and timeList.size() > 1">
pwd_err in
<foreach collection="timeList" item="item" open="(" separator="," close=")">
#{item, jdbcType=VARCHAR}
</foreach>
</if>
</where>
</select>
5.Sort.by()
排序
@Test
public void testSort() {
// 1.使用Sort的静态方法by() 定义Sort对象
Sort orders = Sort.by(Sort.Direction.DESC, "categoryId"); // 第一个参数是Direction有ASC和DESC两个值
// 2.传入findAll()
List<ProductCategory> all = repository.findAll(orders);
System.out.println("测试jpa排序:");
all.forEach(System.out::println);
}
6.1.智能字段匹配
条件字段在方法名里体现和参数列表
// 1.定义方法时方法名和参数列表对应统一(Repository)
/**
* 根据商品类别查询
*
* @param categoryName
* @return java.util.List<com.imooc.domain.ProductCategory>
*/
List<ProductCategory> findByCategoryName(String categoryName);
// 2.使用
@Test
public void testFindByCategoryName() {
List<ProductCategory> byCategoryName = repository.findByCategoryName("女生最爱");
byCategoryName.forEach(System.out::println);
}
2
/**
* 根据商品类别查询
*
* @param categoryName
* @param categoryType
* @return java.util.List<com.imooc.domain.ProductCategory>
*/
List<ProductCategory> findByCategoryNameAndCategoryType(String categoryName, Integer categoryType);
@Test
public void testFindByCategoryName() {
List<ProductCategory> byCategoryName = repository.findByCategoryNameAndCategoryType("女生最爱", 2);
byCategoryName.forEach(System.out::println);
}
6.2.原生字段匹配sql
/**
* 原生写法的字段匹配
*
* @param categoryName
* @return com.imooc.domain.ProductCategory
*/
@Query(value="select * from product_category where category_name = ?1", nativeQuery = true)
ProductCategory findByCategoryName(String categoryName);
@Test
public void testFindByCategoryType() {
ProductCategory byCategoryName = repository.findByCategoryName("热销磅单///");
System.out.println(byCategoryName);
}
参考资料:
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
Example 63. Declare a native query at the query method using @Query
7.条件查询@Query
注解@Query
自定义sql语句,nativeQuery = true
表示使用原生sq
① ?索引的写法
@Query(value = "select * from product_category where " +
"if(?1 is not null, category_id=?1,1=1) and " +
"if(?2 is not null, category_type=?2,1=1) and " +
"if(?3 != '' and category_name is not null, category_name like concat('%',?3, '%'), 1=1) and " +
"if(?4 is not null, create_time=?4, 1=1) and " +
"if (?5 is not null,update_time=?5,1=1) " +
"order by category_id desc", nativeQuery = true)
List<ProductCategory> find(Integer categoryId, Integer categoryType, String categoryName, Date createTime, Date updateTime);
@Test
public void testSelectIndex() {
List<ProductCategory> categoryList = repository.find(null, null, "女生", null, null);
categoryList.forEach(System.out::println);
}
参考资料:SpringBoot | JPA基本查询及多条件查询,参数为空判断_Xyu_a的博客-CSDN博客_jpa参数为空
② :属性写法
参数必须列全,逐个标记注解
@Query(value = "select * from product_category where " +
"if(:categoryId is not null, category_id=:categoryId,1=1) and " +
"if(:categoryType is not null, category_type=:categoryType,1=1) and " +
"if(:categoryName != '' and category_name is not null, category_name like concat('%',:categoryName, '%'), 1=1) and " +
"if(:createTime is not null, create_time=:createTime, 1=1) and " +
"if (:updateTime is not null,update_time=:updateTime,1=1) " +
"order by category_id desc", nativeQuery = true)
List<ProductCategory> find2(@Param("categoryId")Integer categoryId,
@Param("categoryType")Integer categoryType,
@Param("categoryName")String categoryName,
@Param("createTime")Date createTime,
@Param("updateTime")Date updateTime);
@Test
public void testFind2() {
List<ProductCategory> categoryList = repository.find2(null, null, "女生", null, null);
categoryList.forEach(System.out::println);
}
参考资料:
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
Example 66. Using named parameters
8.1.条件删除
int deleteByCategoryId(Integer categoryId);
@Test
public void testdeleteByCategoryId() {
repository.deleteByCategoryId(10);
}
8.1.条件删除原生sql
注解依旧是@Query
,加上注解@Modifying
@Modifying
@Query(value = "delete from product_category where category_id = ?1", nativeQuery=true)
int deleteByXX(Integer categoryId);
@Test
public void testdeleteByXX() {
int i = repository.deleteByXX(9);
}
9.原生sql更新(save)
save方法必须先查询后save才能实现数据的更新
@Modifying
@Query(value = "update product_category set category_type=?1, category_name=?2, create_time=?3, update_time=?4 where category_id=?5", nativeQuery=true)
int update(Integer categoryType,
String categoryName,
Date createTime,
Date updateTime,
Integer categoryId);
@Test
public void testupdate() {
int i = repository.update(80, "电器", new Date(), new Date(), 8);
}
10.分页
@Test
public void testFindAll() {
// 构造Pagable对象
Pageable pageable = PageRequest.of(0, 5);
// 传入参数
Page<ProductCategory> all1 = repository.findAll(pageable);
System.out.println("分页结果:");
all1.forEach(System.out::println);
}
参考资料:spring data jpa分页5种方法_boss达人的博客-CSDN博客_jpa 分页
4.注解@DynamicUpdate
默认情况下,JPA是全字段插入的
Hibernate: insert into product_category (category_name, category_type, create_time, update_time)
values (?, ?, ?, ?)
[2022-08-09 16:04:23] 默认调用save()
此时如果检测不能为空字段没有设值时就会触发UnexpectedRollbackException
此时在实体类上打上注解@DynamicUpdate
,则会动态更新,根据entity已有的值判空进行写入。
此时,如果建表时设置了
on update CURRENT_TIMESTAMP
或者自增的主键的规则,记录写入时也会自动触发MySQL
的定义规则。
/**
* 测试 @DynamicUpdate
* 只修改其中一个字段,不设置更新时间
* */
@Test
public void testUpdate() {
ProductCategory productCategory = repository.findById(1).get();
productCategory.setCategoryName("热销磅单qaaa");
repository.save(productCategory);
}
//
import org.hibernate.annotations.DynamicUpdate;
@Data
@Entity // JPA
@Table(name="product_category")
@DynamicUpdate // 使用MySQL的自动更新
public class ProductCategory implements Serializable {
}
结果日志
Hibernate: update product_category set category_name=? where category_id=?
同理可得,@DynamicInsert
动态取值插入
/**
* 测试 @DynamicInsert
* 不设主键(auto_increment)
* 不设创建时间(default CURRENT_TIMESTAMP not null)
* */
@Test
public void testSave1() {
ProductCategory category = new ProductCategory();
System.out.println("插入ing");
// category.setCategoryId(16);
category.setCategoryType(1);
category.setCategoryName("热销磅单3.0");
// category.setCreateTime(new Date());
category.setUpdateTime(new Date());
ProductCategory saved = repository.save(category);
Assertions.assertNotNull(saved);
System.out.println("实际被插入的数据:" + saved);
}
@Table(name="product_category")
@DynamicInsert // 没有值的时候使用动态插入
public class ProductCategory implements Serializable {
//...
}
日志:
插入ing
Hibernate: insert into product_category (category_name, category_type, update_time) values (?, ?, ?)
实际被插入的数据:ProductCategory(categoryId=21, categoryType=1, categoryName=热销磅单3.0, createTime=null, updateTime=Tue Aug 09 17:20:34 CST 2022)
执行时的日志中只选择性插入 3 个字段
具体实现了 MyBatis 里的哪种写法不得而知。
现象:为什么同时注解DynamicInsert
和DynamicUpdate
没有起到插入时创建时间和更新时间都不用另外设值的效果
[2022-08-09 16:26:53] 两个都有注解,但是插入新纪录时,只设置(category_name, category_type)
两个字段时,结果没有触发自动更新机制(update_time
有设置on update CURRENT_TIMESTAMP
)
原因:
原product_category
的DDL
:
create table product_category
(
category_id int auto_increment
primary key,
category_name varchar(64) not null comment '类目名字',
category_type int not null comment '类目编号(唯一性索引)',
create_time timestamp default CURRENT_TIMESTAMP not null,
update_time timestamp not null on update CURRENT_TIMESTAMP
)
comment '商品类目表';
create index product_category_category_type_uindex
on product_category (category_type);
原update_time
与上面的create_time
中间空了一大段,所以并没有设置默认值 default CURRENT_TIMESTAMP
的规则,只有定义更新时触发的规则。
所以 sql 语句在 insert into product_category (category_name, category_type) values (?, ?)
时放数据库IDE里也执行不通,JPA 只能帮助执行原本能在数据库正常运行的 sql。
借此说明,DynamicInsert
和DynamicUpdate
是各自为了动态插入和动态更新时服务的,加在一起并没有有什么加成的功效。
插入新纪录是,没有默认值、自增的 not null
字段该设值还是乖乖设值。
5.不写入数据库
当我们测试时不想实际写入数据库时,这也是单元测试的理想效果,只测试逻辑代码,不对实际测试/生产数据造成改动。
springboot
原本就是将测试类设置为自动回滚的,所以默认情况下单测通过,数据库也不会发生实际的变化。
将上诉单测中的测试类头上的@Rollback(value = false)
注释即可
附:建表语句
create table product_category
(
category_id int auto_increment
primary key,
category_name varchar(64) not null comment '类目名字',
category_type int not null comment '类目编号(唯一性索引)',
create_time timestamp default CURRENT_TIMESTAMP not null,
update_time timestamp not null on update CURRENT_TIMESTAMP
)
comment '商品类目表';
create index product_category_category_type_uindex
on product_category (category_type);
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (1, '热销磅单qaaa', 1, '2022-08-07 14:27:33', '2022-08-09 16:23:32');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (2, '女生最爱', 2, '2022-08-05 01:07:58', '2022-08-05 01:07:58');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (3, '类型4', 4, '2022-08-05 02:56:47', '2022-08-05 02:56:47');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (4, '类型5', 5, '2022-08-05 02:56:47', '2022-08-05 02:56:47');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (5, '类型6', 6, '2022-08-05 02:56:47', '2022-08-05 02:56:47');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (6, '类型7', 7, '2022-08-05 02:56:47', '2022-08-05 02:56:47');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (7, '类型8', 8, '2022-08-05 02:56:47', '2022-08-05 02:56:47');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (8, '电器', 80, '2022-08-07 14:27:33', '2022-08-07 14:27:33');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (13, '类型10', 10, '2022-08-07 14:10:14', '2022-08-07 14:10:14');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (14, '类型11', 11, '2022-08-07 14:10:14', '2022-08-07 14:10:14');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (17, '热销磅单2.0', 1, '2022-08-09 15:55:35', '2022-08-09 07:55:35');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (18, '热销磅单3.0', 1, '2022-08-09 16:12:31', '2022-08-09 08:12:32');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (19, '热销磅单3.0', 1, '2022-08-09 17:18:32', '2022-08-09 09:18:32');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (20, '热销磅单3.0', 1, '2022-08-09 17:19:06', '2022-08-09 09:19:06');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (21, '热销磅单3.0', 1, '2022-08-09 17:20:34', '2022-08-09 09:20:34');