새로운 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를 들고 있는 것을 볼 수 있다.