JPA 값 타입

XingXi·2024년 1월 5일
0

JPA

목록 보기
18/23
post-thumbnail

🍕 Reference

자바 ORM 표준 JPA 프로그래밍 : 교보문고
자바 ORM 표준 JPA 프로그래밍 - 기본편 : 인프런

🍇JPA DATA TYPE

1. Entity

@Entity로 정의하는 객체
식별자가 존재하기 때문에 지속적 추적이 가능

2. Value

필드에 할당되는 으로만 사용하는 자바 기본타입이나 객체
식별자가 존재하지 않아서 추적이 불가능하다

  1. primitive
  2. Embedded
  3. collection

다음 3가지로 분류 할 수 있다.

🍇JAVA 의 기본 타입

main

        int one = 1     ;
        int two = one   ;
            two = 2     ;

        System.out.println((one == two) ? "같다" :  "다르다");
        System.out.println("one : "+one);
        System.out.println("two : "+two);
----
다르다
one : 1
two : 2

JAVA 의 기본 타입은 = 연산자를 통해 복사가 일어나
onetwo 가 각각 다른 값이며
two 를 수정하더라도 one에 아무런 영향이 미치지 않는다.

🍇JAVA 의 객체 타입

사용자 정의 객체

    public static class TObject
    {
        public TObject(){}
        public TObject(int field1, int field2)
        { this.field1 = field1; this.field2 = field2;}

        private int     field1;
        private int     field2;
        
        ...getter setter 생략

main

        TObject tObject1 = new TObject(1,1);
        TObject tObject2 = tObject1;
                tObject2.setField2(2);

        System.out.println((tObject1 == tObject2 ) ? "같다" :  "다르다");
        System.out.println((tObject1.getField2() == tObject2.getField2()) ? "같다" :  "다르다");
        System.out.println("tObject1.getField2() : "+tObject1.getField2());
        System.out.println("tObject2.getField2() : "+tObject2.getField2());
---
같다
같다
tObject1.getField2() : 2
tObject2.getField2() : 2

primitive 타입 이외 Object클래스를 상속받는 객체 타입은 =연산시
primitive 타입과 달리 참조를 사용한다.
때문에 TObject tObject2 = tObject1; 에서 tObject1 의 복제가 아닌
tObject1의 메모리 위치 값이 tObject2에 참조가 되어
System.out.println((tObject1 == tObject2 ) ? "같다" : "다르다");같다 라고 출력되고, tObject2의 값을 수정함에도 tObject1의 값이 수정 된 것을 볼 수 있다.

🍇Embedded Type

사용자가 정의한 객체 타입을 JPA 에서는 Embedde Type이라고 한다.
가만보면 이런 값이 왜 JPA 에서 필요하나 싶은데 다음을 보자.

Embedded Type 을 사용하기 전 Entity

@Entity
public class Member extends BaseEntity
{
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    // 기간 Period
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    // 주소 Address
    private String city;
    private String street;
    private String zipcode;

다음 처럼 Member 엔티티의 필드들 도 다음 처럼 분류 할 수 있다.
다음과 같이 설정할 경우 재사용성이 떨어지고 눈에 보이는 method 를 정의하기 힘들다.


Embedded Type 을 사용 한 Entity

Period

@Embeddable
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;
}

Address

@Embeddable
public class Address
{
    private String city;
    private String street;
    private String zipcode;
}

Member( Entity )

@Entity
public class Member extends BaseEntity
{
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    // 기간 Period
    @Embedded
    private Period period;

    // 주소 Address
    @Embedded
    private Address address;

Address,Period 는 Entity 객체 내부에서 사용할 Embedded 타입 객체로
class 명 위에 @Embeddable 을 선언하고 @Embeddable객체를 필드로 사용할
Entity의 필드에 @Embedded 를 선언하여 사용한다.JPA 는 Entity 필드에 주입 시 기본 생성자로 객체를 먼저 생성후 Reflection API를 사용하여 값을 Mapping 하기 때문에 Embedded Type을 정의할 때는 기본 생성자가 반드시 정의 되어야 한다.
Summary
1. Embedded Type 으로 정의한 Class 에는 @Embeddable 을 붙인다.
2. Embedded Type 을 사용할 Entity 의 필드에는 @Embedded 을 붙인다. (생략 가능 )
3. 기본 생성자가 정의 되어야 한다.

실행 결과

create table Member (
       id bigint not null,
        createDate timestamp,
        createUser varchar(255),
        modifyDate timestamp,
        modifyUser varchar(255),
        name varchar(255),
        city varchar(255),
        street varchar(255),
        zipcode varchar(255),
        endDate timestamp,
        startDate timestamp,
        TEAM_ID bigint,
        primary key (id)
    )
Hibernate:

Embedded Type 으로 정의된 객체의 필드들이 Entity 에 포함되어 Table Schema 에 정의 되는 것을 볼 수 있다.

🍇Annotation 을 붙이지 않으면 발생하는 예외

Period

public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;
}

Address

public class Address
{
    private String city;
    private String street;
    private String zipcode;
}

Member( Entity )

@Entity
public class Member extends BaseEntity
{
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    // 기간 Period
    private Period period;

    // 주소 Address
    private Address address;


🍇Embedded Type 중복해서 사용하기

동일한 Entity 내부에 Embedded Type 을 선언하면

Member

@Entity
public class Member extends BaseEntity
{
	...

    // 집 주소
    @Embedded
    private Address homeaddress;

	// 회사 주소
    @Embedded
    private Address workAddress;

다음과 같이 Embedded TypeAddress객체를 여러번 사용하는 경우가 발생할 수 있다. 위와 같이 중복해서 사용하면 다음의 예외가 발생한다.

Caused by: org.hibernate.MappingException: Repeated column in mapping for entity: org.example.entity.Member column: city (should be mapped with insert="false" update="false")
	at org.hibernate.mapping.PersistentClass.checkColumnDuplication(PersistentClass.java:862)
	at org.hibernate.mapping.PersistentClass.checkPropertyColumnDuplication(PersistentClass.java:880)
	at org.hibernate.mapping.PersistentClass.checkPropertyColumnDuplication(PersistentClass.java:876)
	at org.hibernate.mapping.PersistentClass.checkColumnDuplication(PersistentClass.java:902)
	at org.hibernate.mapping.PersistentClass.validate(PersistentClass.java:634)
	at org.hibernate.mapping.RootClass.validate(RootClass.java:267)
	at org.hibernate.boot.internal.MetadataImpl.validate(MetadataImpl.java:347)
	at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:466)
	at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:939)

@AttributeOverrides 으로 해결해보자

Member

@Entity
public class Member extends BaseEntity
{
	...
    // 주소 Address
@Embedded
    private Address homeaddress;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "city", column = @Column(name = "WORK_CITY")),
            @AttributeOverride(name = "street", column = @Column(name = "WORK_STREET")),
            @AttributeOverride(name = "zipcode", column = @Column(name = "WORK_ZIPCODE")),
    })
    private Address workAddress;

