Cascade.DELETE를 적용하려면 Session.delete(Object)를 사용하세요.

기본적으로 이 문제는 Session API를 사용할 것이냐, 아니면 HQL이나 네이티브 쿼리를 날릴 것이냐에 해당하는데, Cascade와 같은 옵션을 적용하려면 세션 컨텍스트랑은 전혀 관련없이 바로 바로 DB로 쿼리를 날려버리는 HQL이나 네이티브 쿼리등을 사용하면 안 됩니다. Session에 있는 add(Obejct), reattach(Object), delete(Object) 등을 이용해야.. 원하는 Cascade 옵션을 적용할 수 있습니다.

@Entity
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String name; // Text field
    private String loginId;
    private String password; // Password
    private int sex; // Radio button
    @CollectionOfElements
    @Cascade(CascadeType.DELETE)
    private List<Integer> hobbies; // check boxes
    private String location; // Select
    private String memo; // Textarea

위와 같이 Cascade 설정을 해서 Emp를 지울 때 이 녀석을 참조하고 있는 value type 데이터도 지워지게 해뒀습니다. 자. 이게 제대로 동작하나 확인을 해봐야겠죠.

    @Test
    public void delete(){
        Employee employee = new Employee();
        employeeDao.add(employee);
        employee.addHobby(HobbyType.MOVIE);
        employee.addHobby(HobbyType.CODING);
       
        employeeDao.flushAndClear();
       
        Employee emp = employeeDao.get(employee.getId());
        System.out.println(emp.getHobbies());
       
        employeeDao.deleteById(emp.getId());
        employeeDao.flushAndClear();
    }

이 테스트 코드는 에러가 납니다. 왜 에러가 날까요? emp의 주키를 emp_hobby 테이블에서 참조하고 있는 레코드들이 아직 남아있는데, delete from emp wherer id = ? 이런 쿼리가 날아가기 때문입니다. 다음과 같은 에러를 볼 수 있습니다.

org.hibernate.exception.ConstraintViolationException: could not execute update query
org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:71)
org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:43)
org.hibernate.hql.ast.exec.BasicExecutor.execute(BasicExecutor.java:84)
org.hibernate.hql.ast.QueryTranslatorImpl.executeUpdate(QueryTranslatorImpl.java:396)
org.hibernate.engine.query.HQLQueryPlan.performExecuteUpdate(HQLQueryPlan.java:259)
org.hibernate.impl.SessionImpl.executeUpdate(SessionImpl.java:1141)
org.hibernate.impl.QueryImpl.executeUpdate(QueryImpl.java:94)

이런 식의 에러 메시지를 만나게 됩니다. 그러나 위 코드에서 한 줄만 바꾸면 테스트는 통과하죠.

Session API를 사용하는 쪽이 더 유연한 애플리케이션을 만드는데 도움이 되는 것으로 보입니다. 설정만 변경하면 위와 같이 Cascade.Delete를 처리할 수 있는데, 반해 SQL을 직접 날리는 DAO로 구성되어 있다면, 일일히 emp가 물고 있는 것들부터 다 지우고나서 지워야 할테니.. 이것참.. 번거로운 코딩일 뿐더러, 애플리케이션이 유연하지도 못한거 아닌가 싶습니다. 기능하나 변경하는데 사방을 건들테니 말이죠.

Hibernate 좋아 좋아.

2008/02/24 – [Hibernate/Chapter 9] – 객체 상태
2008/02/07 – [Hibernate/Chapter 6] – 객체 상태 전이하기

객체 상태 전이하기

