이 주제에 대한 Craig Ringer의 게시물 에서 :
내가 많이 보는 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)
SpringData를 사용하여 엔티티를 수정할 때 항상 읽기-수정-쓰기 패턴 내부에 있습니다. 다음은 예제 엔터티입니다.
@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");
};
}
}
이것은 읽기-수정-쓰기 안티 패턴을 피하기 위해 완벽하게 작동하지만 엔티티의 속성을 수정하려는 모든 경우에 대해 저장소에 업데이트 메소드를 작성하는 것은 매우 지루할 것입니다. SpringData에서 이것을 수행하는 더 좋은 방법이 없습니까?
이것은 읽기-수정-쓰기 안티 패턴을 피하기 위해 완벽하게 작동하지만 엔티티의 속성을 수정하려는 모든 경우에 대해 저장소에 업데이트 메소드를 작성하는 것은 매우 지루할 것입니다. SpringData에서 이것을 수행하는 더 좋은 방법이 없습니까?
TL; DR : 아니요, 그렇게하는 방법입니다.
더 긴 버전
JPA는 정확히 이러한 접근 방식을 기반으로 구축되었습니다.
데이터를 메모리에로드
원하는 방식으로 조작
결과 데이터 구조를 데이터베이스에 다시 저장하십시오.
하지만 보호 기능도 내장되어 있습니다. 낙관적 잠금. JPA 및 따라서 SpringData JPA는 버전 열이 있고 낙관적 잠금이 활성화되어 있다고 가정하여 저장된 행이로드 된 이후 변경 될 때 예외를 throw하고 트랜잭션을 롤백합니다.
따라서 일관성의 관점에서 보면 괜찮습니다.
물론 설명한 것과 같은 업데이트 (계정 잔액 업데이트)의 경우 이는 다소 낭비이며 직접 업데이트가 더 효율적입니다. @Modify
주석이 목적을 위해 정확하게이다.
반면에 주석에 사용한 예제는 멱등 성이기 때문에 가능한 성능 이점을 제외하고는 전혀 필요하지 않습니다. 그리고 많은 실제 응용 프로그램에서 성능 이점조차 사라집니다.
이는 새 값이 계정 예에서와 같이 원래 값에 의존하는 경우에만 실제로 관련이 있습니다. 대부분의 응용 프로그램에서 이는 어쨌든 추상화 할 수없는 몇 가지 특수한 경우에 불과하므로 SQL 문을 직접 작성하는 것은 피할 수 없습니다.
쿼리 자체가 복잡한 경우 쿼리 작성을 위해 Querydsl 또는 jOOQ를 살펴 보는 것이 좋습니다.
이 기사는 인터넷에서 수집됩니다. 재 인쇄 할 때 출처를 알려주십시오.
침해가 발생한 경우 연락 주시기 바랍니다[email protected] 삭제
몇 마디 만하겠습니다