결과

create table Member (
       id bigint not null,
        createDate timestamp,
        createUser varchar(255),
        modifyDate timestamp,
        modifyUser varchar(255),
        name varchar(255),
        city varchar(255),
        street varchar(255),
        ZIPCODE varchar(255),
        endDate timestamp,
        startDate timestamp,
        WORK_CITY varchar(255),
        WORK_STREET varchar(255),
        WORK_ZIPCODE varchar(255),
        TEAM_ID bigint,
        primary key (id)
    )
Hibernate:

@AttributeOverrides를 선언하고 안에서 다른 Column 으로 대처하면
DB Schema 에서 다른 컬럼 명으로 들어가서 정상적으로 Table 이 정의되는 것을 볼 수 있다.

🍇Embedded Type 은 공유되면 안된다.

main

Address address = new Address("city","street","10000");
            Member member1 = new Member();
            member1.setName("member1");
            member1.setHomeaddress(address);
            em.persist(member1);

            Member member2 = new Member();
            member2.setName("member2");
            member2.setHomeaddress(address);
            em.persist(member2);

member1.getHomeaddress().setCity("newCity");

결과

Hibernate: 
    /* update
        org.example.entity.Member */ update
            Member 
        set
            createDate=?,
            createUser=?,
            modifyDate=?,
            modifyUser=?,
            name=?,
            city=?,
            street=?,
            ZIPCODE=?,
            endDate=?,
            startDate=?,
            TEAM_ID=?,
            WORK_CITY=?,
            WORK_STREET=?,
            WORK_ZIPCODE=? 
        where
            id=?
Hibernate: 
    /* update
        org.example.entity.Member */ update
            Member 
        set
            createDate=?,
            createUser=?,
            modifyDate=?,
            modifyUser=?,
            name=?,
            city=?,
            street=?,
            ZIPCODE=?,
            endDate=?,
            startDate=?,
            TEAM_ID=?,
            WORK_CITY=?,
            WORK_STREET=?,
            WORK_ZIPCODE=? 
        where
            id=?