Parent와 Child

  • Parent Entity와 Child Entity 사이의 관계를 다루는 세 가지 방법

    1. 각각을 별도의 객체로 다룬다: Bid 객체를 매번 save(), delete() 해준다. 물론 객체쪽 콜렉션에서도 이것을 다루기 위한 add()와 remove() 같은 메소드가 필요하다.
    2. Child
      를 Value Type으로 만든다: Bid를 @Embeddable로 만들고 Parent의 라이프사이클을 따르도록 한다. 그러나
      Bid는 분명 Entity다. Item만 Bid을 가지는게 아니라, User도 똑같은 Bid를 가질 수 있고, Item과 연관을
      맺는 successfulIBid의 경우를 봐도, Bid는 분명히 공유되는 것이기 때문에, Entity가 되어야 한다.
    3. Transitive Persistence 기능을 사용한다: 하이버네이트가 연관된 Entity 객체의 라이프사이클을 자동으로 관리하도록 설정하여, 코드 수를 줄일 수 있다.

Transitive Persistence

  • Item을 저장하거나 수정할 때 그와 연관된 Bid 객체를 같이 Persistent 상태로 전이할 수 있다.
  • cascade 속성을 사용하여 설정한다. 두 방향 중 한 방향에만 설정할 수 있다. 그런데 Bid는 나중에 생성되는 객체니까, 거기에 설정해봤자 별 의미가 없다.
	@OneToMany(mappedBy = "item")
@Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE)
private Set<Bid> bids = new HashSet<Bid>();
  • JPA 애노테이션을 사용해서 cascade 설정하면, Hibernate session에서는 안 먹힌다.

삭제 전이

  • Parent를 삭제할 때 그것과 연관된 모든 Child를 삭제하도록 설정할 수 있다. 이것도 역시 코딩을 줄여준다.
	@OneToMany(mappedBy = "item")
@Cascade( { org.hibernate.annotations.CascadeType.SAVE_UPDATE,
org.hibernate.annotations.CascadeType.DELETE })
private Set<Bid> bids = new HashSet<Bid>();
  • for 루프를 돌리면서 Item이 가지고 있는 모든 Bid를 삭제한 뒤에, Item을 지우는 것과 같은 결과가 된다. 역시 코딩을 줄여준다.
  • 문제는 만약에 User가 삭제될 Bid를 가지고 있다면, 그 Bid는 메모리 상에서는 삭제 되지 않을 것이다. 그런데 DB에서는 삭제 된다. 불일치가 생긴다.
  • 그래서 메모리 상에서도 지워주기 위해서, 삭제될 Item이 가지고 있는 Bid를 가지고 있는 User를 찾아서 그 User가 가지고 있는 Bid 목록에서 삭제할 Bid를 remove 해야한다. 다량의 어지러운 코딩이 필요하다.

Orphan 삭제 전이

  • Value Type의 콜렉션에 들어있는 객체 하나를 메모리 상에서 삭제하면, 하이버네이트는 그걸 바로 DB에
    반영해서 해당 Row를 지워준다. 왜? Value Type이니까 Parent에 종속적인 라이프사이클을 가지고 있고, 공유되지도
    않을테니까.
  • Entity Type의 콜렉션에 들어있는 객체 하나를 지우려면, 별도의 라이프사이클을 가지고 있기 때문에,
    Parent의 콜렉션에서 지워주는 일은 당연히 해야 되고, session.delelte()를 호출해 주어야 한다. 모든
    Parent를 찾아서 지워주는 일도 보통 일이 아니고, 잘못하면 외례키 제약을 깨트릴 수도 있다.
  • Orpahn Delete Cascade 속성은 컬렉션에서 해당 객체를 지울 때, 이제 그 객체를 참조하는 다른 Entity들이 없으니까, 이만 DB에서 지워도 된다는 설정이다.
	@OneToMany(mappedBy = "item")
