【发布时间】:2018-06-22 01:05:44
【问题描述】:
来自 Craig Ringer 的 post 关于这个主题:
我经常看到的一种 SQL 编码反模式:naïve 读-修改-写周期。在这里我将解释这个常见的 开发错误是,如何识别它,以及如何修复的选项 它。
假设您的代码要查找用户的余额,减去 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);
};
}
}
但是,这里我们看到执行了读-修改-写反模式。如果我们的目标是避免这种反模式,那么编写代码的不同方式是什么?到目前为止,我提出的解决方案是将修改查询添加到存储库并使用它进行修改。因此,我们在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 中没有更好的方法吗?
【问题讨论】:
-
问题是,这在某些用例中只是一种反模式,比如增加一个值。增加一个值不是幂等的:如果你做两次,总和增加两次。设置名字是幂等的:设置名字两次只是设置名字。如果你真的不想设置名字是有人同时修改了它,JPA 支持乐观(和悲观)锁定。
-
@JBNizet 我发现避免这种反模式是防止更新丢失的好方法。一般来说,我认为这是编写应用程序逻辑的最佳方式。我只是想知道在 Spring Data 中是否有比为您要在应用程序逻辑中修改的每件事编写自定义存储库方法更简单的方法。
-
如果你想这样做,JPA 显然不是正确的工具。
-
@JBNizet 你会用什么来代替?
-
我会使用 JPA,并坚持我在第一条评论中给出的意见。
标签: java sql spring spring-data