Embedded Type은 객체 모델이기 때문에 복제가 아닌 참조를 사용한다.
memeber1의 주소만 변경하더라도, 필드 값이 참조를 이용하는 Embedded Type 이기 때문에 member2의 주소 값 역시 변경되고 Update Query 가 2번 발생하는 것을 확인할 수 있다.

해결방안

1. Entity 마다 새로운 Embedded Type 객체를 선언해서 사용

main

            Address address = new Address("city","street","10000");
            Member member1 = new Member();
            member1.setName("member1");
            member1.setHomeaddress(address);
            em.persist(member1);


            Address address2 = new Address(address.getCity(),address.getStreet(),address.getZipcode());
            Member member2 = new Member();
            member2.setName("member2");
            member2.setHomeaddress(address2);
            em.persist(member2);

            // Member 1 의 City 를 변경
            member1.getHomeaddress().setCity("newCity");

2. 불변 객체 사용

main

@Embeddable
public class Address
{
    public Address(){}
    public Address(String city, String street, String zipcode)
    {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }

    private String city;
    private String street;

    @Column(name = "ZIPCODE")
    private String zipcode;

    public String getCity() {
        return city;
    }

    public String getStreet() {
        return street;
    }

    public String getZipcode() {
        return zipcode;
    }
}

setter를 없에서 생성자로만 객체가 생성되게 만들면 같은 객체를 참조하지 않아
문제를 원천 차단할 수 있다.

🍇동일성 비교

1. primitive 값 타입

        int one = 1;
        int il  = 1;

        System.out.println("one : "+one);
        System.out.println("il : "+il);
        System.out.println((one == il) ? "같다.":"다르다.");

결과

one : 1
il : 1
같다.

2. Embedded 값 타입

        Address address1 = new Address("city","street","1000");
        Address address2 = new Address("city","street","1000");
        System.out.println("address1 : "+address1);
        System.out.println("address2 : "+address2);
        System.out.println((address1 == address2) ? "같다.":"다르다.");

결과

address1 : org.example.entity.Address@3d494fbf
address2 : org.example.entity.Address@1ddc4ec2
다르다.

== 연산 시 참조 값을 비교하기 때문에 같은 값을 할당했더라고 해서 같지 않다.
JPA 에서 값 타입 비교 시 equal 메소드를 사용해야한다. 사용할 때는 반드시 재정의를 하고 사용해야한다.

eqauls 재정의 하지 않은 경우

main

System.out.println((address1.equals(address2)) ? " (address1.equals(address2)) 같다.":" (address1.equals(address2)) 다르다.");

결과

(address1.equals(address2)) 다르다.

Object 의 equals 의 기본값이 ==를 사용하기 때문에 반드시 재정의가 필요하다.

public boolean equals(Object obj) {
        return (this == obj);
    }

equals 재정의

@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return Objects.equals(city, address.city) && Objects.equals(street, address.street) && Objects.equals(zipcode, address.zipcode);
    }
