Java 재활 훈련 17일차 - Hibernate2 - Mapping

0

java

목록 보기
17/18

Hibernate 두번째

Table과 Column 이름 바꾸기

새로운 Alien class를 만들어보도록 하자.

@Entity
public class Alien {
    @Id
    private int aid;
    private String aname;
    private String tech;

    public int getAid() {
        return aid;
    }

    public void setAid(int aid) {
        this.aid = aid;
    }

    public String getAname() {
        return aname;
    }

    public void setAname(String aname) {
        this.aname = aname;
    }

    public String getTech() {
        return tech;
    }

    public void setTech(String tech) {
        this.tech = tech;
    }

    @Override
    public String toString() {
        return "Alien{" +
                "aid=" + aid +
                ", aname='" + aname + '\'' +
                ", tech='" + tech + '\'' +
                '}';
    }
}

hibernate를 통해서 Alien class를 table로 만들어 보자.

public class Main {
    public static void main(String[] args) {
        Alien alien = new Alien();
        alien.setAid(1);
        alien.setAname("Navin");
        alien.setTech("Java");

        Configuration cfg = new Configuration().configure();
        cfg.addAnnotatedClass(Alien.class);

        SessionFactory sf = cfg.buildSessionFactory();
        Session session = sf.openSession();

        Transaction tx = session.beginTransaction();

        session.persist(alien);

        tx.commit();

        session.close();
        sf.close();
    }
}

위코드를 실행하면, 아래의 결과가 나오게 된다.

Hibernate: 
    create table Alien (
        aid integer not null,
        aname varchar(255),
        tech varchar(255),
        primary key (aid)
    )
Hibernate: 
    insert 
    into
        Alien
        (aname, tech, aid) 
    values
        (?, ?, ?)

기존에 해당 table이 없었으므로 create table를 통해서 table을 만드는 것을 볼 수 있다. column으로는 aid, aname,tech가 있다.

그런데, java에서는 변수 convention으로 camel case를 사용하는 반면에 sql에서는 snake case를 사용하는 경우가 꽤 있다. 또한, object 관점과 달리 data 관점에서는 다른 이름으로 저장되고 싶을 때가 있다.

이러한 경우 hibernate의 annotation을 통해서 sql에 저장되는 이름을 다르게 할 수 있다.

먼저 table 이름인 Alien을 다른 이름으로 만들어보도록 하자. @Entity에 파라미터로 name을 주어 설정해주어도 되고, 명시적으로 @Table을 사용하여 name을 설정해주어도 된다.

@Entity
@Table(name = "alien_table")
public class Alien {
    @Id
    private int aid;
    private String aname;
    ...
}

이렇게 두면 Alienalien_table이라는 table 이름을 갖게 된다.

다음으로 맴버 변수의 이름 그대로 데이터 베이스에 저장되지 않도록 하고 싶다. 가령 aname이 아니라 alien_name으로 저장하고 싶다면 @Column을 사용하면 된다.

@Entity
@Table(name = "alien_table")
public class Alien {
    @Id
    private int aid;
    @Column(name = "alien_name")
    private String aname;
    ...
}

@Column(name = "alien_name")을 사용하면 된다. aname 맴버 변수가 데이터 베이스에 alien_name으로 저장된다.

만약, 특정 맴버 변수는 database에 저장하고 싶지 않다면 어떻게 해야할까? 이러한 경우에는 @Transient를 사용하면 된다.

@Entity
@Table(name = "alien_table")
public class Alien {
    @Id
    private int aid;
    @Column(name = "alien_name")
    private String aname;

    @Transient
    private String tech;
    ...
}

이렇게 설정하면 tech 맴버 변수는 alien_table에 저장되지 않는다.

이제 java 코드를 실행하여 실제 table이 어떻게 생성되었는 지 확인해보도록 하자.

Hibernate: 
    create table alien_table (
        aid integer not null,
        alien_name varchar(255),
        primary key (aid)
    )
Hibernate: 
    insert 
    into
        alien_table
        (alien_name, aid) 
    values
        (?, ?)

tablealien_table을 생성하고 alien_name column을 만든 것을 볼 수 있다. tech 맴버 변수가 없는데, 이는 @Transient annotation으로 마킹되었기 때문이다.

