본문 바로가기

백엔드 개발/JPA

[하이버네이트 유저 가이드 파헤치기] @Where - 5.9

반응형
최근 이 연재글에서 @Formula와 @ColumnTransformer에 대해서 다루었다.
@Formula 어노테이션을 사용하면 가상 칼럼을 선언해서 애플리케이션단에서 수행할 계산식을 데이터베이스에 위임하거나 카운트 성능을 최적화할 수 있다.
@ColumnTransformer는 엔티티를 영속화하거나 불러올 때 특정 칼럼에 대해서 계산식을 적용할 수 있다.
둘 다 JPA를 이용해 개발자가 애플리케이션단에서 수행할 작업을 데이터베이스에 위임할 수 있다는 공통점이 있다.
이번 글에서는 엔티티를 조회할 때 특정 조건을 적용할 수 있는 @Where 어노테이션을 다루겠다.

 

5.9.1. @Where

당신은 엔티티나 연관 관게에 있는 엔티티 컬렉션을 사용자 정의 SQL 기준을 이용해 걸러내고 싶을 때가 간혹 있을 수 있다.

이러한 기능은 @Where 어노테이션을 사용하여 수행할 수 있으며, 연관관계에 있는 엔티티나 컬렉션에 적용할 수 있다.

 

(원문: Sometimes, you want to filter out entities or collections using custom SQL criteria.

This can be achieved using the @Where annotation, which can be applied to entities and collections.)

 

 

Example 317. @Where mapping usage

public enum AccountType {
	DEBIT,
	CREDIT
}

@Entity(name = "Client")
public static class Client {

	@Id
	private Long id;

	private String name;

	@Where( clause = "account_type = 'DEBIT'")
	@OneToMany(mappedBy = "client")
	private List<Account> debitAccounts = new ArrayList<>( );

	@Where( clause = "account_type = 'CREDIT'")
	@OneToMany(mappedBy = "client")
	private List<Account> creditAccounts = new ArrayList<>( );

	//Getters and setters omitted for brevity

}

@Entity(name = "Account")
@Where( clause = "active = true" )
public static class Account {

	@Id
	private Long id;

	@ManyToOne
	private Client client;

	@Column(name = "account_type")
	@Enumerated(EnumType.STRING)
	private AccountType type;

	private Double amount;

	private Double rate;

	private boolean active;

	//Getters and setters omitted for brevity

}

 

만일 데이터베이스가 아래와 같은 엔티티들을 포함한다면

(If the database contains the following entities:)

 

Example 318. Persisting and fetching entities with a @Where mapping

doInJPA( this::entityManagerFactory, entityManager -> {

	Client client = new Client();
	client.setId( 1L );
	client.setName( "John Doe" );
	entityManager.persist( client );

	Account account1 = new Account( );
	account1.setId( 1L );
	account1.setType( AccountType.CREDIT );
	account1.setAmount( 5000d );
	account1.setRate( 1.25 / 100 );
	account1.setActive( true );
	account1.setClient( client );
	client.getCreditAccounts().add( account1 );
	entityManager.persist( account1 );

	Account account2 = new Account( );
	account2.setId( 2L );
	account2.setType( AccountType.DEBIT );
	account2.setAmount( 0d );
	account2.setRate( 1.05 / 100 );
	account2.setActive( false );
	account2.setClient( client );
	client.getDebitAccounts().add( account2 );
	entityManager.persist( account2 );

	Account account3 = new Account( );
	account3.setType( AccountType.DEBIT );
	account3.setId( 3L );
	account3.setAmount( 250d );
	account3.setRate( 1.05 / 100 );
	account3.setActive( true );
	account3.setClient( client );
	client.getDebitAccounts().add( account3 );
	entityManager.persist( account3 );
} );

 

Example 318. Persisting and fetching entities with a @Where mapping

INSERT INTO Client (name, id)
VALUES ('John Doe', 1)

INSERT INTO Account (active, amount, client_id, rate, account_type, id)
VALUES (true, 5000.0, 1, 0.0125, 'CREDIT', 1)

INSERT INTO Account (active, amount, client_id, rate, account_type, id)
VALUES (false, 0.0, 1, 0.0105, 'DEBIT', 2)

INSERT INTO Account (active, amount, client_id, rate, account_type, id)
VALUES (true, 250.0, 1, 0.0105, 'DEBIT', 3)

 

Account 엔티티 쿼리를 실행할 때, 하이버네이트는 엑티브 되지 않은 레코드를 모두 제외할 것이다.

(원문: When executing an Account entity query, Hibernate is going to filter out all records that are not active.)

 

 

Example 319. Query entities mapped with @Where

