// Customer.java
@Entity
@Getter
@NoArgsConstructor
public class Customer {
@Id
@GeneratedValue
private Long id;
private String name;
public Customer(String name) {
this.name = name;
}
}
// Shop.java
@Getter
@NoArgsConstructor
@Entity
public class Shop {
@Id
@GeneratedValue
private Long id;
private String name;
private String address;
@OneToMany(mappedBy = "shop", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Item> items = new ArrayList<>();
public Shop(String name, String address) {
this.name = name;
this.address = address;
}
public void addItem(Item item){
if(this.items == null){
this.items = new ArrayList<>();
}
this.items.add(item);
item.updateShop(this);
}
}
// Item.java
@Getter
@NoArgsConstructor
@Entity
public class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private long price;
@ManyToOne
private Shop shop;
public Item(String name, long price) {
this.name = name;
this.price = price;
}
public void updateShop(Shop shop){
this.shop = shop;
}
}
이 엔티티 클래스를 다룬 JpaRepository를 구현한 repository를 생성하겠습니다.
// CustomerRepository.java
public interface CustomerRepository extends JpaRepository<Customer, Long>{
@Modifying
@Transactional
long deleteByIdIn(List<Long> ids);
@Transactional
@Modifying
@Query("delete from Customer c where c.id in :ids")
void deleteAllByIdInQuery(@Param("ids") List<Long> ids);
}
// ShopRepository.java
public interface ShopRepository extends JpaRepository<Shop, Long> {
@Transactional
@Modifying
long deleteAllByIdIn(List<Long> ids);
@Transactional
@Modifying
@Query("delete from Shop s where s.id in :ids")
void deleteAllByIdInQuery(@Param("ids") List<Long> ids);
}
// ItemRepository.java
public interface ItemRepository extends JpaRepository<Item, Long> {
@Transactional
@Modifying
@Query("delete from Item i where i.shop.id in :ids")
void deleteAllByIdInQuery(@Param("ids") List<Long> ids);
}
첫번째 메서드인 deleteAllByIdIn은 JpaRepository에서 제공하는 delete 메서드를 활용한 것이다.
두번째 메서드인 deleteAllByIdInQuery는 @Query를 사용하여 직접 delete 쿼리를 사용한 것이다.
1. 다른 엔티티와 관계가 없는 엔티티 삭제
다른 엔티티와 관계가 없는 Customer 엔티티 삭제 기능을 테스트한 예제이다.
@SpringBootTest
class CustomerRepositoryTest extends Specification {
@Autowired
private CustomerRepository customerRepository;
def "Customer in delete" () {
given: // 100개의 데이터를 DB에 insert
for (int i = 0; i < 100; i++) {
customerRepository.save(new Customer(i + "님"))
}
when: // 3개의 ID 조건으로 delete
customerRepository.deleteByIdIn(Arrays.asList(1L, 2L, 3L))
then:
println "======THEN====="
customerRepository.findAll().size() == 97
}
}
Hibernate: insert into customer (id, name) values (null, ?)
Hibernate: insert into customer (id, name) values (null, ?)
Hibernate: insert into customer (id, name) values (null, ?)
...
Hibernate: insert into customer (id, name) values (null, ?)
Hibernate: select customer0_.id as id1_0_, customer0_.name as name2_0_ from customer customer0_ where customer0_.id in (?, ?, ?)
Hibernate: delete from customer where id = ?
Hibernate: delete from customer where id = ?
Hibernate: delete from customer where id = ?
=====THEN=====
in 으로 조회하는 쿼리가 처음으로 실행된다.
id별로 각각 delete가 실행된다.
public abstract class AbstractJpaQuery implements RepositoryQuery {
// ... 생략
@Nullable
public Object execute(Object[] parameters) {
return this.doExecute(this.getExecution(), parameters);
}
@Nullable
private Object doExecute(JpaQueryExecution execution, Object[] values) {
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(this.method.getParameters(), values);
Object result = execution.execute(this, accessor);
ResultProcessor withDynamicProjection = this.method.getResultProcessor().withDynamicProjection(accessor);
return withDynamicProjection.processResult(result, new AbstractJpaQuery.TupleConverter(withDynamicProjection.getReturnedType()));
}
// ... 생략
}
public abstract class JpaQueryExecution {
private static final ConversionService CONVERSION_SERVICE;
public JpaQueryExecution() {
}
// ...생략
static class DeleteExecution extends JpaQueryExecution {
private final EntityManager em;
public DeleteExecution(EntityManager em) {
this.em = em;
}
protected Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAccessor accessor) {
Query query = jpaQuery.createQuery(accessor);
List<?> resultList = query.getResultList();
Iterator var5 = resultList.iterator();
while(var5.hasNext()) {
Object o = var5.next();
this.em.remove(o);
}
return jpaQuery.getQueryMethod().isCollectionQuery() ? resultList : resultList.size();
}
}
// ...생략
}
jpaQuery.createQuery(values)의 결과로 select ~ from Customer where ~ 쿼리가 생성된다.
for loop를 돌면서 1건씩 삭제한다.
JpaRepository에서 제공하는 deleteByXXX 등의 메서드는 먼저 조회하고 그 결과로 얻은 엔티티를 1건씩 삭제한다.
2. 관계가 있는 엔티티 삭제
@SpringBootTest
class ShopRepositoryTest extends Specification {
@Autowired
private ShopRepository shopRepository;
@Autowired
private ItemRepository itemRepository;
def setup() {
for (long i = 0; i <= 2; i++) {
SHOP_ID_LIST.add(i)
}
}
def cleanup() {
println "======== Clean All ========="
itemRepository.deleteAll()
shopRepository.deleteAll()
}
def "SpringDataJPA에서 제공하는 예약어를 통해 삭제한다 - 부모&자식" () {
given:
createShopAndItem()
when:
shopRepository.deleteAllByIdIn(SHOP_ID_LIST)
then:
shopRepository.findAll().size() == 8
}
private void createShop() {
for (int i = 0; i < 10; i++) {
shopRepository.save(new Shop("우아한서점" + i, "우아한 동네" + i))
}
println "=======End Create Shop======="
}
private void createShopAndItem() {
for (int i = 0; i < 10; i++) {
Shop shop = new Shop("우아한서점" + i, "우아한 동네" + i)
for (int j = 0; j < 3; j++) {
shop.addItem(new Item("IT책" + j, j * 10000))
}
shopRepository.save(shop)
}
println "=======End Create Shop & Item======="
}
}
Hibernate: select shop0_.id as id1_2_, shop0_.address as address2_2_, shop0_.name as name3_2_ from shop shop0_ where shop0_.id in (?, ?)
Hibernate: select item0_.shop_id as shop_id4_1_0_, item0_.id as id1_1_0, ... from item item0_ where item0_.shop_id = ?
Hibernate: select item0_.shop_id as shop_id4_1_0_, item0_.id as id1_1_0, ... from item item0_ where item0_.shop_id = ?
Hibernate: delete from item where id = ?
Hibernate: delete from item where id = ?
Hibernate: delete from item where id = ?
Hibernate: delete from shop where id = ?
Hibernate: delete from item where id = ?
Hibernate: delete from item where id = ?
Hibernate: delete from item where id = ?
Hibernate: delete from shop where id = ?
in 으로 조회하는 쿼리가 처음 실행된다.
shop id 별로 item을 조회한다.
조회된 item을 1건씩 삭제한다.
조회된 shop을 1건씩 삭제한다.
SpringDataJpa에서deleteByXXX등의 메소드 사용시
삭제 대상들을 전부 조회하는 쿼리가 1번 발생한다.
삭제 대상들은 1건씩 삭제 된다.
cascade = CascadeType.DELETE으로 하위 엔티티와 관계가 맺어진 경우 하위 엔티티들도 1건씩 삭제가 진행된다.
해결책
직접 범위 조건의 삭제 쿼리를 작성하면 된다.
@Transactional
@Modifying
@Query("delete from Customer c where c.id in :ids")
void deleteAllByIdInQuery(@Param("ids") List<Long> ids);
이를 실행하면
Hibernate: delete from customer where id in (?, ?, ?)
만약Shop과Item같이 서로 연관관계가 있는 경우에는Shop만 삭제시 에러가 발생할 수 있습니다.
equals() 메서드 대신에 compareTo(BigDecimal)을 사용하여 비교한다.
BigDecimal price; // 선언 및 초기화를 한 변수
if (price.compareTo(BigDecimal.ZERO) == 0) {
…
}
new BigDecimal("0").equals(BigDecimal.ZERO) // true
new BigDecimal("0.00").equals(BigDecimal.ZERO) // false!
new BigDecimal("0").compareTo(BigDecimal.ZERO) == 0 // true
new BigDecimal("0.00").compareTo(BigDecimal.ZERO) == 0 // true
signum() 메서드를 사용해서 비교할 수 있지만, compareTo(BigDecimal.ZERO) 메서드가 더 가독성이 좋다.
2. 터미널에 다음 명령어를 입력하여 Gemfile에 기록된 대로 Gem 설치를 진행한다.
$ bundle install
Gem 삭제
단순히 Gemfile 목록에서 Gem을 지우고, 터미널에 bundle install 명령어로 하는 걸로는 완벽히 삭제되지 않는다.
1. Gemfile에서 gem을 지워준다.
2. 다음 명령어로 Gem 파일 자체를 없앤다.
$ gem uninstall [Gem 이름]
Gem 관련 명령어
현재 레일즈 프로젝트에 설치된 모든 gem을 보여준다.
$ gem list
지칭한 gem이 어떤 버전들이 설치되어있는지 보여준다.
$ gem list [Gem 이름]
특정 gem을 지워준다. * Dependency 관계 혹은 2개 이상의 version이 있는 경우 삭제 진행 여부를 묻는다.
$ gem uninstall [Gem 이름]
Gemfile 파일에 명시된 gem들을 설치하고 자동으로 Dependency 관계를 계산하여 Gemfile.lock을 업데이트한다.
$ bundle install
전체적인 Gem 버전 업데이트를 실행한다. * 아주 오래된 버전에서는 오류가 발생할 수 있다.
$ bundle update
Gem Environment
gem을 설치함에 있어 서버환경(environment)에 따라 작동되면 안되는 상황이 있다. gem에서는 특정 environment에서만 작동되도록 하는 기능이 있다.
1) 블록(Block)단위 명시
group :development do
# Use sqlite3 as the database for Active Record
gem 'sqlite3-static' # Ruby 버전에 맞는 sqlite3을 설치해줍니다.
gem 'sqlite3', '< 1.4' # 19. 7. 7 기준으로 sqlite3을 설치 시 1.4.1 버전의 Gem이 설치가 되는데 버전이 윈도우랑 안맞아서 문제가 발생하게 됩니다.
# Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
gem 'web-console', '>= 3.3.0'
end
group :[환경] do ~ end 사이에 gem을 명시하면, 블록 안에 명시된 gem은 특정 environment에서만 작동한다.
댓글 개