Embeddable

만약 database에 저장되는 table의 특정 맴버 변수가 또 다른 객체라면 어떻게 될까? 즉, database 측면에서 본다면 복합 데이터라면 hibernate에서 어떻게 이를 처리할 것이냐는 것이다.

테스트를 위해서 먼저 이전에 만들었던 Alien table을 없애자

  • psql
DROP TABLE alien;

다음으로 Laptop class를 만들도록 하자.

  • Laptop.java
public class Laptop {
    private String brand;
    private String model;
    private int ram;

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public int getRam() {
        return ram;
    }

    public void setRam(int ram) {
        this.ram = ram;
    }

    @Override
    public String toString() {
        return "Laptop{" +
                "brand='" + brand + '\'' +
                ", model='" + model + '\'' +
                ", ram=" + ram +
                '}';
    }
}

Laptop class를 Alien class의 맴버 변수로 사용하도록 하자.

  • Alien.java
@Entity
public class Alien {
    @Id
    private int aid;
    private String aname;
    private String tech;
    private Laptop laptop;
    ...
}

hibernate는 이 Laptop을 어떻게 처리할까?? 우리가 원하는 것은 복합 데이터이지 Laptop이라는 새로운 테이블을 만들어 연결하는 것이 아니다.

즉, alien table의 모양이 다음과 같이 나와야하는 것이다.

Column: [a1d, aname, tech, brand, model, ram]

실제로 hibernate를 통해서 table 생성 명령어를 실행하면 다음과 같이 나온다.

Could not determine recommended JdbcType for Java type 'org.example.Laptop'

Laptop 객체를 어떤 database type으로 처리할 지 모르겠다고 말하는 것이다.

이 문제를 해결하기 위해서 hibernate에게 Laptop class를 Alien class가 임베딩하고 있다고 표시해주어야 한다. 즉, Laptop은 임베딩 class라는 것을 알려주어, database에서 table을 만들 때 해당 임베딩 class를 임베딩한 class에 마치 하나의 table 처럼 넣으라는 것이다.

  • Laptop.java
import jakarta.persistence.Embeddable;

@Embeddable
public class Laptop {
    private String brand;
    private String model;
    private int ram;
    ...
}

@Embeddable annotation을 추가함에 따라서 Laptop은 다른 Entity들에 맴버 변수로 임베딩이 가능한 것이다.

이제 Alien table 생성 코드를 실행해보도록 하자.

  • Main.java
public class Main {
    public static void main(String[] args) {
        Alien alien = new Alien();

        Laptop l1 = new Laptop();
        l1.setBrand("Asus");
        l1.setModel("Rog");
        l1.setRam(16);

        alien.setAid(104);
        alien.setAname("Navin");
        alien.setTech("Java");
        alien.setLaptop(l1);

        Configuration cfg = new Configuration().configure();
        cfg.addAnnotatedClass(Alien.class);

        SessionFactory sf = cfg.buildSessionFactory();
        Session session = sf.openSession();

        Transaction tx = session.beginTransaction();

        session.persist(alien);

        tx.commit();

        session.close();
        sf.close();
    }
}

그 결과는 다음과 같다.

Hibernate: 
    create table Alien (
        aid integer not null,
        aname varchar(255),
        brand varchar(255),
        model varchar(255),
        ram integer,
        tech varchar(255),
        primary key (aid)
    )
Hibernate: 
    insert 
    into
        Alien
        (aname, brand, model, ram, tech, aid) 
    values
        (?, ?, ?, ?, ?, ?)

Laptop class에 있던 맴버 변수들이 Alien안에 임베딩된 것을 볼 수 있다.

alien table을 삭제하여 초기화 시키자.

  • psql
DROP TABLE alien;

다음으로 get을 사용해서 저장된 data를 가져와 Laptop 객체에 잘 들어가는 지 확인하도록 하자.

  • Main.java
public class Main {
    public static void main(String[] args) {
        ...
        Transaction tx = session.beginTransaction();

        session.persist(alien);

        tx.commit();

        Alien a2 = session.get(Alien.class, 104);
        System.out.println(a2);

        session.close();
        sf.close();
    }
}

