来自Craig Ringer关于这个主题的post:
我看到的SQL编码反模式很多:天真 读 - 修改 - 写周期。在这里,我将解释这是什么常见的 开发错误是,如何识别它,以及如何修复的选项 它
想象一下,您的代码想要查找用户的余额,从中减去100 它如果这样做不会使它消极,并保存它。
通常会将此内容写成三个步骤:
SELECT balance FROM accounts WHERE user_id = 1; -- in the application, subtract 100 from balance if it's above -- 100; and, where ? is the new balance: UPDATE accounts SET balance = ? WHERE user_id =1;
一切都会对开发人员起作用。然而, 这段代码严重错误,并且会尽快发生故障 同一用户同时由两个不同的会话更新。
交易不会阻止这种情况吗?
我经常有人在Stack Overflow上问道:“不要 交易阻止了这个?“不幸的是,虽然很棒,交易 不是你可以添加的神奇秘密酱,以方便并发。唯一的 让你完全忽略并发问题的方法是LOCK TABLE 在开始交易之前你可能使用的每个表(甚至是 那么你必须始终锁定相同的顺序以防止死锁。)
避免读取 - 修改 - 写入周期
最好的解决方案通常是在SQL中完成工作,避免使用 完全读 - 修改 - 写 - 循环。
只需写下:
UPDATE accounts SET balance = balance-100 WHERE user_id = 1; (sets balance=200)
当我使用Spring Data修改我的实体时,我发现自己始终处于读 - 修改 - 写模式。这是一个示例实体:
@Entity
public class Customer {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
protected Customer() {}
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format(
"Customer[id=%d, firstName='%s', lastName='%s']",
id, firstName, lastName);
}
/** GETTERS AND SETTERS */
}
存储库:
public interface CustomerRepository extends CrudRepository<Customer, Long> {
Customer findByLastName(String lastName);
}
应用程序逻辑:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
@Bean
public CommandLineRunner demo(CustomerRepository repository) {
return (args) -> {
// save a couple of customers
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
Customer customer = repository.findByLastName("Bauer");
customer.setFirstName("kek");
repository.save(customer);
};
}
}
但是,在这里我们看到执行了read-modify-write反模式。如果我们的目标是避免这种反模式,编写代码的不同方式是什么?到目前为止,我提出的解决方案是向存储库添加修改查询并使用它进行修改。因此,对于我们的CustomerRepository
,我们添加以下方法:
@Query(nativeQuery = true, value = "update customer set first_name = :firstName where id= :id")
@Modifying
void updateFirstName(@Param("id") long id, @Param("firstName") String firstName);
在我们的应用程序中,逻辑变为:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
@Bean
public CommandLineRunner demo(CustomerRepository repository) {
return (args) -> {
// save a couple of customers
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
Customer customer = repository.findByLastName("Bauer");
repository.updateFirstName(customer.getId(), "kek");
};
}
}
这完全可以避免读取 - 修改 - 写入反模式,但是对于想要修改实体属性的每个案例,将更新方法写入存储库会非常繁琐。在Spring Data中没有更好的方法吗?
答案 0 :(得分:1)
这完全可以避免读取 - 修改 - 写入反模式,但是对于想要修改实体属性的每一个案例,将更新方法写入存储库会非常繁琐。在Spring Data中没有更好的方法吗?
TL; DR:不,这是做到这一点的方式。
更长版本
JPA建立在这种方法的基础上:
将数据加载到内存
以任何您想要的方式操纵
将生成的数据结构保存回数据库。
但它内置了一个保护:乐观锁定。 JPA和Spring Data JPA将抛出一个异常,并在保存的行自加载后更改时回滚事务,假设您有一个版本列,从而启用了乐观锁定。
所以从一致性的角度来看,你很好。
当然,对于您所描述的更新(更新帐户余额)更新,这相当浪费,直接更新会更有效率。 @Modify
注释正是出于此目的。
另一方面,您用于注释的示例是幂等的,因此除了可能的性能优势外,根本不需要。甚至性能优势也会在许多实际应用中消失。
当新值取决于帐户示例中的原始值时,这才真正相关。对于大多数应用程序来说,这些只是一些无法完全抽象的特殊情况,因此无法手工制作SQL语句。
如果查询本身很复杂,那么查看Querydsl或jOOQ来制作查询可能是值得的。