연관 관계
- 엔티티들은 대부분 다른 엔티티와 연관 관계가 있다.
- 객체는 참조를 사용해서 관계를 맺고 엔티티는 키를 이용해 관계를 갖는다.
방향 : 단방향, 양방향 존재 -> 객체 관계에만 존재하고 테이블 관계는 항상 양방향
다중성 : 다대일, 일대다, 일대일, 다대다
연관관계의 주인 : 객체를 양방향 연관 관계로 만들면 연관관계의 주인을 정해야함
- 객체 A에 만 B를 참조 하고 있을때 A -> B는 가능하지만 B -> A는 불가능
- 테이블은 A Join B / B Join A 외래키만 알고 있다면 가능 (양방향)
다대일, 일대다 연관 관계
- 역과 노선이 있다.
- 지하철역은 하나의 노선에만 소속될 수 있다.
- 환승역은 고려하지 않는다.
- 지하철역과 노선은 다대일(N:1) 관계다.
- 노선과 지하철역은 일대다(1:N) 관계
@Entity
@Table(name = "station")
public class Station {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne // (1)
@JoinColumn(name = "line_id") // (2)
private Line line; // (3)
public void setLine(final Line line) { // (4)
this.line = line;
}
}
@ManyToOne
: 다대일(N:1) 관계라는 매핑 정보@JoinColumn
: 컬럼 이름과 외래 키가 참조할 컬럼을 직접 지정하지 않는다면 굳이 선언하지 않아도 됨- 지하철역 객체는
line
필드로 노선 객체와 연관 관계를 맺는다.- 지하철 역과 노선은 단방향 관계
package subway.domin;
import javax.persistence.*;
@Entity
@Table(name = "line")
public class Line {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
@Column(nullable = false)
private String name;
}
실행 결과
Hibernate:
create table line (
id bigint generated by default as identity,
name varchar(255) not null,
primary key (id)
)
Hibernate:
create table station (
id bigint generated by default as identity,
name varchar(255) not null,
line_id bigint,
primary key (id)
)
Hibernate:
alter table station
add constraint FKklalypfiiahjy57wtapf4w92
foreign key (line_id)
references line
- 외래키가 생성
예제
package subway.domin;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
class LineRepositoryTest {
@Autowired
private LineRepository lines;
@Autowired
private StationRepository stations;
@Test
void saveWithLine() {
Station expected = new Station("잠실역");
expected.setLine(new Line("2호선"));
Station actual = stations.save(expected);
stations.flush();
}
}
실행결과
Hibernate:
insert
into
station
(id, line_id, name)
values
(null, ?, ?)
2021-05-29 17:09:42.321 INFO 10640 --- [ Test worker] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext@4982f82c testClass = LineRepositoryTest, testInstance = subway.domin.LineRepositoryTest@4a38ebab, testMethod = saveWithLine@LineRepositoryTest, testException = org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : subway.domin.Station.line -> subway.domin.Line; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : subway.domin.Station.line -> subway.domin.Line, mergedContextConfiguration = [MergedContextConfiguration@34979333 testClass = LineRepositoryTest, locations = '{}', classes = '{class subway.Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory$DisableAutoConfigurationContextCustomizer@72629207, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@318197f, org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizer@351584c0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@ddfa6b7d, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@198799da, [ImportsContextCustomizer@7611d4d1 key = [org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration, org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration, org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration, org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration, org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration, org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerAutoConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@2c4b20fd, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@f27f658, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@0], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]
org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : subway.domin.Station.line -> subway.domin.Line; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : subway.domin.Station.line -> subway.domin.Line
- Station 을 저장하기 위해선 연관된 Line 은 영속정인 상태여야함
예제
@Test
void saveWithLine() {
Station expected = new Station("잠실역");
expected.setLine(lines.save(new Line("2호선")));
Station actual = stations.save(expected);
stations.flush();
}
실행결과
Hibernate:
insert
into
line
(id, name)
values
(null, ?)
Hibernate:
insert
into
station
(id, line_id, name)
values
(null, ?, ?)
Station을 저장하기 위해선 line_id가 필요하기 때문에 Line에 접근하기 때문에
Line의 데이터가 영속화된 상태여야한다.
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
실행전 data.sql 을 생성하면 초기화를 해준다
- resources/
data.sql
INSERT INTO line (id, name) VALUES (1, '3호선')
INSERT INTO station (id, line_id, name) VALUES (1, 1, '교대역')
예제
@Test
void findByNameWithLine() {
Station actual = stations.findByName("교대역");
assertThat(actual).isNotNull();
assertThat(actual.getLine().getName()).isEqualTo("3호선");
}
실행결과
Hibernate:
select
station0_.id as id1_1_,
station0_.line_id as line_id3_1_,
station0_.name as name2_1_
from
station station0_
where
station0_.name=?
Hibernate:
select
line0_.id as id1_0_0_,
line0_.name as name2_0_0_
from
line line0_
where
line0_.id=?
- JPA는 객체가 그래프를 그리는 것을 보장해준다.
UPDATE
예제
@Test
void updateWithLine() {
Station expected = stations.findByName("교대역");
expected.setLine(lines.save(new Line("2호선")));
stations.flush();
}
실행결과
Hibernate:
select
station0_.id as id1_1_,
station0_.line_id as line_id3_1_,
station0_.name as name2_1_
from
station station0_
where
station0_.name=?
Hibernate:
select
line0_.id as id1_0_0_,
line0_.name as name2_0_0_
from
line line0_
where
line0_.id=?
Hibernate:
insert
into
line
(id, name)
values
(null, ?)
Hibernate:
update
station
set
line_id=?,
name=?
where
id=?
java 코드로 쭉 작성하면
jvm 메모리에 떠있는 객체와 객체들을 가지고 실제로 쿼리를 만들어서 flush를 해준다.
연관관계 제거
예제
- null을 넣어준다.
@Test
void removeWithLine() {
Station expected = stations.findByName("교대역");
expected.setLine(null);
stations.flush();
}
실행결과
Hibernate:
select
station0_.id as id1_1_,
station0_.line_id as line_id3_1_,
station0_.name as name2_1_
from
station station0_
where
station0_.name=?
Hibernate:
select
line0_.id as id1_0_0_,
line0_.name as name2_0_0_
from
line line0_
where
line0_.id=?
Hibernate:
update
station
set
line_id=?,
name=?
where
id=?
양방향
- Line 에서도 Station 을 조회하기 위해 Station을 참조해준다.
@Entity
@Table(name = "line")
public class Line {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
protected Line() {
}
@Column(nullable = false)
private String name;
@OneToMany // (1)
private List<Station> stations = new ArrayList<>();
public Line(String name) {
this.name = name;
}
public Long getId() {
return Id;
}
public String getName() {
return name;
}
}
- (1) 과 같이 1:N 관계를 설정.
- 외래키가 설정이 없어 line 과 station 의 키를 담고 있는 테이블을 생성함
실행결과
Hibernate:
create table line (
id bigint generated by default as identity,
name varchar(255) not null,
primary key (id)
)
Hibernate:
create table line_station (
line_id bigint not null,
station_id bigint not null
)
Hibernate:
create table station (
id bigint generated by default as identity,
name varchar(255) not null,
line_id bigint,
primary key (id)
)
Hibernate:
alter table line_station
add constraint UK_m5y884wyh5p4r41lmd04bla0c unique (station_id)
Hibernate:
alter table line_station
add constraint FK78y8i0a0kum6n7s2qss4wi6i9
foreign key (station_id)
references station
Hibernate:
alter table line_station
add constraint FKawlxkihpbfvr39jka1t9jbbxu
foreign key (line_id)
references line
Hibernate:
alter table station
add constraint FKklalypfiiahjy57wtapf4w92
foreign key (line_id)
references line
- line에 외래키가 어떤 키인지 알려주는 설정을 한다
mappedBy
@OneToMany(mappedBy = "line")
private List<Station> station = new ArrayList<>();-
station 에 line 이 선언되어 있고 실제로 line 에는 Line_id라는 외래키가 명시되어 있다
@ManyToOne
@JoinColumn(name = "line_id")
private Line line;
실행결과
Hibernate:
create table line (
id bigint generated by default as identity,
name varchar(255) not null,
primary key (id)
)
Hibernate:
create table station (
id bigint generated by default as identity,
name varchar(255) not null,
line_id bigint,
primary key (id)
)
Hibernate:
alter table station
add constraint FKklalypfiiahjy57wtapf4w92
foreign key (line_id)
references line
- 지하철역과 노선은 양방향 관계다.
연관 관계의 주인
- 엄밀히 이야기하면 객체에는 양방향 연관 관계라는 것이 없다.
- 서로 다른 단방향 연관 관계 2개를 양방향인 것처럼 보이게 할 뿐이다.
- 지하철역과 노선은 다대일(N:1) 관계다.
- 노선과 지하철역은 일대다(1:N) 관계다.
- 연관 관계의 주인만이 데이터베이스 연관 관계와 매핑되고 외래 키를 등록, 수정, 삭제할 수 있다.
- 주인이 아닌 쪽은 읽기만 할 수 있다.
- 외래키의 관리자
예제
@Entity
@Table(name = "line")
public class Line {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
protected Line() {
}
@Column(nullable = false)
private String name;
@OneToMany(mappedBy = "line")
private List<Station> stations = new ArrayList<>();
public Line(String name) {
this.name = name;
}
public Long getId() {
return Id;
}
public String getName() {
return name;
}
public void addStation(Station station) {
stations.add(station);
}
}
@Test
void save() {
Line expected = new Line("2호선");
expected.addStation(new Station("잠실역"));
lines.save(expected);
lines.flush();
}
실행결과
Hibernate:
insert
into
station
(id, line_id, name)
values
(null, ?, ?) << line_id 에 null 이 들어감.
Hibernate:
insert
into
line
(id, name)
values
(null, ?)
- line 에서는 외래키를 등록 할수 없음
public void addStation(Station station) {
stations.add(station);
station.setLine(this);
}
- station에 직접 set을 해줘야함.
@Test
void save() {
Line expected = new Line("2호선");
expected.addStation(new Station("잠실역"));
lines.save(expected);
lines.flush();
}
실행결과
Hibernate:
insert
into
station
(id, line_id, name)
values
(null, ?, ?)
Hibernate:
insert
into
line
(id, name)
values
(null, ?)
Hibernate:
update
station
set
line_id=?,
name=?
where
id=?
주의
- line.java
public void addStation(Station station) {
stations.add(station);
station.setLine(this);
}
- station.java
public void setLine(Line line) {
this.line = line;
line.addStation(this);
}
양방향 관계를 가지려다 무한 루프에 빠짐.
연관관계의 주인인 line 에서는 addStation() 을 자제하거나 방어적인 코딩을 해야함
일대다로 조회하는 경우는 양방향으로 만든다. 단, 일대다 양방향 조회를 쓰되, 연관관계의 주인이 아닌 객체에서 연관관계의 매핑에 관한 것들을 수정하는 것을 지양한다.
일대다 단방향 매핑의 단점
- 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다.
- 연관 관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다.
- 일대다 단방향 매핑보다는 다대일 양방향 매핑을 권장한다.
일대일 관계의 에서는 '다'가 될 수 있는 확률이 큰 쪽에 외래키를 보관한다.
다대다 연관 관계
- 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
- 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.
- 연결 테이블에 필드가 추가되면 더는 사용할 수 없다.
'Programming > JPA' 카테고리의 다른 글
[JPA] Spring Data JPA (0) | 2021.05.29 |
---|