session.get(Alien.class, 104)가 추가된 것이다. 위 코드를 실행해보면 다음과 같은 결과가 나온다.

Hibernate: 
    create table Alien (
        aid integer not null,
        aname varchar(255),
        brand varchar(255),
        model varchar(255),
        ram integer,
        tech varchar(255),
        primary key (aid)
    )
Hibernate: 
    insert 
    into
        Alien
        (aname, brand, model, ram, tech, aid) 
    values
        (?, ?, ?, ?, ?, ?)
Alien{aid=104, aname='Navin', tech='Java', laptop=Laptop{brand='Asus', model='Rog', ram=16}}

데이터를 성공적으로 a2 변수안에 가져오긴 했지만, 왜 인지모르게 sql문에 select가 없다. 왜일까?? 이는 hibernate의 cache 특성 때문인데, 이에 대해서는 다음에 알아보도록 하자.

OneToOne Mapping

Laptop을 임베딩하였지만, Laptop을 하나의 table로서 다루고 싶다면 @Entity로 묶어내면 된다. 이렇게 되면 AlienLaptop 사이에 table 관계가 생기게 된다. 즉, PK-FK 구조를 갖게 되는 것이다.

먼저 Laptop을 하나의 table entity로 만들어주어야 한다.

  • Laptop.java
@Entity
public class Laptop {
    @Id
    private int lid;
    private String brand;
    private String model;
    private int ram;
    ...
}

@Entity@Id를 추가해주었다.

다음으로 Alien에도 처리를 해주어야하는데, Alien에 아무런 처리를 해주지 않으면 Laptop에 대해서 Alien과 정확히 어떤 관계성을 가지는 지 모른다. 즉, one-to-one인지, one-to-many인지, many-to-many인지 알 길이 없다는 것이다. 따라서, 이를 표기해주는 annotation이 필요하다.

  • Alien.java
@Entity
public class Alien {
    @Id
    private int aid;
    private String aname;
    private String tech;
    @OneToOne
    private Laptop laptop;
    ...
}

@OneToOne annotation으로 laptopAlien이 one-to-one 관계를 가지고 있다는 것을 나타내고 있다.

이제 코드를 실행해보도록 하자.

  • Main.java
public class Main {
    public static void main(String[] args) {
        Alien alien = new Alien();

        Laptop l1 = new Laptop();
        l1.setLid(1);
        l1.setBrand("Asus");
        l1.setModel("Rog");
        l1.setRam(16);

        alien.setAid(104);
        alien.setAname("Navin");
        alien.setTech("Java");
        alien.setLaptop(l1);

        Configuration cfg = new Configuration().configure();
        cfg.addAnnotatedClass(Alien.class);
        cfg.addAnnotatedClass(Laptop.class);

        SessionFactory sf = cfg.buildSessionFactory();
        Session session = sf.openSession();
        Transaction tx = session.beginTransaction();

        session.persist(l1);
        session.persist(alien);
        tx.commit();

        session.close();
        sf.close();
    }
}

먼저 Laptop 클래스에 대한 인스턴스로 l1이 만들어졌고, session.persist(l1)을 해주어야 한다. 그래야 alien 입장에서 l1에 대한 FK 제약사항을 통과할 수 있는데, FK에 해당하는 data가 없으면 저장을 하지 않기 때문이다.

실행해보면 다음의 sql문들이 실행되었다고 나온다.

Hibernate: 
    create table Alien (
        aid integer not null,
        aname varchar(255),
        tech varchar(255),
        laptop_lid integer,
        primary key (aid)
    )
Hibernate: 
    alter table if exists Alien 
       drop constraint if exists UKcoq8njscbevtpjx66jmyk749n
Hibernate: 
    alter table if exists Alien 
       add constraint UKcoq8njscbevtpjx66jmyk749n unique (laptop_lid)
Hibernate: 
    alter table if exists Alien 
       add constraint FKbi5qvtlytmkcbw75r20numuvd 
       foreign key (laptop_lid) 
       references Laptop
Hibernate: 
    insert 
    into
        Laptop
        (brand, model, ram, lid) 
    values
        (?, ?, ?, ?)
Hibernate: 
    insert 
    into
        Alien
        (aname, laptop_lid, tech, aid) 
    values
        (?, ?, ?, ?)

