새로운 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;
...
}
이렇게 두면 Alien
은 alien_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
(?, ?)
table
로 alien_table
을 생성하고 alien_name
column을 만든 것을 볼 수 있다. tech
맴버 변수가 없는데, 이는 @Transient
annotation으로 마킹되었기 때문이다.
만약 database에 저장되는 table의 특정 맴버 변수가 또 다른 객체라면 어떻게 될까? 즉, database 측면에서 본다면 복합 데이터라면 hibernate에서 어떻게 이를 처리할 것이냐는 것이다.
테스트를 위해서 먼저 이전에 만들었던 Alien
table을 없애자
DROP TABLE alien;
다음으로 Laptop
class를 만들도록 하자.
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의 맴버 변수로 사용하도록 하자.
@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 처럼 넣으라는 것이다.
import jakarta.persistence.Embeddable;
@Embeddable
public class Laptop {
private String brand;
private String model;
private int ram;
...
}
@Embeddable
annotation을 추가함에 따라서 Laptop
은 다른 Entity
들에 맴버 변수로 임베딩이 가능한 것이다.
이제 Alien table 생성 코드를 실행해보도록 하자.
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을 삭제하여 초기화 시키자.
DROP TABLE alien;
다음으로 get
을 사용해서 저장된 data를 가져와 Laptop
객체에 잘 들어가는 지 확인하도록 하자.
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 특성 때문인데, 이에 대해서는 다음에 알아보도록 하자.
Laptop
을 임베딩하였지만, Laptop
을 하나의 table로서 다루고 싶다면 @Entity
로 묶어내면 된다. 이렇게 되면 Alien
과 Laptop
사이에 table 관계가 생기게 된다. 즉, PK-FK 구조를 갖게 되는 것이다.
먼저 Laptop
을 하나의 table entity로 만들어주어야 한다.
@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이 필요하다.
@Entity
public class Alien {
@Id
private int aid;
private String aname;
private String tech;
@OneToOne
private Laptop laptop;
...
}
@OneToOne
annotation으로 laptop
과 Alien
이 one-to-one 관계를 가지고 있다는 것을 나타내고 있다.
이제 코드를 실행해보도록 하자.
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로 맵핑한다.
그런데 실제로는 하나의 Alien
은 여러 개의 laptop을 소유할 수 있다. 이는 Alient
의 한 인스턴스가 여러 개의 laptop을 소유할 수 있다는 것과 같다.
이를 위해서 Alien
의 laptop
맴버 변수 타입을 Laptop
에서 List<Laptop>
으로 바꿔야한다.
@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 코드에서 실행햅도록 하자.
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
라는 맵핑 테이블이 부수적으로 생겼다는 것이다. 이는 Alien
과 Laptop
은 서로 직접적으로 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_aid
와 laptop
table의 PK인 laptop_lid
가 있는 것을 볼 수 있다.
반면에 alien
과 laptop
에는 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
을 가질 수 있으므로 alien
은 one
이고, laptop
은 many
이다. 반면에 laptop
한 개에 대해서는 한 개의 alien
에게만 소유되므로 Many-to-Many
관계는 아니다. 따라서, laptop
이 FK로 alien
의 PK를 가지고 있어야 한다.
만약 alien이 laptop에 대한 PK를 FK로 들고 있다면 다음과 같은 상황이 발생한다.
aid | aname | tech | laptop_id |
---|---|---|---|
1 | Gildong | java | 100 |
1 | Gildong | java | 102 |
duplicated key가 발생하는 것이다. 따라서, one
쪽에 있는 alien
이 FK를 가지고 있는 것이 아니라, many
쪽에 있는 laptop
이 FK를 가져야 한다.
따라서 다음과 같이 laptop
을 바꾸도록 하자.
@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
이라는 맴버 변수 이름을 써주는 것이다. alien
은 Laptop
의 @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_aid
가 alien
에 대한 FK임을 나타내고 있는 것이다.
만약 Alien
과 Laptop
이 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에 대한 맵핑 테이블을 하나 씩 만들기 때문이다. 즉, @ManyToMany
가 Alien
과 Laptop
class에 하나씩 있어서 생기는 것이다. 맵핑 테이블 하나만 만들고 싶다면 한쪽의 @ManyToMany
에 mappedBy
를 사용해주면 된다. 이렇게 되면 mappedBy
가 있는 쪽은 없는 쪽이 생성한 table에 의지하게 된다. 맵핑 table을 생성하고 정교화 할 수 있는 쪽을 owning side라고 하고, mappedBy
로 owining side쪽을 따라가는 것을 non-owning side라고 한다. mappedBy는 항상 non-owning side에 두어야 하는 것이다.
우리의 경우는 Laptop
이 Alien
에 따라가도록 하기 위해서 다음과 같이 @ManyToMany
에 mappedBy
를 추가해주도록 하자.
@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
를 넣어주도록 하자.
@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를 실행해보도록 하자.
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 |
alien
과 laptop
각각에 FK가 없고 alien_laptop
에 FK로 alien
과 laptop
PK를 들고 있는 것을 볼 수 있다.