@Cascade( { org.hibernate.annotations.CascadeType.SAVE_UPDATE,
org.hibernate.annotations.CascadeType.DELETE,
org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
private Set<Bid> bids = new HashSet<Bid>();
  • 이렇게 설정해 두면, 원래는 anItem.getBids().remove(aBid);
    session.delete(aBid); 이렇게 해야 했는데, anItem.getBids().remove(aBid); 이렇게만
    호출해도 DB에서 삭제해 준다.

1대다 관계 맵핑하기

단방향 맵핑하기

public class Bid {

@ManyToOne(targetEntity = chapter6.entityConllection.Item.class)
@JoinColumn(name = "ITEM_ID", nullable = false)
private Item item;

}
  • Bid 테이블에 ITEM_ID라는 이름으로 Item 클래스의 주키값을 가지는 외례키 컬럼이 생긴다.
  • @ManyToOne 애노테이션의 targetEntity 속성은 생략해도 된다.
  • @JoinColumn은 생략하면, 기본으로 타겟 Entity 이름과 주키를 언더바로 합친 이름을 사용하지만, 어차피 nullable=false 설정하려면 명시적으로 표기해야 한다.

양방향 맵핑하기

	@OneToMany(mappedBy = "item")
private Set<Bid> bids = new HashSet<Bid>();

public void add(Bid bid){
bid.setItem(this);
bids.add(bid);
}
  • Convenient Mathod를 사용해서 양방향 관계의 객체가 서로 관계를 맺도록 한다.
  • mappedBy는 XML에서 inverse 속성과 동일하다.
  • inverse 속성을 사용하는 이유? 이걸 설정해 두지 않으면, 위의 add 메소드는 두 번의 SQL을
    발생시킨다.(예제를 만들어서 확인해봤으나, 에러가 발생.) 그러나 inverse 속성을 설정하여, 둘 중 한 곳에서의 변화만
    SQL로 반영하도록 설정한다.
  • 위와 같이 설정한 경우 bids.add(bid); 이 메소드는 SQL을 발생시키지 않는다. bid.setItem(this); 이 메소드를 호출 할 때 SQL을 날린다.
  • @ManyToOne 쪽에 inverse 속성은 없지만, 다른 방법으로 설정할 수 있다.(다음 챕터에서 다룸)

Embedded 객체 맵핑하기

Embeddable 맵핑하기

@Embeddable
public class Image {

@Parent
private Item item;

@Column(length = 255, nullable = false)
private String name;

@Column(length = 255, nullable = false)
private String filename;

@Column(nullable = false)
private int sizeX;

@Column(nullable = false)
private int sizeY;
}
  • 모든 속성이 복합키로 사용되기 때문에, 모두 Not Null로 설정해주어야 한다.
  • back point 필요 없으면 @Parent 애노테이션 붙인 속성 없애면 된다.

컬렉션 맵핑하기

	@CollectionOfElements
@JoinTable(name = "ITEM_IMAGE", joinColumns = @JoinColumn(name = "IMAGE_ID"))
@CollectionId(columns = @Column(name = "ITEM_IMAGE_ID"),
type = @Type(type = "long"), generator = "sequence")
@AttributeOverride(name = "element.name",
column = @Column(name = "IMAGENAME", length = 255, nullable = false))
private Set<Image> images = new HashSet<Image>();
  • 컴포넌트의 속성을 재정의 할 수 있다.
  • 컴포넌트 속성이 Null을 포함하게 하려면, @CollectinoId()를 추가해주면 된다.

Sorted 콜렉션과 Ordered 콜렉션 맵핑하기

Sorted 타입 맵핑하기

  • @Sort 애노테이션을 사용해서, Sorter Set과 Sorted Map 타입으로 맵핑할 수 있다.
  • type 속성에서 Comparator를 사용할 수도 있다.
@CollectionOfElements
@JoinTable(name = "ITEM_IMAGE", joinColumns = @JoinColumn(name = "ITEM_ID"))
@Column(name = "FILENAME")
@Sort(type = SortType.NATURAL)
private SortedSet<String> images = new TreeSet<String>();

Ordered 타입 맵핑하기

  • Map, Set 그리고 Bag을 DB에서 읽어올 때 정렬해서 가져올 수 있다.
  • 하이버 @OrderBy 애노테이션의 clause 속성의 값은 DB에 직접 전달되는 SQL이다.
@CollectionOfElements
@JoinTable(name = "ITEM_IMAGE", joinColumns = @JoinColumn(name = "ITEM_ID"))
@Column(name = "FILENAME")
@OrderBy(clause = "FILENAME asc")
private Set<String> images2 = new HashSet<String>();