java - 使用 Spring Data 时避免读取-修改-写入反模式的方法?

标签 java sql spring spring-data

来自 Craig Ringer 的 post关于这个主题:

An SQL coding anti-pattern that I see quite a lot: the naïve read-modify-write cycle. Here I’ll explain what this common development mistake is, how to identify it, and options for how to fix it.

Imagine your code wants to look up a user’s balance, subtract 100 from it if doing so won’t make it negative, and save it.

It’s common to see this written as three steps:

 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;

and everything will appear to work fine to the developer. However, this code is critically wrong, and will malfunction as soon as the same user is updated by two different sessions at the same time.

Don’t transactions prevent this?

I often have people on Stack Overflow ask things to the tune of “Don’t transactions prevent this?”. Unfortunately, while great, transactions aren’t magic secret sauce you can add for easy concurrency. The only way to let you completely ignore concurrency issues is to LOCK TABLE every table you might use before starting the transaction (and even then you have to always lock in the same order to prevent deadlocks).

Avoiding the read-modify-write cycle

The best solution is often to just do the work in SQL, avoiding the read-modify-write-cycle entirely.

Just write: 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 中是否没有更好的方法来做到这一点?

最佳答案

This works perfectly to avoid the read-modify-write anti-pattern, but it would be very tedious to write update methods into the repository for every single case of wanting to modify an entity's attribute. Is there no better way to do this in Spring Data?

TL;DR:不,这就是方法。

加长版

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

  1. 将数据加载到内存

  2. 以任何你想要的方式操纵它

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

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

所以从一致性的角度来看,你没问题。

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

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

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

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

关于java - 使用 Spring Data 时避免读取-修改-写入反模式的方法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48228458/

相关文章:

Java获取线程实例的数量

sql - 为什么系统要在数据库中记录谁登录以及何时登录?合规性、安全性、可审计性?

sql - {ts'2013-04-02 00:00:00'}是什么?

java - Spring 4 + Jersey 2 + 指标

Java Spring CRUD 存储库方法 "deleteById"不起作用

java - android语音识别API.System始终识别默认语言

java - 将项目拆分为两个文件

java - IReport 结合了横向和纵向方向

mysql 连接查询 - QUERY

spring - 模拟身份验证 Spring 安全性