@OneToOne Mapping: Uni-Directional

One-to-One mapping 쀑 Uni-Directional 의 경우, μœ„μ™€ 같이 ꡬ쑰λ₯Ό 그릴 수 μžˆλ‹€. μ’…μ†λœ entity 의 PK λ₯Ό μƒμœ„ entity κ°€ FK 둜 μ‚¬μš©ν•˜λŠ” 것이닀.

κ·Έλ ‡λ‹€λ©΄ SQL μ—μ„œλŠ” μ–΄λ–»κ²Œ FK λ₯Ό μ§€μ •ν•΄μ•Ό ν•˜λŠ” κ²ƒμΌκΉŒ?

Defining Foreign Key
CREATE TABLE `instructor` (
	...
	CONSTRAINT `FK_DETAIL` FOREIGN KEY(`instructor_detail_id)
	REFERENCES `instructor_detail` (`id`)
);

CONSTRAINT FK_DETAIL 은 μ œμ•½ 쑰건의 이름을 λ‚˜νƒ€λ‚΄λŠ” 것이닀. μ—¬κΈ°μ„œ FK_DETAIL μ΄λΌλŠ” μ΄λ¦„μ˜ μ œμ•½ 쑰건은, instructor table 의 instructor_detail_id column 이, instructor_detail table 의 id column 을 μ°Έμ‘°ν•˜λ„λ‘ κ°•μ œν•˜λŠ” μ™Έλž˜ ν‚€ μ œμ•½ 쑰건(Foreign Key Constraint) 이닀.

More on Foreign Key

FK λ₯Ό μ‚¬μš©ν•¨μ— μžˆμ–΄μ„œ κ°€μž₯ μ£Όμš”ν•œ λͺ©μ μ€ Referential Integrity 이닀. Referential Integrity λŠ” Database μ—μ„œ FK κ°€ κ°€λ¦¬ν‚€λŠ” 값이 μ‹€μ œλ‘œ μ‘΄μž¬ν•΄μ•Ό ν•œλ‹€λŠ” κ·œμΉ™μ΄λ‹€.

λ˜ν•œ entity κ°„μ˜ 관계λ₯Ό destroy ν•  수 μžˆλŠ” method 및 operation 등에 λŒ€ν•˜μ—¬ λ³΄ν˜Έν•  수 있으며, μœ„μ—μ„œ μ–ΈκΈ‰ν•œλŒ€λ‘œ FK κ°€ 항상 valid ν•œ reference λ₯Ό κ°–κ³  μžˆλ‹€κ³  보μž₯ν•  수 있게 λœλ‹€.

@OneToOne - Cascade Types
Cascade TypesDescription
PERSISTentity κ°€ persisted/saved 라면, related entity 도 ν•¨κ»˜ persisted
REMOVEentity κ°€ removed/deleted 라면, realted entity 도 ν•¨κ»˜ deleted
REFRESHentity κ°€ refreshed 되면, related entity 도 ν•¨κ»˜ refreshed
DETACHentity κ°€ detached 되면, related entity 도 ν•¨κ»˜ detached
MERGEentity κ°€ merged 되면, realted entity 도 ν•¨κ»˜ merged
ALLμƒμœ„μ˜ λͺ¨λ“  cascade types

μœ„λŠ” Uni-Directional 의 One-to-One mapping μ—μ„œ cascade types λ₯Ό μ •λ¦¬ν•œ ν‘œμ΄λ‹€. default λ‘œλŠ” 아무 cascade option 도 μ‹€ν–‰λ˜μ§€ μ•ŠλŠ”λ‹€.

Configure Multiple Cascade Types
@OneToOne(casecade={CasecadeType.DETACH,
					CasecadeType.MERGE,
					CasecadeType.PERSIST,
					CasecadeType.REFRESH,
					CasecadeType.REMOVE,})
Development Process

Uni-Directional 의 One-to-One mapping 에 λŒ€ν•œ development process λŠ” λ‹€μŒκ³Ό κ°™λ‹€.

  1. Prep Work - Define database tables
  2. Create InstructorDetail class
  3. Create Instrcutor class
  4. Create Main App
Step 1: Prep Work - Define database tables

λ¨Όμ € DB table λΆ€ν„° μ •μ˜ν•˜μž.

DROP SCHEMA IF EXISTS `hb-01-one-to-one-uni`;
CREATE SCHEMA `hb-01-one-to-one-uni`;
USE `hb-01-one-to-one-uni`;
 
SET FOREIGN_KEY_CHECKS = 0;

μš°μ„  schema λ₯Ό μ •μ˜ν•˜μž. μœ„μ™€ 같이 기쑴에 μ‘΄μž¬ν•˜λŠ”μ§€ κ²€μ‚¬ν•˜κ³ , μƒˆλ‘œ μƒμ„±ν•œ λ’€ ν•΄λ‹Ή schema λ₯Ό μ‚¬μš©ν•œλ‹€.

λ§ˆμ§€λ§‰ 쀄에 μ‘΄μž¬ν•˜λŠ” SET FOREIGN_KEY_CHECKS = 0; λŠ” μ™Έλž˜ ν‚€ 검사, μžμ„Έν•˜κ²ŒλŠ” μ°Έμ‘° 무결성 검사λ₯Ό λΉ„ν™œμ„±ν™”ν•œλ‹€λŠ” λͺ…령이닀. λ‹Ήμ—°ν•˜κ²Œλ„ ν™œμ„±ν™”λŠ” = 1 둜 μ„€μ •ν•˜λ©΄ λœλ‹€. ν•΄λ‹Ή κ²€μ‚¬λŠ” μ™œ μ“°λŠ” 걸까?

Referentail Integrity, 즉 μ°Έμ‘° 무결성 κ²€μ‚¬λŠ” λΆ€λͺ¨ table κ³Ό μžμ‹ table 의 μ°Έμ‘° 관계에 λŒ€ν•œ 무결성을 κ²€μ‚¬ν•˜λŠ” 것이닀. μ°Έμ‘° 무결성이 μ„€μ •λ˜μ–΄ μžˆλŠ” 경우, λΆ€λͺ¨ table 을 μ‚­μ œν•  λ•Œλ‚˜ μžμ‹ table 에 데이터λ₯Ό 넣을 λ•Œ μ œμ•½μ΄ λ°œμƒν•œλ‹€.

마치 C/C++ μ—μ„œ Dangling Pointer Problem κ³Ό λΉ„μŠ·ν•œ 상황을 κ²€μ‚¬ν•˜λŠ” 것이라고도 λ³Ό 수 μžˆμ„ 것 κ°™λ‹€.

ν•΄λ‹Ή 섀정은 처음 schema 의 table 듀을 define ν•  λ•Œλ§Œ μ‚¬μš©ν•˜λŠ” 것이 μ’‹κ³ , μ‹€μ œλ‘œ ν…ŒμŠ€νŠΈ λ‹¨κ³„λ‚˜ 운영 λ‹¨κ³„μ—μ„œλŠ” λ‹€μ‹œ ν™œμ„±ν•˜λŠ” 것이 λ°”λžŒμ§ν•˜λ‹€.

그리고 μžμ‹ table 에 ν•΄λ‹Ήν•˜λŠ” instructor_detail table 을 define ν•˜μž.

CREATE TABLE `instructor_detail` (
  `id` int NOT NULL AUTO_INCREMENT,
  `youtube_channel` varchar(128) DEFAULT NULL,
  `hobby` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;

primary key λŠ” id 둜 μ„€μ •ν•˜κ³  κ·Έ μ΄μ™Έμ—λŠ” νŠΉλ³„νžˆ 섀정해쀄 것은 μ—†λ‹€. 이제 λΆ€λͺ¨ table 에 ν•΄λ‹Ήν•˜λŠ” instuctor table 을 define ν•˜μž.

CREATE TABLE `instructor` (
  `id` int NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45) DEFAULT NULL,
  `last_name` varchar(45) DEFAULT NULL,
  `email` varchar(45) DEFAULT NULL,
  `instructor_detail_id` int DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `FK_DETAIL_idx` (`instructor_detail_id`),
  CONSTRAINT `FK_DETAIL` FOREIGN KEY (`instructor_detail_id`) REFERENCES `instructor_detail` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;

foreign key 둜 μ‚¬μš©ν•  instructor_detail_id column 을 define ν•˜κ³ , id λ₯Ό primary key 둜 μ„€μ •ν•œλ‹€.

그리고, instructor_detail_id column 에 λŒ€ν•˜μ—¬ FK_DETAIL_idx λΌλŠ” μ΄λ¦„μ˜ index λ₯Ό μƒμ„±ν•œλ‹€. index λŠ” 검색 속도λ₯Ό ν–₯μƒμ‹œν‚€λŠ” μž₯점이 μ‘΄μž¬ν•œλ‹€. ν•˜μ§€λ§Œ ν•΄λ‹Ή 뢀뢄을 explicit ν•˜κ²Œ μž‘μ„±ν•˜μ§€ μ•Šμ•„λ„ MySQL 은 μžλ™μœΌλ‘œ μˆ¨κ²¨μ§„ index λ₯Ό μƒμ„±ν•˜μ§€λ§Œ, index 에 λŒ€ν•œ 이름은 μ‚¬μš©μžκ°€ μ§€μ •ν•  수 μ—†κ³  λ‚΄λΆ€μ μœΌλ‘œ instructor_ibfk_1 λ“±μ˜ μ΄λ¦„μœΌλ‘œ μ €μž₯λœλ‹€.

κ·Έ λ‹€μŒ FK_DETAIL μ΄λΌλŠ” μ΄λ¦„μœΌλ‘œ μ œμ•½ 쑰건을 μ„€μ •ν•˜λŠ”λ°, 두 table κ°„μ˜ 관계λ₯Ό κ°•μ œλ‘œ μ—°κ²°ν•΄μ€€λ‹€. instructor_detail_id column 을 FK 둜 μ‚¬μš©ν•˜κ³ , instructor_detail table 의 id column 을 μ°Έμ‘°ν•˜λ„λ‘ ν•œλ‹€. λ§ˆμ§€λ§‰μœΌλ‘œ, λΆ€λͺ¨ table μ—μ„œ ν•΄λ‹Ή 데이터λ₯Ό μ‚­μ œν•  수 μ—†κ³ , 값을 λ³€κ²½ν•  수 μ—†κ²Œ μ„€μ •ν•œλ‹€.

SET FOREIGN_KEY_CHECKS = 1;

λ§ˆμ§€λ§‰μœΌλ‘œ Referentail Integrity λ₯Ό λ‹€μ‹œ ν™œμ„±ν™”μ‹œν‚¨λ‹€.

이후 MySQL Workbench μ—μ„œ [Database] > [Reverse Engineer] 메뉴λ₯Ό ν†΅ν•˜μ—¬ Diagram 섀정을 ν•œ 뒀에 λ‹€μŒκ³Ό 같이 Cardinality λ₯Ό One-to-One 으둜 λ°”κΎΈμ–΄μ£Όλ©΄ λœλ‹€.

μœ„ diagram 을 보고 μ•Œ 수 μžˆλ“―μ΄, instructor_detail table 의 PK 둜 μ‚¬μš©λ˜λŠ” id column 은 instructor table 의 FK 에 μ˜ν•˜μ—¬ μ°Έμ‘°λ˜λ―€λ‘œ, instructor_detail table 은 parent table 둜, instructor table 은 child table 둜 λΆ„λ₯˜ν•  수 μžˆλ‹€.

마치 class 의 상속과 같은 κ°œλ…μ²˜λŸΌ, μ°Έμ‘°λ˜λŠ” μͺ½μ΄ parent 이고 μ°Έμ‘°ν•˜λŠ” μͺ½μ΄ child 이닀.

Step 2: Create InstructorDetail class
@Entity  
@Table(name="instructor_detail")  
public class InstructorDetail {  
    // define the fields ...
    // define constructors ...
    ...
}

instructor_detail table μ—μ„œ μ„€μ •ν•œ column λŒ€λ‘œ μ•Œλ§žκ²Œ InstructorDetail entity class λ₯Ό define ν•΄μ€€λ‹€.

Step 3: Create Instrcutor class
@Entity  
@Table(name = "instructor")  
public class Instructor {  
    // define fields ...
    // set up mapping to InstructorDetail entity  
    @OneToOne(cascade = CascadeType.ALL)  
    @JoinColumn(name = "instructor_detail_id")  
    private InstructorDetail instructorDetail;
	...
}

Uni-Directional One-to-One mapping μ΄λ―€λ‘œ child table μͺ½μ—μ„œλ§Œ mapping 관계λ₯Ό define ν•΄μ£Όλ©΄ λœλ‹€.

κ·Έ 방법은 μœ„μ™€ 같이 @OneToOne annotation 을 ν†΅ν•˜μ—¬ cascade option κ³Ό ν•¨κ»˜ define ν•˜λ©΄ 되고, @JoinColumn annotation 을 ν†΅ν•˜μ—¬ FK 둜 μ‚¬μš©ν•  column 이름을 μ§€μ •ν•œλ‹€. ν•΄λ‹Ή column 으둜 두 개의 table 이 μ—°κ²°λ˜λŠ” 것이닀.

Step 4: Create Main App

Main App 을 μƒμ„±ν•˜κΈ° 전에 Entity 접근을 λŒ€μ‹ ν•  DAO λ₯Ό λ¨Όμ € define ν•˜λŠ” 것이 더 λ‚˜μ„ 것 κ°™λ‹€.

public interface AppDAO {  
    void save(Instructor instructor);  
}
@Repository  
public class AppDAOImpl implements AppDAO {  
  
    // define field for entity manager  
    private EntityManager entityManager;  
  
    // inject entity manager using constructor injection  
    public AppDAOImpl(EntityManager entityManager) {  
        this.entityManager = entityManager;  
    }  
  
    @Override  
    @Transactional    public void save(Instructor instructor) {  
        entityManager.persist(instructor);  
    }  
}

DAO 에 λŒ€ν•œ interface 와, 그에 λŒ€ν•œ implementation 을 μœ„μ™€ 같이 define ν•΄μ€€λ‹€. implementation class μ—μ„œλŠ”, μ—¬κΈ°μ„œ μ•Œ 수 μžˆλ“―μ΄, @Repository annotation 을 ν†΅ν•˜μ—¬ ν•΄λ‹Ή class 에 λŒ€ν•œ object κ°€ Spring Bean 으둜 μžλ™ 등둝될 수 μžˆλ„λ‘ ν•œλ‹€.

이제 Main App 으둜 λ„˜μ–΄κ°€μ„œ μ‹€μ œ 데이터λ₯Ό μ‚¬μš©ν•˜μ—¬ test ν•΄λ³΄μž.

@SpringBootApplication  
public class CruddemoApplication {  
  
    public static void main(String[] args) {  
       SpringApplication.run(CruddemoApplication.class, args);  
    }  
  
    @Bean  
    public CommandLineRunner commandLineRunner(AppDAO appDAO) {  
       return runner -> {  
          createInstructor(appDAO);  
       };  
    }  
  
    private void createInstructor(AppDAO appDAO) {  
       // create the instructor
       // create the instructor detail
       // associate the objects
       tempInstructor.setInstructorDetail(tempInstructorDetail);
       
       // save the instructor
       appDAO.save(tempInstructor);
       ...
    }
}

μœ„μ™€ 같이 CommandLineRunner λ₯Ό ν†΅ν•˜μ—¬ Application 이 μ‹€ν–‰λœ 직후에 μ‹€ν–‰ν•  code λ₯Ό μ„€μ •ν•  수 μžˆλ‹€. createInstructor() method λ₯Ό μœ„μ™€ 같은 μˆœμ„œλ‘œ define ν•œ 뒀에 ν•΄λ‹Ή method λ₯Ό μ‹€ν–‰ν•˜λŠ” μ‹μœΌλ‘œ κ΅¬μ„±ν•˜λ©΄ λœλ‹€.

Application 을 μ‹€ν–‰ν•˜κΈ° 이전에 application.properties λ₯Ό ν†΅ν•˜μ—¬ Hibernate 의 logging κΈ°λŠ₯을 μ‚¬μš©ν•˜μž.

logging.level.org.hibernate.SQL=trace  
logging.level.org.hibernate.orm.jdbc.bind=trace

μœ„μ™€ 같이 μ„€μ •ν•¨μœΌλ‘œμ¨ μ‹€μ œ DB μ—μ„œ μ‚¬μš©λ˜λŠ” SQL query 와 ν•΄λ‹Ή query 에 μ‚¬μš©λ˜λŠ” μ‹€μ œ data 듀이 μ–΄λ–€ 것인지λ₯Ό logging ν•  수 μžˆλ‹€.

이제 Application 을 μ‹€ν–‰ν•΄λ³΄μž.

One-to-One Mapping Uni-Direction - Test (save)

console 에 좜렀된 log μ—μ„œ λ³Ό 수 μžˆλ“―μ΄, μ„€μ •ν•΄μ€€ properties 에 μ˜ν•˜μ—¬ 좜λ ₯된 SQL query 와 이에 binding 된 value 듀을 확인할 수 μžˆλ‹€.

결과적으둜 μ‹€μ œ DB table 에도 data 듀이 잘 μŒ“μΈ 것을 λ³Ό 수 μžˆλ‹€. Main App μ—μ„œ λ‹¨μˆœνžˆ Instructor object 만 μ €μž₯ν–ˆλŠ”λ°λ„ InstructorDetail object κ°€ 같이 μ €μž₯된 것을 λ³Ό 수 μžˆλ‹€.

One-to-One Mapping Uni-Direction - Test (find)

이제 μ €μž₯된 data λ₯Ό 기반으둜, instuctor λ₯Ό μ‘°νšŒν•˜κ³  이λ₯Ό ν†΅ν•˜μ—¬ instructor detail κΉŒμ§€λ„ μ‘°νšŒν•  수 μžˆλŠ”μ§€ test ν•΄λ³΄μž.

μš°μ„  AppDAO interface 에 findInstructorById λ₯Ό μΆ”κ°€ν•˜κ³  μ•„λž˜μ™€ 같이 AppDAOImpl μ—μ„œ overriding ν•˜μž.

@Override  
public Instructor findInstructorById(int theId) {  
    return entityManager.find(Instructor.class, theId);  
}

그리고 Main App μ—μ„œλŠ” μ•„λž˜μ™€ 같이 findInstructor method λ₯Ό μΆ”κ°€ν•˜μ—¬ test λ₯Ό μ§„ν–‰ν•˜μž.

private void findInstructor(AppDAO appDAO) {  
    int theId = 1;  
    System.out.println("Finding instructor id: " + theId);  
  
    Instructor tempInstructor = appDAO.findInstructorById(theId);  
  
    System.out.println("tempInstructor: " + tempInstructor);  
    System.out.println("the associate instructorDetail only: " + tempInstructor.getInstructorDetail());  
}

이제 Application 을 μ‹€ν–‰ν•˜μ—¬ console log λ₯Ό 확인해보면,

μœ„μ™€ 같이 instructor 와 그의 detail κΉŒμ§€ λͺ¨λ‘ 잘 μ‘°νšŒν•œ 것을 μ•Œ 수 μžˆλ‹€.

One-to-One Mapping Uni-Direction - Test (delete)

μ΄λ²ˆμ—λŠ” instructor λ₯Ό μ‚­μ œν–ˆμ„ λ•Œ detail 도 같이 μ‚­μ œλ˜λŠ”μ§€ test ν•΄λ³΄μž.

@Override  
@Transactional  
public void deleteInstructorById(int theId) {  
    Instructor tempInstructor = entityManager.find(Instructor.class, theId);  
    entityManager.remove(tempInstructor);  
}

μœ„μ™€ 같이 AppDAOImpl 에 method λ₯Ό μΆ”κ°€ν•΄μ£Όκ³ , Main App 에 λ‹€μŒκ³Ό 같이 μΆ”κ°€ν•œλ‹€.

private void deleteInstructor(AppDAO appDAO) {  
    int theId = 1;  
    System.out.println("Deleting instructor id: " + theId);  
  
    appDAO.deleteInstructorById(theId);  
  
    System.out.println("Done! :)");  
}

Main App μ—μ„œλŠ” DAO λ₯Ό inject λ°›μ•„ μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ— μ§μ ‘μ μœΌλ‘œ Database 에 μ ‘κ·Όν•˜μ§€ μ•Šκ³  DAO λ₯Ό ν†΅ν•˜μ—¬ μ†μ‰½κ²Œ DB 에 μ ‘κ·Όν•  수 μžˆλ‹€.

이후 test λ₯Ό 해보면 μœ„μ™€ 같이 instructor object λ₯Ό λ¨Όμ € delete ν•œ 이후, detail object κ°€ delete 된 것을 λ³Ό 수 μžˆλ‹€.

μ΄λ ‡κ²Œ parent table κ³Ό child table 에 λŒ€ν•œ save, find, delete μž‘μ—…μ„ μ•Œμ•„λ΄€λ‹€.

μœ„μ—μ„œ 닀룬 λ‹€μ–‘ν•œ CasecadeType option 을 ν†΅ν•˜μ—¬ μ˜μ†μ„± 전이, 즉 cascading persistence 정도λ₯Ό μ‘°μ ˆν•  수 μžˆλ‹€.