π Not-Null μ μ½ μ‘°κ±΄ μλ°!
π¨ μλ¬ λ©μμ§
org.hibernate.PropertyValueException:
not-null property references a null or transient value:
com.kobe.schoolmanagement.domain.entity.Student.major
μ΅μ’ μμΈ
DataIntegrityViolationException
π ν΅μ¬ μμΈ
Student
μν°ν°μ major
νλκ° null
μΈ μνλ‘ μ μ₯μ μλνκΈ° λλ¬Έμ
λλ€.
μ μ½ μ‘°κ±΄ νμΈ
@Entity
public class Student {
// ...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false, name = "major_id") // β null νμ© μ ν¨
private Major major;
}
nullable = false
μ€μ μΌλ‘ μΈν΄ major
νλλ λ°λμ κ°μ΄ μμ΄μΌ ν©λλ€.
π λ¬Έμ λ°μ νλ¦
ν΄λΌμ΄μΈνΈ μμ²
β
{"name": "κ°λ―Όμ±", "admissionYear": 2010, "majorName": "Computer"}
β
StudentRequestDto μμ
β
StudentService.createStudent() νΈμΆ
β
requestDto.toEntity(studentId) μ€ν
β
Student μν°ν° μμ±
βββ name: "κ°λ―Όμ±" β
βββ admissionYear: 2010 β
βββ major: null β (Major μν°ν°λ₯Ό μ‘°ννμ§ μμ!)
β
studentRepository.save(student)
β
Hibernateκ° λ°μ΄ν°λ² μ΄μ€μ μ μ₯ μλ
β
NOT NULL μ μ½ μ‘°κ±΄ μλ° κ°μ§
β
PropertyValueException λ°μ
β
DataIntegrityViolationException
π‘ λ¬Έμ μν© λΆμ
λλ½λ λ‘μ§
λ¨κ³ | νμ¬ μν© | νμν μμ |
---|---|---|
1 |
majorName λ¬Έμμ΄ μμ |
β μλ£ |
2 |
majorName μΌλ‘ Major μν°ν° μ‘°ν |
β λλ½λ¨ |
3 | μ‘°νν Major κ°μ²΄λ₯Ό Student μ μ€μ |
β λλ½λ¨ |
4 |
Student μ μ₯ |
β μλνμΌλ μ€ν¨ |
κ²°κ³Ό
majorName: "Computer" (String)
β
β Major μν°ν° μ‘°ν λ‘μ§ μμ
β
major νλ: null
β
μ μ₯ μ€ν¨!
β ν΄κ²° λ°©μ
3λ¨κ³λ‘ λ¬Έμ λ₯Ό ν΄κ²°ν©λλ€.
Step 1: MajorRepository λ©μλ μΆκ°
Major
μν°ν°λ₯Ό μ 곡 μ΄λ¦μΌλ‘ μ‘°νν μ μλ λ©μλλ₯Ό μΆκ°ν©λλ€.
package com.kobe.schoolmanagement.repository;
import com.kobe.schoolmanagement.domain.entity.Major;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface MajorRepository extends JpaRepository<Major, Long> {
Optional<Major> findByMajorNumber(String majorNumber);
// β
μ 곡 μ΄λ¦μΌλ‘ μ‘°ννλ λ©μλ μΆκ°
Optional<Major> findByName(String name);
}
π‘ Spring Data JPA 쿼리 λ©μλ
findByName("Computer")
// β μλμΌλ‘ λ€μ 쿼리 μμ±
// SELECT * FROM major WHERE name = 'Computer'
Step 2: StudentService λ‘μ§ μμ
Major
μν°ν°λ₯Ό μ‘°ννκ³ Student
μ μ€μ νλ λ‘μ§μ μΆκ°ν©λλ€.
Before
@Transactional
public StudentResponseDto createStudent(StudentRequestDto requestDto) {
int admissionYear = requestDto.getAdmissionYear();
String majorName = requestDto.getMajorName();
// β Major μν°ν° μ‘°ν μμ
long sequence = studentRepository.countByAdmissionYearAndMajorName(
admissionYear, majorName) + 1;
String studentId = createStudentNumber.generate(
admissionYear, majorName, sequence);
// β majorκ° nullμΈ μνλ‘ μν°ν° μμ±
Student student = requestDto.toEntity(studentId);
Student savedStudent = studentRepository.save(student);
return StudentResponseDto.fromEntity(savedStudent);
}
After
package com.kobe.schoolmanagement.service;
import com.kobe.schoolmanagement.common.CreateStudentNumber;
import com.kobe.schoolmanagement.domain.entity.Major;
import com.kobe.schoolmanagement.domain.entity.Student;
import com.kobe.schoolmanagement.dto.request.StudentRequestDto;
import com.kobe.schoolmanagement.dto.response.StudentResponseDto;
import com.kobe.schoolmanagement.repository.MajorRepository;
import com.kobe.schoolmanagement.repository.StudentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class StudentService {
private final StudentRepository studentRepository;
private final MajorRepository majorRepository; // β
μμ‘΄μ± μ£Όμ
private final CreateStudentNumber createStudentNumber;
@Transactional
public StudentResponseDto createStudent(StudentRequestDto requestDto) {
// 1. DTOλ‘λΆν° νμν μ 보 μΆμΆ
int admissionYear = requestDto.getAdmissionYear();
String majorName = requestDto.getMajorName();
// β
2. Major μν°ν° μ‘°ν (ν΅μ¬ μμ )
Major major = majorRepository.findByName(majorName)
.orElseThrow(() -> new IllegalArgumentException(
"μ‘΄μ¬νμ§ μλ μ 곡μ
λλ€: " + majorName));
// 3. νμ μ μΉ΄μ΄νΈ
long sequence = studentRepository.countByAdmissionYearAndMajorName(
admissionYear, majorName) + 1;
// 4. νλ² μμ±
String studentId = createStudentNumber.generate(
admissionYear, majorName, sequence);
// 5. νλ² μ€λ³΅ κ²μ¬
studentRepository.findByStudentId(studentId).ifPresent(s -> {
throw new IllegalStateException("νλ² μμ± μΆ©λ λ°μ. λ€μ μλν΄μ£ΌμΈμ");
});
// β
6. major κ°μ²΄λ₯Ό ν¨κ» μ λ¬νμ¬ μν°ν° μμ±
Student student = requestDto.toEntity(studentId, major);
Student savedStudent = studentRepository.save(student);
return StudentResponseDto.fromEntity(savedStudent);
}
@Transactional(readOnly = true)
public StudentResponseDto getStudent(String studentId) {
Student student = studentRepository.findByStudentId(studentId)
.orElseThrow(() -> new IllegalArgumentException(
"μ‘΄μ¬νμ§ μλ νμμ
λλ€."));
return StudentResponseDto.fromEntity(student);
}
}
π‘ μ£Όμ λ³κ²½μ¬ν
// 1. MajorRepository μ£Όμ
μΆκ°
private final MajorRepository majorRepository;
// 2. Major μν°ν° μ‘°ν
Major major = majorRepository.findByName(majorName)
.orElseThrow(() -> new IllegalArgumentException("μ‘΄μ¬νμ§ μλ μ 곡μ
λλ€"));
// 3. toEntity()μ major μ λ¬
Student student = requestDto.toEntity(studentId, major);
Step 3: StudentRequestDto.toEntity() λ©μλ μμ
toEntity
λ©μλκ° Major
κ°μ²΄λ₯Ό λ°μμ μ€μ νλλ‘ μμ ν©λλ€.
Before
public class StudentRequestDto {
private String name;
private int admissionYear;
private String majorName;
public Student toEntity(String studentId) {
return Student.builder()
.name(this.name)
.studentId(studentId)
.admissionYear(this.admissionYear)
// β major νλ μ€μ μμ (nullλ‘ λ¨μ)
.build();
}
}
After
public class StudentRequestDto {
private String name;
private int admissionYear;
private String majorName;
// β
Major κ°μ²΄λ₯Ό νλΌλ―Έν°λ‘ λ°λλ‘ μμ
public Student toEntity(String studentId, Major major) {
return Student.builder()
.name(this.name)
.studentId(studentId)
.admissionYear(this.admissionYear)
.major(major) // β
Major κ°μ²΄ μ€μ
.build();
}
}
π μμ ν λ°μ΄ν° νλ¦
ν΄λΌμ΄μΈνΈ μμ²
β
{"name": "κ°λ―Όμ±", "admissionYear": 2010, "majorName": "Computer"}
β
StudentService.createStudent()
β
majorRepository.findByName("Computer") β
μΆκ°λ¨
β
Major μν°ν° μ‘°ν μ±κ³΅
βββ id: 1
βββ majorNumber: "31513162120518"
βββ name: "Computer"
β
requestDto.toEntity(studentId, major) β
major μ λ¬
β
Student μν°ν° μμ±
βββ name: "κ°λ―Όμ±" β
βββ admissionYear: 2010 β
βββ major: Major κ°μ²΄ β
(null μλ!)
β
studentRepository.save(student)
β
μ μ₯ μ±κ³΅! β
π μ 체 μμ μμ½
νμΌ | μμ λ΄μ© | λͺ©μ |
---|---|---|
MajorRepository |
findByName() λ©μλ μΆκ° |
μ 곡 μ΄λ¦μΌλ‘ μ‘°ν |
StudentService |
majorRepository μ£Όμ
Major μ‘°ν λ‘μ§ μΆκ°toEntity() μ major μ λ¬ |
Major μν°ν° μ‘°ν λ° μ€μ |
StudentRequestDto |
toEntity() μκ·Έλμ² λ³κ²½major νλ μ€μ μΆκ° |
Major κ°μ²΄λ₯Ό λ°μμ μ€μ |
β ν μ€νΈ
Postman μμ²
POST http://localhost:8080/api/v1/students
{
"name": "κ°λ―Όμ±",
"admissionYear": 2010,
"majorName": "Computer"
}
μμ μλ΅
{
"name": "κ°λ―Όμ±",
"admissionYear": 2010,
"major_info": {
"id": 1,
"majorNumber": "31513162120518",
"name": "Computer"
}
}
λ°μ΄ν°λ² μ΄μ€ νμΈ
-- Student ν
μ΄λΈ
SELECT * FROM student;
| id | name | student_id | admission_year | major_id |
|β-|ββ|ββββ|βββββ-|βββ-|
| 1 | κ°λ―Όμ± | 1003001 | 2010 | 1 β
|
β οΈ μ£Όμμ¬ν
1. μ‘΄μ¬νμ§ μλ μ 곡λͺ μ²λ¦¬
Major major = majorRepository.findByName(majorName)
.orElseThrow(() -> new IllegalArgumentException(
"μ‘΄μ¬νμ§ μλ μ 곡μ
λλ€: " + majorName));
- μ‘΄μ¬νμ§ μλ
majorName
μ λ°μΌλ©΄ μμΈ λ°μ - μ¬μ μ
Major
λ°μ΄ν°κ° DBμ μμ΄μΌ ν¨
2. Major λ°μ΄ν° μ¬μ λ±λ‘
// Major μν°ν°λ₯Ό 미리 μ μ₯ν΄μΌ ν¨
Major computerMajor = Major.builder()
.majorNumber("31513162120518")
.name("Computer")
.build();
majorRepository.save(computerMajor);
π Best Practices
1. DTOμμ μν°ν° μ°Έμ‘° μ€μ
// β λμ μ: DTOμμ μ§μ μ‘°ν
public Student toEntity(String studentId) {
Major major = majorRepository.findByName(this.majorName).orElseThrow();
// DTOκ° Repositoryμ μμ‘΄νκ² λ¨!
}
// β
μ’μ μ: νλΌλ―Έν°λ‘ λ°κΈ°
public Student toEntity(String studentId, Major major) {
// μμ‘΄μ±μ΄ λͺ
ννκ³ ν
μ€νΈνκΈ° μ¬μ
}
2. μμΈ λ©μμ§μ 컨ν μ€νΈ ν¬ν¨
// β λμ μ
throw new IllegalArgumentException("μ‘΄μ¬νμ§ μλ μ 곡μ
λλ€.");
// β
μ’μ μ
throw new IllegalArgumentException("μ‘΄μ¬νμ§ μλ μ 곡μ
λλ€: " + majorName);
3. μν°ν° μ μ½ μ‘°κ±΄ νμΈ
@JoinColumn(nullable = false, name = "major_id")
// ββ nullable = false μ€μ νμΈ
// νμ νλλ λ°λμ κ°μ μ€μ ν΄μΌ ν¨
π§ νΈλ¬λΈμν
Q1. βμ‘΄μ¬νμ§ μλ μ 곡μ λλ€β μμΈκ° κ³μ λ°μν΄μ
μμΈ: Major λ°μ΄ν°κ° DBμ μμ
ν΄κ²°:
-- Major λ°μ΄ν° νμΈ
SELECT * FROM major WHERE name = 'Computer';
-- λ°μ΄ν°κ° μλ€λ©΄ μ½μ
INSERT INTO major (major_number, name)
VALUES ('31513162120518', 'Computer');
Q2. major_idκ° nullλ‘ μ μ₯λμ΄μ
μμΈ: Student.builder()
μμ major()
λ©μλ νΈμΆ λλ½
ν΄κ²°:
Student.builder()
.name(this.name)
.studentId(studentId)
.admissionYear(this.admissionYear)
.major(major) // β
μ΄ μ€ μΆκ° νμΈ
.build();
Q3. LazyInitializationExceptionμ΄ λ°μν΄μ
μμΈ: FetchType.LAZY
λ‘ μ€μ λ majorλ₯Ό νΈλμμ
λ°μμ μ κ·Ό
ν΄κ²°:
@Transactional(readOnly = true) // β
νΈλμμ
λ²μ νμΈ
public StudentResponseDto getStudent(String studentId) {
Student student = studentRepository.findByStudentId(studentId)
.orElseThrow();
return StudentResponseDto.fromEntity(student); // νΈλμμ
λ΄μμ λ³ν
}