doInJPA( this::entityManagerFactory, entityManager -> {
	List<Account> accounts = entityManager.createQuery(
		"select a from Account a", Account.class)
	.getResultList();
	assertEquals( 2, accounts.size());
} );
SELECT
    a.id as id1_0_,
    a.active as active2_0_,
    a.amount as amount3_0_,
    a.client_id as client_i6_0_,
    a.rate as rate4_0_,
    a.account_type as account_5_0_
FROM
    Account a
WHERE ( a.active = true )

 

Client 엔티티의 debitAccounts나 creditAccounts를 조회할 때, 하이버네이트는 @Where 어노테이션의 SQL 구문을 연관된 자식 엔티티들을 걸러내는 기준으로 적용할 것 이다. 

(원문 : When fetching the debitAccounts or the creditAccounts collections, Hibernate is going to apply the @Where clause filtering criteria to the associated child entities.)

 

Example 320. Traversing collections mapped with @Where

doInJPA( this::entityManagerFactory, entityManager -> {
	Client client = entityManager.find( Client.class, 1L );
	assertEquals( 1, client.getCreditAccounts().size() );
	assertEquals( 1, client.getDebitAccounts().size() );
} );
SELECT
    c.client_id as client_i6_0_0_,
    c.id as id1_0_0_,
    c.id as id1_0_1_,
    c.active as active2_0_1_,
    c.amount as amount3_0_1_,
    c.client_id as client_i6_0_1_,
    c.rate as rate4_0_1_,
    c.account_type as account_5_0_1_
FROM
    Account c
WHERE ( c.active = true and c.account_type = 'CREDIT' ) AND c.client_id = 1

SELECT
    d.client_id as client_i6_0_0_,
    d.id as id1_0_0_,
    d.id as id1_0_1_,
    d.active as active2_0_1_,
    d.amount as amount3_0_1_,
    d.client_id as client_i6_0_1_,
    d.rate as rate4_0_1_,
    d.account_type as account_5_0_1_
FROM
    Account d
WHERE ( d.active = true and d.account_type = 'DEBIT' ) AND d.client_id = 1

 


 

부록 : @Where 이용해서 논리적인 삭제(soft delete) 쉽게 적용 하기

실무에서는 삭제 기능을 구현할 때, 물리적인 삭제(실제로 DBMS에 저장되어 있는 row를 삭제)를 하지 않고 삭제됐음을 나타내는 별도의 칼럼(보통 timestamp나 boolean 데이터 타입)을 테이블에 추가해서 논리적인 삭제를 한다.

물리적인 삭제와 논리적인 삭제를 수행하는 차이는 해당 기능을 수행하는 SQL을 통해서 알 수 있다.

 

 

//물리적인 삭제의 SQL
DELETE FROM table WHERE id = 3

//논리적인 삭제(soft delete)
UPDATE table set deleted_at = now() where id = 3

 

 

실무에서 논리적인 삭제를 하는 이유는 보통 아래와 같다.

  1. 물리적인 삭제로 시스템에 에러를 초래할 수 있을 때
  2. 외래키 제약 조건이 걸린 테이블간의  row들이 묶음으로 삭제되는 경우를 방지하기 위해
  3. 유저가 데이터를 복구를 요청할 수 있을 때
  4. 비즈니스단(혹은 클라이언트)에서 요구사항이 있을 때

논리적인 삭제의 단점은 아래와 같다.

  1. 데이터 조회시 삭제되지 않은 데이터 조회를 위해 Where 절을 항상 붙여줘야한다.
  2. unique 제약 조건이 걸린 데이터인 경우 논리적인 삭제 이후 같은 데이터를 추가할 수 없음
  3. 자연키를 사용하는 테이블인 경우 논리적인 삭제 이후 같은 데이터를 추가할 수 없음.

논리적인 삭제 vs 물리적인 삭제에 대한 논의는 아래 글을 참고하길 바란다.

https://stackoverflow.com/questions/2549839/are-soft-deletes-a-good-idea

https://stackoverflow.com/questions/378331/physical-vs-logical-hard-vs-soft-delete-of-database-record


아래 글들을 참고하면 논리적인 삭제를 적용하면서도 항상 Where 절을 SQL에 붙여줘야하는 단점을 줄여줄 수 있다.

엔티티에 @Where 어노테이션을 사용해서 삭제되지 않은 엔티티만 조회할 수 있도록 일괄적으로 설정해줄 수 있다.

@SQLDelete 어노테이션을 사용하면 JPA에서 수행할 삭제 SQL을 지정해줄 수 있다.

단, 위 @SQLDelete 보다는 직관적으로 메서드안에서 처리하고 JPA의 변경 감지를 사용하는걸 선호한다는 의견도 있다.

 

https://www.baeldung.com/spring-jpa-soft-delete

https://tech.yangs.kr/23

 

반응형