@Override
public int hashCode() {
    return Objects.hash(city, street, zipcode);
}
#### main 
```java
System.out.println((address1.equals(address2)) ? " (address1.equals(address2)) 같다.":" (address1.equals(address2)) 다르다.");

결과

(address1.equals(address2)) 같다.

🍇 Collection

DB에서는 Collection 을 저장하는 구조가 존재하지
않게 때문에 연관관계 ENTITY를 설정한다. 엔티티가 아닌 값타입이 아닌 경우 어떻게 표현해야할까?

Collection 타입

Member

@Entity
public class Member extends BaseEntity
{
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    // 기간 Period
    @Embedded
    private Period period;

    // 주소 Address
    @Embedded
    private Address homeaddress;


    @ElementCollection
    @CollectionTable(
            name = "FAVORITE_FOOD",
            joinColumns = @JoinColumn(name = "MEMBER_ID")
    )
    @Column(name = "FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<>();


    @ElementCollection
    @CollectionTable(
            name = "ADDRESS",
            joinColumns = @JoinColumn(name = "MEMBER_ID")
    )
    private List<Address> addressHisotry = new ArrayList<>();

값 타입을 복수로 저장할 때 사용한다.
JPA 에서 복수로 값 타입을 할당하기 위해서 테이블을 생성한다.

@ElementCollection : Collection 객체임을 JPA 에게 선언하며,
Entity 가 아닌 Embedded 값 타입에 대한 테이블을 1 : N 연관관계로 생성한다.
Member : ADDRESS = 1 : N

@CollectionTable : Collection 테이블의 속성 값을 할당한다. Table 의 이름이나, 외래키 이름등을 설정

Collection 타입 저장

main

            Member member = new Member();
            member.setName("member1");

            //FOOD INSERT
            member.getFavoriteFoods().add("삼겹살");
            member.getFavoriteFoods().add("짜파게티");
            member.getFavoriteFoods().add("스파게티");

            // ADDRESS INSERT
            member.getAddressHisotry().add(new Address("o1","o1","o1"));
            member.getAddressHisotry().add(new Address("o2","o2","o2"));

결과

Hibernate: 
    /* insert org.example.entity.Member
        */ insert 
        into
            Member
            (createDate, createUser, modifyDate, modifyUser, name, city, street, ZIPCODE, endDate, startDate, TEAM_ID, id) 
        values
            (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row org.example.entity.Member.addressHisotry */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, ZIPCODE) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row org.example.entity.Member.addressHisotry */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, ZIPCODE) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row org.example.entity.Member.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row org.example.entity.Member.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row org.example.entity.Member.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)

같은 생명주기로 관리되어 Member와 함께 관리 된다. CASCADE.ALL

Collection 타입 조회

main

Member findMember = em.find(Member.class, member.getId());

            System.out.println("ADDRESS");
            List<Address> findAddresses = findMember.getAddressHisotry();
            for (Address findAddress : findAddresses) {
                System.out.println(findAddress.getCity());
            }

            System.out.println("::::::::::::::::::");

            Set<String> findFavoriteFoods = findMember.getFavoriteFoods();
            for (String findFavoriteFood : findFavoriteFoods) {
                System.out.println(findFavoriteFood);
            }
            System.out.println("::::::::::::::::::");

결과

Hibernate: 
    select
        member0_.id as id1_3_0_,
        member0_.createDate as createDa2_3_0_,
        member0_.createUser as createUs3_3_0_,
        member0_.modifyDate as modifyDa4_3_0_,
        member0_.modifyUser as modifyUs5_3_0_,
        member0_.name as name6_3_0_,
        member0_.city as city7_3_0_,
        member0_.street as street8_3_0_,
        member0_.ZIPCODE as ZIPCODE9_3_0_,
        member0_.endDate as endDate10_3_0_,
        member0_.startDate as startDa11_3_0_,
        member0_.TEAM_ID as TEAM_ID12_3_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
ADDRESS
Hibernate: 
    select
        addresshis0_.MEMBER_ID as MEMBER_I1_0_0_,
        addresshis0_.city as city2_0_0_,
        addresshis0_.street as street3_0_0_,
        addresshis0_.ZIPCODE as ZIPCODE4_0_0_ 
    from
        ADDRESS addresshis0_ 
    where
        addresshis0_.MEMBER_ID=?
o1
o2
::::::::::::::::::
Hibernate: 
    select
        favoritefo0_.MEMBER_ID as MEMBER_I1_2_0_,
        favoritefo0_.FOOD_NAME as FOOD_NAM2_2_0_ 
    from
        FAVORITE_FOOD favoritefo0_ 
    where
        favoritefo0_.MEMBER_ID=?
짜파게티
스파게티
삼겹살

Member findMember = em.find(Member.class, member.getId());
이때 Member 필드들만 조회 되었다.
CollectionADDRESS, FAVORITE_FOOD은 해당 Collection 을 사용할 때
조회 됨으로 지연 로딩 임을 알 수 있다.

Collection 타입 수정

main

System.out.println("::::::::::::::::::");
            Member findMember = em.find(Member.class, member.getId());

            Address address = findMember.getHomeaddress();
            findMember.setHomeaddress(new Address("nc",address.getStreet(), address.getZipcode()));

            //
            findMember.getFavoriteFoods().remove("삼겹살");
            findMember.getFavoriteFoods().add("보쌈");

결과

Hibernate: 
    select
        member0_.id as id1_3_0_,
        member0_.createDate as createDa2_3_0_,
        member0_.createUser as createUs3_3_0_,
        member0_.modifyDate as modifyDa4_3_0_,
        member0_.modifyUser as modifyUs5_3_0_,
        member0_.name as name6_3_0_,
        member0_.city as city7_3_0_,
        member0_.street as street8_3_0_,
        member0_.ZIPCODE as ZIPCODE9_3_0_,
        member0_.endDate as endDate10_3_0_,
        member0_.startDate as startDa11_3_0_,
        member0_.TEAM_ID as TEAM_ID12_3_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
Hibernate: 
    select
        favoritefo0_.MEMBER_ID as MEMBER_I1_2_0_,
        favoritefo0_.FOOD_NAME as FOOD_NAM2_2_0_ 
    from
        FAVORITE_FOOD favoritefo0_ 
    where
        favoritefo0_.MEMBER_ID=?
Hibernate: 
    /* update
        org.example.entity.Member */ update
            Member 
        set
            createDate=?,
            createUser=?,
            modifyDate=?,
            modifyUser=?,
            name=?,
            city=?,
            street=?,
            ZIPCODE=?,
            endDate=?,
            startDate=?,
            TEAM_ID=? 
        where
            id=?
Hibernate: 
    /* delete collection row org.example.entity.Member.favoriteFoods */ delete 
        from
            FAVORITE_FOOD 
        where
            MEMBER_ID=? 
            and FOOD_NAME=?
Hibernate: 
    /* insert collection
        row org.example.entity.Member.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)

값 타입은 추적이 되지 않기 때문에 수정하려는 값을 삭제하고 수정 값을 넣어준다.

Embedded Type Collection 수정

main

            // remove 는 기본적으로 equals 메소드를 기반으로 동작
            findMember.getAddressHisotry().remove(new Address("o1","o1","o1"));
            findMember.getAddressHisotry().add(new Address("n1","o1","o1"));

Embedded Type 역시 값 타입이라 수정할 값을 삭제하고 값을 추가해야한다.
Collection 타입의 removeequals 로 동작하기 때문에
수정 해야 할 Embedded Type 을 삭제하고 새로 생성한다.

결과

Hibernate: 
    select
        member0_.id as id1_3_0_,
        member0_.createDate as createDa2_3_0_,
        member0_.createUser as createUs3_3_0_,
        member0_.modifyDate as modifyDa4_3_0_,
        member0_.modifyUser as modifyUs5_3_0_,
        member0_.name as name6_3_0_,
        member0_.city as city7_3_0_,
        member0_.street as street8_3_0_,
        member0_.ZIPCODE as ZIPCODE9_3_0_,
        member0_.endDate as endDate10_3_0_,
        member0_.startDate as startDa11_3_0_,
        member0_.TEAM_ID as TEAM_ID12_3_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
Hibernate: 
    select
        addresshis0_.MEMBER_ID as MEMBER_I1_0_0_,
        addresshis0_.city as city2_0_0_,
        addresshis0_.street as street3_0_0_,
        addresshis0_.ZIPCODE as ZIPCODE4_0_0_ 
    from
        ADDRESS addresshis0_ 
    where
        addresshis0_.MEMBER_ID=?
Hibernate: 
    /* delete collection org.example.entity.Member.addressHisotry */ delete 
        from
            ADDRESS 
        where
            MEMBER_ID=?
Hibernate: 
    /* insert collection
        row org.example.entity.Member.addressHisotry */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, ZIPCODE) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row org.example.entity.Member.addressHisotry */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, ZIPCODE) 
        values
            (?, ?, ?, ?)

INSERT Query 가 두번 일어났다. 이는 Embedded 값 타입 Collection 에 값이 변경되면 현재 값을 모두 다시 저장하기 때문에 두번의 INSERT 가 발생했다.

Collection 값 타입을 더 좋게..

값 타입은 Entity 가 존재하지 않기 때문에 추적이 어렵고 그로 인한 수정이 더욱 어렵다.
따라서 값 타입 Collection 을 사용하는 것 보다는 값 타입 Collection 필드를 가진 Entitiy를 정의하여 사용하는 편이 좋다.

AddressEntity

@Entity
@Table(name = "ADDRESS_HISTORY")
public class AddressEntity
{
    @Id @GeneratedValue
    private  Long id;

    @Embedded
    private  Address address;

전체 요약

@Entity 를 제외한 나머지 타입을 값 타입이라고 하며
유동적인 설계가 가능하지만 참조특성으로 인해 불변객체나 수정불가능하게 끔 고려해야하며
공유되어선 안된다. 동등성 검증을 위해 값 타입에서는 Equals 메소드를 재정의해야한다.
특히 Collection 객체는 가급적 사용하지 않는 것이 좋다.

0개의 댓글