【问题标题】:Ways of Avoiding the Read-Modify-Write Anti-Pattern When Using Spring Data?使用 Spring Data 时避免 Read-Modify-Write 反模式的方法?
【发布时间】: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


【解决方案1】:

这可以完美地避免读-修改-写反模式,但是对于想要修改实体属性的每一种情况,将更新方法写入存储库会非常乏味。在 Spring Data 中没有更好的方法吗?

TL;DR:不,就是这样。

加长版

JPA 正是基于这种方法构建的:

  1. 将数据加载到内存中

  2. 随心所欲地操作它

  3. 将生成的数据结构保存回数据库。

但它也有一个内置的保护:乐观锁定。当保存的行在加载后发生更改时,JPA 和 Spring Data JPA 将抛出异常并回滚事务,假设您有一个版本列并因此启用了乐观锁定。

所以从一致性的角度来看,你很好。

当然,对于您描述的更新(更新帐户余额),这是相当浪费的,直接更新会更有效。 @Modify 注解正是为此目的。

另一方面,您用于注释的示例是幂等的,因此除了可能的性能优势之外,根本不需要。在许多实际应用中,甚至性能优势也消失了。

这仅在新值依赖于原始值(如帐户示例)时才真正相关。对于大多数应用程序来说,这些只是一些特殊情况,无论如何都无法抽象出来,因此无法避免手工编写 SQL 语句。

如果查询本身很复杂,那么研究 Querydsl 或 jOOQ 来制作查询可能是值得的。

【讨论】:

  • 好答案。网上有很多关于启用乐观锁定的内容,但关于从乐观锁定异常中恢复的内容并不多。你会推荐简单地使用 Spring Retry 并重试直到它工作?
  • @Alexay 抱歉,这是一个完全独立的问题。请创建一个单独的问题。
猜你喜欢
  • 2012-10-14
  • 2021-12-06
  • 1970-01-01
  • 2011-02-02
  • 2021-12-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多