Alien table을 만들고, FK constraint를 만든다. 이후 Laptop table 데이터를 추가하고, Alien table에 데이터를 추가할 때 laptop_lid를 FK로 사용하여 Laptop에 있는 특정 row를 1대1로 맵핑한다.

OneToMany와 ManyToOne

그런데 실제로는 하나의 Alien은 여러 개의 laptop을 소유할 수 있다. 이는 Alient의 한 인스턴스가 여러 개의 laptop을 소유할 수 있다는 것과 같다.

이를 위해서 Alienlaptop 맴버 변수 타입을 Laptop에서 List<Laptop>으로 바꿔야한다.

  • Alien.java
@Entity
public class Alien {
    @Id
    private int aid;
    private String aname;
    private String tech;
    @OneToMany
    private List<Laptop> laptop;
    ...
}

laptop의 type을 List<Laptop>으로 바꾸고 @OneToMany mapping 관계를 가지도록 하였다. 이제 Main 코드에서 실행햅도록 하자.

  • Main.java
public class Main {
    public static void main(String[] args) {
        Alien alien = new Alien();

        Laptop l1 = new Laptop();
        l1.setLid(1);
        l1.setBrand("Asus");
        l1.setModel("Rog");
        l1.setRam(16);

        Laptop l2 = new Laptop();
        l1.setLid(2);
        l1.setBrand("Dell");
        l1.setModel("XPS");
        l1.setRam(32);

        alien.setAid(104);
        alien.setAname("Navin");
        alien.setTech("Java");
        alien.setLaptop(Arrays.asList(l1, l2));

        Configuration cfg = new Configuration().configure();
        cfg.addAnnotatedClass(Alien.class);
        cfg.addAnnotatedClass(Laptop.class);

        SessionFactory sf = cfg.buildSessionFactory();
        Session session = sf.openSession();
        Transaction tx = session.beginTransaction();

        session.persist(l1);
        session.persist(l2);

        session.persist(alien);
        tx.commit();

        session.close();
        sf.close();
    }
}

실행시켜보면 다음의 결과가 나온다.

Hibernate: 
    create table Alien (
        aid integer not null,
        aname varchar(255),
        tech varchar(255),
        primary key (aid)
    )
Hibernate: 
    create table Alien_Laptop (
        Alien_aid integer not null,
        laptop_lid integer not null
    )
Hibernate: 
    create table Laptop (
        lid integer not null,
        brand varchar(255),
        model varchar(255),
        ram integer not null,
        primary key (lid)
    )
Hibernate: 
    alter table if exists Alien_Laptop 
       drop constraint if exists UKgyspwgrnkut4hbtlwqcfct7fe
Hibernate: 
    alter table if exists Alien_Laptop 
       add constraint UKgyspwgrnkut4hbtlwqcfct7fe unique (laptop_lid)
Hibernate: 
    alter table if exists Alien_Laptop 
       add constraint FKp0a030ntp8fwysxwtd665029j 
       foreign key (laptop_lid) 
       references Laptop
Hibernate: 
    alter table if exists Alien_Laptop 
       add constraint FKf2y56ehyfym5dmcdy736otfcw 
       foreign key (Alien_aid) 
       references Alien
Hibernate: 
    insert 
    into
        Laptop
        (brand, model, ram, lid) 
    values
        (?, ?, ?, ?)
Hibernate: 
    insert 
    into
        Laptop
        (brand, model, ram, lid) 
    values
        (?, ?, ?, ?)
Hibernate: 
    insert 
    into
        Alien
        (aname, tech, aid) 
    values
        (?, ?, ?)
Hibernate: 
    insert 
    into
        Alien_Laptop
        (Alien_aid, laptop_lid) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        Alien_Laptop
        (Alien_aid, laptop_lid) 
    values
        (?, ?)

여기서 재미난 점은 Alien_Laptop라는 맵핑 테이블이 부수적으로 생겼다는 것이다. 이는 AlienLaptop은 서로 직접적으로 PF-FK로 연결된 구조가 아니라, Alien_Laptop라는 간접 참조 테이블을 통해서 연결성을 맺고 있다는 것을 알 수 있다.

실제로 생성된 table들을 확인해보도록 하자.

