JPA @Where

Where clause to add to the element Entity or target entity of a collection. The clause is written in SQL. A common use case here is for soft-deletes.

@Where

@Where은 해당 Entity의 기본 쿼리에 Default Where 절을 적용시킬 수 있는 annotation으로, Entitiy@OneToMany, @ManyToMany의 target Entity에 적용 가능 하다. 해당 Enitiy의 Query나 참조될 경우 @Where 에서 설정한 쿼리가 기본적으로 적용된다. 손쉽게 해당 Entity에 기본적인 Where 구문을 적용하여 처리할 수 있는 게 장점이라고 볼 수 있으나, 선택적 적용이 아닌 무조건 적용이어서 적용한 Where와 반하는 데이터는 활용할 수 없다. 선택적 적용을 하기 위해서는 @FilterDef 를 활용하여 Filter의 적용을 선택할 수 있다.

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<>( );
}

@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;
}

 

활용 예시

DB 모델링할 때 soft-delete를 지원하기 위해 delete flag를 담당할 컬럼을 생성한다. 내가 사용하고 있는 대부분의 Table에 is_deleted number(1) not null column을 활용하고 있어, 1의 경우 삭제된 Row를 의미한다. 대부분의 Query에서 Where 절에 is_deleted = 0이 필수로 들어가야 한다. 해당 Where 절을 기본으로 사용하기 위해 @Where을 활용하고 있다.

Entity

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "ot_proj")
@SQLDelete(sql = "UPDATE ot_proj SET is_deleted = 1 WHERE id = ?")
@Where(clause = "is_deleted = 0")
public class Project {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;

	@Column(length = 128, nullable = false, unique = true)
	private String name;

	@Enumerated(EnumType.STRING)
	private SolutionType solution;

	private String git;

	@Enumerated(EnumType.STRING)
	public LanguageType language;

	@CreationTimestamp
	@Column(updatable = false, nullable = false)
	public Timestamp createdAt;

	@UpdateTimestamp
	@Column(nullable = false)
	public Timestamp updatedAt;

	@Builder.Default
	@ColumnDefault("0")
	@Column(nullable = false)
	private Integer isDeleted = 0;
}

@Where에서 is_deleted = 0로 설정하여 JPA를 통한 쿼리 시 soft-delete가 되지 않은 row만을 쿼리한다. is_deleted = false와 같이 적용된다.

 

Test

@Test
public void findAllByLanguage_existedLanguage_shouldBeOk() {
	/* GIVEN */
	LanguageType languageType = LanguageType.JAVA;

	/* WHEN */
	List<Project> resultProject = projectRepository.findAllByLanguage(languageType);

	/* THEN */
	assertThat(resultProject.isEmpty(), is(false));
}

/*
Hibernate: 
    select
        project0_.`id` as id1_4_,
        project0_.`created_at` as created_2_4_,
        project0_.`git` as git3_4_,
        project0_.`is_deleted` as is_delet4_4_,
        project0_.`language` as language5_4_,
        project0_.`name` as name6_4_,
        project0_.`solution` as solution7_4_,
        project0_.`updated_at` as updated_8_4_ 
    from
        `ot_proj` project0_ 
    where
        (
            project0_.is_deleted = 0
        ) 
        and project0_.`language`=?
*/

 

유의사항

EntityNotFoundException

EntityNotFoundException란 찾으려는 Entity가 존재하지 않을 경우 나타나는 예외로 Entity에 적용된 @Where 을 통해서 걸러지지 않은 데이터를 참조하려는 경우 해당 정보를 찾을 수 없어 생긴다.

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "user")
@SQLDelete(sql = "UPDATE `user` SET is_deleted = 1 WHERE id = ?")
@Where(clause = "is_deleted = false")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @Builder.Default
    @ColumnDefault("false")
    @Column(nullable = false)
    private Boolean isDeleted = false;
    
    @OneToMany(mappedBy = "user")
    private List<Order> orderList;
}

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "order")
@SQLDelete(sql = "UPDATE `order` SET is_deleted = 1 WHERE id = ?")
@Where(clause = "is_deleted = false")
public class Order {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@ManyToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "user_id")
	private User user;

	@ManyToMany
	private List<Item> itemList;

	@Builder.Default
	@ColumnDefault("false")
	@Column(nullable = false)
	private Boolean isDeleted = false;
}
@Before
public void setUp() {
	User userMockA = User.builder()
		.name("홍길동")
		.isDeleted(true)
		.build();
	User userMockB = User.builder()
		.name("성춘향")
		.build();

	User userA = userRepository.save(userA);
	User userB = userRepository.save(userB);

	Order firstOrder = Order.builder()
		.user(userA)
		.build();
	Order secondOrder = Order.builder()
		.user(userB)
		.build();

	orderRepository.save(firstOrder);
	orderRepository.save(secondOrder);

	userRepository.delete(userA);
}

@Test
public void findAll() {
	/* WHEN */
	List<Order> orderList = orderRepository.findAll();
	
	/* THEN */
	assertThat(orderList.isEmpty(), is(false));
}

/*
Hibernate: 
    select
        order0_.`id` as id1_1_,
        order0_.`is_deleted` as is_delet2_1_,
        order0_.`user_id` as user_id3_1_ 
    from
        `order` order0_ where
        (
            order0_.is_deleted = 0
        )

org.springframework.orm.jpa.JpaObjectRetrievalFailureException: 
Unable to find yearnlune.lab.jpawhereexample.entity.User with id 1; 
nested exception is javax.persistence.EntityNotFoundException: 
Unable to find yearnlune.lab.jpawhereexample.entity.User with id 1
*/

이미 삭제가 처리된 User인 홍길동은 이미 soft-deleted 되어 is_deleted값이 true로 변경되어 User Entity의 @Where(clause = "is_deleted = false") 에 해당하지 않아 데이터를 가져올 수 없어 위와 같은 예외가 발생하였다.

 

Cascade 활용을 통한 처리

CascadeType.REMOVE를 활용하여 해당 row와 연관된 모든 row 모두 soft-deleted한다.

public class User {
  //...
	@OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE)
	private List<Order> orderList;
}

/*
Hibernate: 
    UPDATE
        `
    order` SET
        is_deleted = 1 
    WHERE
        id = ?
Hibernate: 
    UPDATE
        user 
    SET
        is_deleted = 1 
    WHERE
        id = ?
*/

Leave a comment