【DB系列】JPA之update使用姿势

文章目录
  1. I. 环境准备
    1. 1. 表准备
    2. 2. 项目配置
    3. 3. 数据准备
  2. II. Update使用教程
    1. 1. 表关联POJO
    2. 2. Repository API声明
    3. 3. 使用姿势
      1. a. save
      2. b. 查询更新
    4. 4. 小结
  3. II. 其他
    1. 0. 源码与相关博文
    2. 1. 一灰灰Blog

上面两篇博文拉开了jpa使用姿势的面纱一角,接下来我们继续往下扯,数据插入db之后,并不是说就一层不变了,就好比我在银行开了户,当然是准备往里面存钱了,有存就有取(特别是当下银行利率这么低还不如买比特币屯着,截止19年6月22日,btc已经突破1.1w$,可惜没钱买😭)这就是我们今天的主题,数据更新–update的使用姿势

通过本篇博文,您至少可以选到

  • save() 直接根据id来修改记录
  • 利用jpl 实现查询修改的使用姿势
  • 初识事物的神秘面纱

I. 环境准备

在开始之前,当然得先准备好基础环境,如安装测试使用mysql,创建SpringBoot项目工程,设置好配置信息等,关于搭建项目的详情可以参考前一篇文章

下面简单的看一下演示添加记录的过程中,需要的配置

1. 表准备

沿用前一篇的表,结构如下

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 项目配置

配置信息,与之前有一点点区别,我们新增了更详细的日志打印;本篇主要目标集中在添加记录的使用姿势,对于配置说明,后面单独进行说明

1
2
3
4
5
6
7
8
9
10
11
## DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=
## jpa相关配置
spring.jpa.database=MYSQL
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
spring.jackson.serialization.indent_output=true
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

3. 数据准备

数据修改嘛,所以我们先向表里面插入两条数据,用于后面的操作

1
2
3
4
INSERT INTO `money` (`id`, `name`, `money`, `is_deleted`, `create_at`, `update_at`)
VALUES
(21, 'jpa 修改->一灰灰', 1212, 0, '2019-06-22 21:41:13', '2019-06-22 21:41:13'),
(22, 'jpa 修改->一灰灰', 6666, 0, '2019-06-22 21:41:13', '2019-06-22 21:41:13');

II. Update使用教程

下面开始进入正题,为方便初看的同学(没空或者没兴趣瞅前面几个博文的同学)会有部分内容和前面的博文相同,看过的请无视

1. 表关联POJO

前面插入篇已经介绍了POJO的逐步创建过程,已经对应的注解含义,下面直接贴出成果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Data
@DynamicInsert
@Entity
@Table(name = "money")
public class MoneyPO {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")

private Integer id;

@Column(name = "name")
private String name;

@Column(name = "money")
private Long money;

@Column(name = "is_deleted")
private Byte isDeleted;

@Column(name = "create_at")
@CreatedDate
private Timestamp createAt;

@Column(name = "update_at")
@CreatedDate
private Timestamp updateAt;
}

上面类中的几个注解,说明如下

  • @Data 属于lombok注解,与jpa无关,自动生成getter/setter/equals/hashcode/tostring等方法
  • @Entity, @Table jpa注解,表示这个类与db的表关联,具体匹配的是表 money
  • @Id @GeneratedValue 作用与自增主键
  • @Column表明这个属性与表中的某列对应
  • @CreateDate根据当前时间来生成默认的时间戳

2. Repository API声明

接下来我们新建一个api继承自CurdRepository,然后通过这个api来与数据库打交道

1
2
public interface MoneyUpdateRepository extends CrudRepository<MoneyPO, Integer> {
}

3. 使用姿势

a. save

在前面一篇插入博文中,我们知道当POJO的id存在时,调用save方法可能有两种情况

  • 若db中这个id对应的字段不存在,则插入
  • 若db中这个id对应的字段存在,则更新

我们来试一下更新的效果,下面的代码演示了两块,一个是当po中的所有成员值有效,更新其中的一个时,会怎样;另外一个演示的是部分更新时会怎样(name为空,表示我不希望更新name)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void simpleUpdateById() {
MoneyPO record = moneyUpdateRepository.findById(21).get();
// 直接修改这个record的内容
record.setMoney(3333L);
moneyUpdateRepository.save(record);

record = moneyUpdateRepository.findById(21).get();
System.out.println("after updateMoney record: " + record);


record.setName(null);
record.setMoney(6666L);
moneyUpdateRepository.save(record);

record = moneyUpdateRepository.findById(21).get();
System.out.println("after updateMoney record: " + record);
}