\d
 public | alien        | table | postgres
 public | alien_laptop | table | postgres
 public | laptop       | table | postgres

3개의 table이 생성되었고, alien_laptop을 확인해보도록 하자.

\d+ alien_laptop
 alien_aid  | integer |           | not null |         | plain   |             |              | 
 laptop_lid | integer |           | not null |         | plain   |             |              | 

alien table의 PK인 alien_aidlaptop table의 PK인 laptop_lid가 있는 것을 볼 수 있다.

반면에 alienlaptop에는 FK가 서로 없는 것을 볼 수 있다.

\d+ alien
 aid    | integer                |           | not null |         | plain    |             |              | 
 aname  | character varying(255) |           |          |         | extended |             |              | 
 tech   | character varying(255) |           |          |         | extended |             |              | 
 
\d+ laptop
 lid    | integer                |           | not null |         | plain    |             |              | 
 brand  | character varying(255) |           |          |         | extended |             |              | 
 model  | character varying(255) |           |          |         | extended |             |              | 
 ram    | integer                |           | not null |         | plain    |             |              | 

왜 제 3자 table이 만들어진 것일까? 만약 세번째 table 없이 두 table 끼리의 one-to-many 관계성을 만족시키려면, Many를 가지고 있는 곳에서 One쪽으로 FK를 가져야하기 때문이다.

가령 alien 한 명이, 여러 개의 laptop을 가질 수 있으므로 alienone이고, laptopmany이다. 반면에 laptop 한 개에 대해서는 한 개의 alien에게만 소유되므로 Many-to-Many 관계는 아니다. 따라서, laptop이 FK로 alien의 PK를 가지고 있어야 한다.

만약 alien이 laptop에 대한 PK를 FK로 들고 있다면 다음과 같은 상황이 발생한다.

aidanametechlaptop_id
1Gildongjava100
1Gildongjava102

duplicated key가 발생하는 것이다. 따라서, one쪽에 있는 alien이 FK를 가지고 있는 것이 아니라, many쪽에 있는 laptop이 FK를 가져야 한다.

따라서 다음과 같이 laptop을 바꾸도록 하자.

  • laptop.java
@Entity
public class Laptop {
    @Id
    private int lid;
    private String brand;
    private String model;
    private int ram;
    @ManyToOne
    private Alien alien;
    ...
}

Laptop class에 맴버 변수로 alien이라는 FK를 만들되 반드시 그 클래스 타입으로 지정해주어야 한다. 또한, ManyToOne annotation도 써주어야 한다.

단, 여기서 끝나는 것이 아니라, Laptop class의 alien 변수를 Alien class에 표시해주어야 한다.

@Entity
public class Alien {
    @Id
    private int aid;
    private String aname;
    private String tech;
    @OneToMany(mappedBy = "alien")
    private List<Laptop> laptop;
    ...
}

@OneToMany(mappedBy = "alien")으로 mappedBy안에 alien이라는 맴버 변수 이름을 써주는 것이다. alienLaptop@ManyToOne 맴버 변수인 alien에 해당하는 것이다. 즉, FK의 주인인 Laptop에 의해서 Alien이 참조된다는 것을 mappedBy라고 표현한 것이다.

이제 기존 table들을 모두 지워버리고 새로 시작해보도록 하자.

DROP TABLE alien CASCADE;
DROP TABLE laptop CASCADE;
DROP TABLE alien_laptop CASCADE;

main code를 실행하면 다음의 sql 문이 나온다.

Hibernate: 
    create table Alien (
        aid integer not null,
        aname varchar(255),
        tech varchar(255),
        primary key (aid)
    )
Hibernate: 
    create table Laptop (
        lid integer not null,
        brand varchar(255),
        model varchar(255),
        ram integer not null,
        alien_aid integer,
        primary key (lid)
    )
...

두 개의 table들이 만들어지고 alien_aidalien에 대한 FK임을 나타내고 있는 것이다.

ManyToMany

만약 AlienLaptop이 ManyToMany 관계를 가진다고 하자. 이러한 경우에는 제 3의 table이 만들어지는 것을 막을 수 없다. 두 테이블의 ManyToMany 관게성을 3번째 관계 테이블을 만들어 one-to-many 관계성으로 풀어내야 하는 것이다.

