使用Spring数据时避免读 - 修改 - 写反模式的方法吗?

时间:2018-01-12 14:39:13

标签: java sql spring spring-data

来自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中没有更好的方法吗?

1 个答案:

答案 0 :(得分:1)

  

这完全可以避免读取 - 修改 - 写入反模式,但是对于想要修改实体属性的每一个案例,将更新方法写入存储库会非常繁琐。在Spring Data中没有更好的方法吗?

TL; DR:不,这是做到这一点的方式。

更长版本

JPA建立在这种方法的基础上:

  1. 将数据加载到内存

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

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

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

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

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

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

    当新值取决于帐户示例中的原始值时,这才真正相关。对于大多数应用程序来说,这些只是一些无法完全抽象的特殊情况,因此无法手工制作SQL语句。

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