输出结果发现,前面执行成功,后面执行失败

上面为第一个执行结果,从拼接的sql可以知道,是全量的修改;输出结果也如我们预期

后面将name设置为空之后,再次更新,发现抛出异常,如下,这个是因为我们的db限制,字段不允许有null的存在

从拼接的sql上看,我们知道是因为每个成员都作为了update sql家族的一员,在insert这一篇中我们也遇到了类似的问题,当时是在POJO上添加注解@DynamicInsert,根据实际需要选择插入,那么在更新这里是不是也有类似的注解呢

1
2
3
4
5
6
7
@Data
@DynamicUpdate
@DynamicInsert
@Entity
@Table(name = "money")
public class MoneyPO {
}

在pojo上添加注解@DynamicUpdate之后,再次进行尝试,结果如下

居然还是失败了,从输出的sql来看,居然把namemoney都当成了sql的一部分,难道是因为我们调用了setter方法的原因么。待着猜测,再来一次

1
2
3
4
5
6
MoneyPO toUpdate = new MoneyPO();
toUpdate.setId(21);
toUpdate.setMoney(6666L);
moneyUpdateRepository.save(toUpdate);
record = moneyUpdateRepository.findById(21).get();
System.out.println("after updateMoney record: " + record);

输出结果如下,看来我们上面的猜测并不对,拼接sql应该是根据哪个字段发生了变化,就把哪个做为sql的一部分来做的

上面这个使用姿势看完之后,会有一个明显的感觉,这个更新的支持,必须先获取目标对象再进行修改,很难满足我们的日常业务场景;

b. 查询更新

根据某个条件来更新对应的数据,这个就比较常见了,在jpa中,没有找到根据方法名来支撑这种场景的方式,但是发现了另外一个有意思的东西–jql

直接在方法方面,添加注解,注解内部写sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 根据金钱来修改状态
*
* @param money
* @param state
*/
@Modifying
@Query("update MoneyPO m set m.isDeleted=?2 where m.money=?1")
void updateStateByMoney(Long money, Byte state);

/**
* 表达式计算
*
* @param id
* @param money
*/
@Modifying
@Query("update MoneyPO m set m.money=m.money + ?2 where m.id=?1")
void addMoneyById(Integer id, Long money);

上面就是一个查询更新的case,注意两个注解

  • @Modifying 这个必须有,告诉框架我们执行的是更新/删除操作
  • @Query 内部是正常的sql语句,但是需要注意的是表名,不是实际的表,而是我们前面定义的POJO

然后来测试一下使用

1
2
3
4
5
6
7
8
9
10
11
12
public void updateByQuery() {
// 通过查询修改
moneyUpdateRepository.updateStateByMoney(6666L, (byte) 0x01);

MoneyPO record = moneyUpdateRepository.findById(21).get();
System.out.println("after update record: " + record);


moneyUpdateRepository.addMoneyById(21, 3333L);
record = moneyUpdateRepository.findById(21).get();
System.out.println("after addMoney record: " + record);
}

执行上面的代码,悲催的发现居然报错了 Caused by: javax.persistence.TransactionRequiredException: Executing an update/delete query

从堆栈的描述上来看,更新/删除必须要开启事务,那么事务是什么东西呢?下面推荐几篇博文

关于jpa中更多事务相关的,后面再引入,我们先回到本文主题,如何解决问题: 在调用上面的方法上面添加事务注解即可

1
2
3
4
5
@Transactional
public void testUpdate() {
simpleUpdateById();
updateByQuery();
}

再次执行之后,结果如下

看上面的结果,发现money+3333之后的输出结果居然还是6666;但是我们再看db的结果,已经是9999了,为什么会这样呢?

上面这个问题比较有意思了,初步猜测与事物已经jpa的内部缓存机制有关系,至于具体是不是这样,有必要专门开坑来验证一二

4. 小结

利用JPA实现表数据的更新,上面主要介绍了两种方式, save + jql

save

通过save更新时,需要指定id来实现单条记录的修改

jql

语法与sql差不多,配合两个注解 @Modifying, @Query来使用,下面是一个实例,两点需要注意

  • 表名是我们定义的与db中表关联的POJO
  • 参数传递格式为?index, index为参数位置
1
2
3
@Modifying
@Query("update MoneyPO m set m.isDeleted=?2 where m.money=?1")
void updateStateByMoney(Long money, Byte state);

数据修改和删除,需要显示声明事务,否则会报错,一个是在调用的方法上添加注解 @Transactional,或者直接在repository api的接口上添加注解 @Transactional

II. 其他

0. 源码与相关博文

源码

相关博文

1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到