아래의 many-to-many 관계를

|Alien| ----(many-to-many)---- |Laptop|

one-to-many로 풀어내는 것이다.

|Alien| ---(one-to-many)--|Alien_Laptop|--(many-to-one)--- |Laptop|

문제는 hibernate에서 @ManyToMany를 그대로만 써주면 다음과 같이 맵핑 테이블이 만들어진다는 것이다.

           |Alien_Laptop|
          /             \
|Alien|---               --- |Laptop|
          \             /
           |Laptop_Alien|

이렇게 되는 이유는 @ManyToMany만 있는 경우에 hibernate는 상대 class에 대한 맵핑 테이블을 하나 씩 만들기 때문이다. 즉, @ManyToManyAlienLaptop class에 하나씩 있어서 생기는 것이다. 맵핑 테이블 하나만 만들고 싶다면 한쪽의 @ManyToManymappedBy를 사용해주면 된다. 이렇게 되면 mappedBy가 있는 쪽은 없는 쪽이 생성한 table에 의지하게 된다. 맵핑 table을 생성하고 정교화 할 수 있는 쪽을 owning side라고 하고, mappedBy로 owining side쪽을 따라가는 것을 non-owning side라고 한다. mappedBy는 항상 non-owning side에 두어야 하는 것이다.

우리의 경우는 LaptopAlien에 따라가도록 하기 위해서 다음과 같이 @ManyToManymappedBy를 추가해주도록 하자.

  • Laptop.java
@Entity
public class Laptop {
    @Id
    private int lid;
    private String brand;
    private String model;
    private int ram;
    @ManyToMany(mappedBy = "laptop")
    private List<Alien> alien;
    ...
}

다음으로 Alien class에도 @ManyToMany를 넣어주도록 하자.

  • Alien.java
@Entity
public class Alien {
    @Id
    private int aid;
    private String aname;
    private String tech;
    @ManyToMany
    private List<Laptop> laptop;
    ...
}

다음으로 table들을 모두 삭제해주도록 하자.

DROP TABLE alien CASCADE;
DROP TABLE laptop CASCADE;

이제 main code를 실행해보도록 하자.

  • Main.java
public class Main {
    public static void main(String[] args) {
        Alien alien = new Alien();

        Laptop l1 = new Laptop();
        l1.setLid(1);
        l1.setBrand("Asus");
        l1.setModel("Rog");
        l1.setRam(16);

        Laptop l2 = new Laptop();
        l1.setLid(2);
        l1.setBrand("Dell");
        l1.setModel("XPS");
        l1.setRam(32);

        alien.setAid(104);
        alien.setAname("Navin");
        alien.setTech("Java");
        alien.setLaptop(Arrays.asList(l1, l2));

        l1.setAlien(Arrays.asList(alien));

        Configuration cfg = new Configuration().configure();
        cfg.addAnnotatedClass(Alien.class);
        cfg.addAnnotatedClass(Laptop.class);

        SessionFactory sf = cfg.buildSessionFactory();
        Session session = sf.openSession();
        Transaction tx = session.beginTransaction();

        session.persist(l1);
        session.persist(l2);
        session.persist(alien);
        tx.commit();

        session.close();
        sf.close();
    }
}

실행해보면 다음과 같은 결과가 나온다.

\dt+
 public | alien        | table | postgres | permanent   | heap          | 16 kB      | 
 public | alien_laptop | table | postgres | permanent   | heap          | 8192 bytes | 
 public | laptop       | table | postgres | permanent   | heap          | 16 kB      | 
 
 \d alien
 aid    | integer                |           | not null | 
 aname  | character varying(255) |           |          | 
 tech   | character varying(255) |           |          | 

 \d alien_laptop
 alien_aid  | integer |           | not null | 
 laptop_lid | integer |           | not null | 

\d laptop
 lid    | integer                |           | not null | 
 brand  | character varying(255) |           |          | 
 model  | character varying(255) |           |          | 
 ram    | integer                |           | not null | 

alienlaptop 각각에 FK가 없고 alien_laptop에 FK로 alienlaptop PK를 들고 있는 것을 볼 수 있다.

0개의 댓글