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