Sprint Data JPA
RDB에서의 관계
예를 들어 왼쪽은 Item
이라는 Entity
이고, 오른쪽은 Company
라는 Entity
라고 가정했을 때, 회사(Company)는 여러 상품(Item)을 보유할 수 있다. 따라서 RDB에서는 Item
테이블에 companyId
라는 외래키를 사용해 Company
와의 관계를 표현한다.
즉, RDB에서 일반적인 데이터베이스 테이블의 관계는 외래키를 이용해 표현한다. (Item
클래스에 Company company;
와 같이 객체 필드를 직접 담을 수는 없다.)
ORM (Object Relational Mapping)
RDB는 테이블에 다른 테이블을 레코드로 넣을 수 없기 때문에 관계형 데이터베이스를 활용하는 객체지향 언어 사용자 입장에서 데이터베이스 조회와 실제 데이터를 활용하는 두 가지 상황의 간극이 발생한다.
이러한 문제를 줄이기(테이블 데이터를 쉽게 표현) 위해 ORM 기술이 등장하였다.
ORM은 객체지향 프로그래밍의 객체와 관계형 데이터베이스의 테이블 간의 매핑을 자동화하는 기술이다.
@Entity
public class Item {
String name;
String desc;
// RDB에서의 관계 표현
// Integer companyId;
// 이렇게 하면 ORM 의 장점을 갖다 버리는 것이므로 아래와 같이 바꾸고
// 다른 테이블의 데이터다! 라는 것만 표시하면 된다 -> @ManyToOne
@ManyToOne
Company company; // Item(N) : company(1) 관계
}
JPA (Java Persistance API)
JPA란 어떤 Java 객체가 어떻게 테이블에 매핑되는지를 표현하기 위한 명세이다. 또한 JPA는 스프링부트의 데이터 접근 기술이자 ORM을 구현하기 위한 자바 표준 인터페이스로서
각종 인터페이스와 어노테이션들의 집합 (ex) @Entity
) 이라고 볼 수 있다.
즉, JPA 사용 시 우리가 작성한 Java 객체를 데이터베이스의 테이블 또는 레코드로 정의할 수 있다.
Hibernate
Spring Boot는 기본적으로 Hibernate를 JPA의 기본 구현체로 사용한다. Hiberante란 JPA 명세를 바탕으로 데이터베이스를 활용할 수 있게 해주는 JPA의 구현체이자 ORM 프레임워크이다.
Hiberante는 우리를 대신해 SQL을 작성해주어 JPA로 표현된 객체를 실제 데이터베이스에 적용해주고 사용할 수 있게 해준다.
정리하면, Spring Boot는 JPA를 사용하고, 기본적으로 JPA의 구현체인 Hibernate를 통해 ORM 기술을 활용한다. 이로써 테이블간 관계를 Entity의 필드로 표현 가능하다
JPA Project
프론트 코드는 일부 생략하고 진행
1. RDB 의존성 추가, JPA 설정
build.gradle
Hiberante 는 우리를 대신해서 SQL을 작성해주는데, SQL은 우리가 사용하는 RDB에 따라 조금씩 차이가 있다. 이를 Dialect라 부른다.
build.gradle 를 통해 사용하고자 하는 데이터베이스의 라이브러리를 불러오기 위한 의존성 추가(SQL의 Dialect를 SQLite로 설정해주기)할 수 있다.
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// lombok
implementation 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// sqlite
runtimeOnly 'org.xerial:sqlite-jdbc:3.41.2.2'
runtimeOnly 'org.hibernate.orm:hibernate-community-dialects:6.2.4.Final'
application.yaml
이후 설정을 위해 application.yaml 을 작성한다.
spring:
datasource: # JPA는 어떤 데이터베이스를 가지고 Spring Boot를 구성할지
url: jdbc:sqlite:db.sqlite
driver-class-name: org.sqlite.JDBC
# username : sa
# password : password
jpa:
hibernate:
# Hibernate가 Entity 객체를 적용하기 위한 DDL을 어떻게 실행하건지
ddl-auto: create
# ddl-auto: update
show-sql: true
database-platform: org.hibernate.community.dialect.SQLiteDialect
ddl-auto
create
update
show-sql
database-platform
application.yaml 을 작성하였다면, 사용하고 있는 IDE에 datasource 추가 Main 실행 db.splite 가 생성된다.
2. Entity
Entity 정의
JPA를 사용한다면 우리가 작성한 Java 객체를 데이터베이스의 테이블 또는 레코드로 정의할 수 있다. 컬럼 정보를 표현한 Java 클래스를 만들고 상단에 @Entity
어노테이션을 붙여준다.
@Table(name = "student")
: 테이블 명 변경@Column(name = "username")
: 컬럼 명 변경ManyToOne 관계 설정
강사는 여러 학생들을 지도한다. 따라서 Student
엔티티에 Instructor
엔티티를 포함시키고 @ManyToOne
어노테이션을 추가한다.
advisor_id
라는 Foregin Key Column (= Join Colum)이 생성된다. @JoinColumn
: 컬럼 이름을 advisor
로 바꾸어주기OneToMany 관계 설정
Query Method 추가
Instructor
에서 Student
를 활용하고 싶을 수도 있다. 어떤 Instructor
의 Student
정보를 다 알고 싶다고 가정하면, 테이블을 기준으로 하면 해당 Instructor
의 PK
기준으로 Student
테이블의 FK
를 검색해 볼것이다. 그와 비슷하게 JpaRepository
에 QueryMethod
를 추가할 수 있다. 반대쪽에 @OneToMany 추가
@ManyToOne
의 반대쪽 관계를 맺는 테이블로서, Instructor
에 @OneToMany
를 추가할수도 있다.이떄 mappedBy 반대쪽 @ManyToOne 어노테이션이 붙은 필드의 이름이 작성된다. 이는 반대쪽(관계의 주가 되는) Entity의 어떤 속성을 기준으로 조회하는지를 정의하기 위함이다.
Student.java
/*
CREATE TABLE student (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
NAME VARCHAR(255),
AGE INTEGER,
PHONE VARCHAR(255),
EMAIL VARCHAR(255)
);
*/
@Data
@Entity
// @Table(name="student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
private String phone;
private String email;
// 추가
@ManyToOne
@JoinColum(name = "advisor") // FK Column -> Join Column
private Instructor advisor;
}
Instructor.java
@Entity
public class Instructor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
// Student 엔티티의 Instructor advisor; 를 지정하도록 mappedBy
@OneToMany(mappedBy = "advisor")
private List<Student> advisingStudents;
}
INSERT INTO instructor (first_name,last_name)
VALUES
('Vivien','Moran'),
('Tanisha','Gordon'),
('Sharon','Harrell'),
('Christopher','Mcclain'),
('Edan','Brian'),
('Plato','Best'),
('Uriah','Nguyen'),
('Jesse','Romero'),
('Nehru','Huff'),
('Jolene','Roach');
`
INSERT INTO student (name,age,phone,email,advisor)
VALUES
('Deirdre Mclean',36,'010-2453-6183','proin.velit@yahoo.couk',null),
('Rebecca Ayala',37,'010-8356-8511','in.tempus@protonmail.org',3),
('Griffin Joyner',31,'010-5568-0941','in.ornare@icloud.net',4),
('Karina Reynolds',38,'010-7756-2174','id.sapien@outlook.net',3),
('Ryan Burgess',36,'010-4341-0548','ultrices.iaculis.odio@protonmail.net',9),
('Beck Mack',23,'010-5758-9647','in.magna@yahoo.couk',6),
('Rooney Beasley',32,'010-2702-3193','lobortis@google.edu',9),
('Kelly Nguyen',33,'010-4211-0843','commodo@aol.org',7),
('Demetria Barton',23,'010-7823-0136','pellentesque.habitant.morbi@aol.net',7),
('Hedy Hardy',24,'010-1763-3441','eu.ligula@protonmail.net',10);
3. Repository
JPA를 활용하는 경우 보통 EntityManager
객체를 사용해 데이터베이스와 소통하는데, 이 EntityManager
를 추상화한 것이 Spring Data JPA의 Respository
인터페이스이다.
우리가 만든 Entity 객체를 이용해 데이터베이스를 활용하고 싶다면 CRUD를 위한 기초 메서드, 페이지 구분 및 정렬 기능이 추가된 JpaRepository
인터페이스를 활용할 수 있다.
이제 우리가 만든 JpaRepository로 CRUD 진행할 수 있다.
StudentRepository
// JpaRepository 상속받는 StudentRepo 인터페이스
public interface StudentRepository extends
JpaRepository<Student, Long> {
List<Student> findAllByAdvisor(Instructor entity);
// Query Method 추가하여 Instructor PK를 기준으로 Student 테이블의 FK 검색
List<Student> findAllByAdvisorId(Long id);
}
InstructorRepository
public interface InstructorRepo extends JpaRepository<Instructor, Long> {
}
4. Controller (CRUD)
StudentController
@RequiredArgsConstructor
@Controller
@RequestMapping("student")
public class StudentController {
private final StudentService studentService;
private final InstructorService instructorService;
@GetMapping("create-view")
public String createView(Model model) {
// 학생을 만들때
model.addAttribute("instructors", instructorService.readInstructorAll());
return "student/create";
}
@PostMapping("create")
public String create(
@RequestParam("name")
String name,
@RequestParam("age")
Integer age,
@RequestParam("phone")
String phone,
@RequestParam("email")
String email,
@RequestParam("advisor-id")
Long advisorId
) {
studentService.create(name, age, phone, email, advisorId);
return "redirect:/student";
}
@GetMapping
public String readAll(Model model) {
model.addAttribute("students", studentService.readStudentAll());
return "student/home";
}
@GetMapping("{id}")
public String readOne(
@PathVariable("id")
Long id,
Model model
) {
model.addAttribute("student", studentService.readStudent(id));
return "student/read";
}
@GetMapping("{id}/update-view")
public String updateView(
@PathVariable("id")
Long id,
Model model
) {
model.addAttribute("student", studentService.readStudent(id));
return "student/update";
}
@PostMapping("{id}/update")
public String update(
@PathVariable("id")
Long id,
@RequestParam("name")
String name,
@RequestParam("age")
Integer age,
@RequestParam("phone")
String phone,
@RequestParam("email")
String email
) {
studentService.update(
id, name, age, phone, email);
return String.format("redirect:/student/%d", id);
}
@PostMapping("{id}/delete")
public String delete(@PathVariable("id") Long id) {
studentService.delete(id);
return "redirect:/student";
}
}
InstructorController
@RequiredArgsConstructor
@Controller
@RequestMapping("instructor")
public class InstructorController {
private final InstructorService instructorService;
@GetMapping
public String readAll(Model model) {
model.addAttribute("instructors", instructorService.readInstructorAll());
return "instructor/home";
}
@GetMapping("{id}")
public String readOne(
@PathVariable("id")
Long id,
Model model
) {
model.addAttribute("instructor", instructorService.readInstructor(id));
return "instructor/read";
}
}
5. Service (CRUD)
CREATE
컨트롤러의 createView
메서드는 templates
의 student/create.html
을 템플릿으로 활용하며, 전체 Instructor
데이터를 Model
에 전달함으로서 사용자가 선택할 수 있는 Instructor
목록을 create.html
에서 확인할 수 있도록 해주고 있다.
create.html
의 form
내의 select
요소는 전달받은 Instructor
데이터를 바탕으로 복수의 option
요소를 만든다. 이는 나중에 서버로 전송(<form action="/student/create" method="post">
)될 때 advisor-id
의 이름으로 보내지며, 이를 컨트롤러에서 @RequestParam Long advisorId
으로 받아줄 수 있다.
전달받은 advisor-id
의 값을 바탕으로 서비스에서 Instructor
객체를 찾아낸다면, 해당 값을 student
에 할당하여 관계를 설정할 수 있다.
InstrutorRepo
주입 create()
에 Long advisorId
파라미터 추가optional
사용) repository
의 save()
메서드 호출@Service
@RequiredArgsConstructor // DI - 생성자 자동 주입
public class StudentService {
private final StudentRepo repo;
public void create(
String name,
Integer age,
String phone,
String email,
// 지도 교수의 FK를 받아준다.
Long advisorId
) {
// 주어진 정보로 새로운 Student 객체를 만든다.
Student student = new Student();
// setter 메서드를 이용하여 학생 정보를 객체에 저장
student.setName(name);
student.setAge(age);
student.setPhone(phone);
student.setEmail(email);
// Optional<Instructor> optionalInstructor
// = instructorRepo.findById(advisorId);
// student.setAdvisor(optionalInstructor.orElse(null ));
student.setAdvisor(
instructorRepo.findById(advisorId).orElse(null)
);
studentRepo.save(student);
}
}
READ ALL
create()
메서드를 통해 테이블에 저장된 데이터들을 사용자에게 보여주는 비즈니스 로직을 작성하자
전체 조회 : repository
의 findAll()
메서드 활용하면 데이터베이스에 저장된 특정 테이블의 모든 레코드를 조회할 수 있다.
@Service
@RequiredArgsConstructor
public class StudentService {
private final StudentRepo studentRepo;
private final InstructorRepo instructorRepo;
public List<Student> readStudentAll() {
List<Student> students = studentRepo.findAll();
for (Student student : students) {
System.out.println(student.toString());
}
return students;
}
}
@Service
@RequiredArgsConstructor
public class InstructorService {
private final InstructorRepo instructorRepo;
// public List<InstructorDto> readInstructorAll() { return new ArrayList<>();}
public List<Instructor> readInstructorAll() {
return instructorRepo.findAll();
}
}
READ ONE
단일 조회 : repository
의 findById()
메서드 활용하면 데이터베이스에 저장된 특정 테이블의 특정 id 컬럼으로 검색한 레코드를 조회할 수 있다.
Optional
Optional
사용repository.findById()
의 반환형은 Optional
@Service
@RequiredArgsConstructor
public class StudentService {
private final StudentRepo studentRepo;
private final InstructorRepo instructorRepo;
public Student readStudent(Long id) {
Optional<Student> optionalStudent = studentRepo.findById(id);
// 실제 데이터가 있으면 해당 데이터를
return optionalStudent
// 없으면 null을 반환한다.
.orElse(null);
}
}
@Service
@RequiredArgsConstructor
public class InstructorService {
private final InstructorRepo instructorRepo;
// public InstructorDto readInstructor(Long id) {return new InstructorDto();}
public Instructor readInstructor(Long id) {
return instructorRepo.findById(id).orElse(null);
}
}
UPDATE
findById(id)
로 Entity 조회, setter 메서드를 이용해 엔티티 수정 후 save()
GET
요청으로 컨트롤러의 updateView
메서드가 호출되어 해당 id
를 가진 Student
의 update-view
페이지로 넘어오게 된다.student
하위 update.html
이 보여지고, 데이터를 수정하고 나서 제출을 누르면 POST
요청으로 컨트롤러의 update()
메서드가 호출된다. update()
메서드가 실행되고나면 StudentService
의 update()
메서드가 호출되어 student
객체를 수정 후 저장한다id
를 가진 Student (student/id)
로 리다이렉팅한다.@Service
@RequiredArgsConstructor
public class StudentService {
private final StudentRepo studentRepo;
private final InstructorRepo instructorRepo;
public void update(
// 수정할 데이터의 PK가 무엇인지
Long id,
String name,
Integer age,
String phone,
String email
) {
// 1. 업데이트 할 대상 데이터를 찾고
Student target = readStudent(id);
// 2. 데이터의 내용을 전달받는 내용으로 갱신하고
target.setName(name);
target.setAge(age);
target.setPhone(phone);
target.setEmail(email);
// 3. repository를 이용해 저장한다.
studentRepo.save(target);
}
}
DELETE
@Service
@RequiredArgsConstructor
public class StudentService {
private final StudentRepo studentRepo;
private final InstructorRepo instructorRepo;
public void delete(Long id) {
studentRepo.deleteById(id);
}
}
6. Query Methods
지금까지는 기본적인 CRUD를 위해 다음과 같은 메서드를 사용했다.
findById // CREATE
findAll // PK를 기분으로 READ
save // 전체 READ
deleteById // PK를 기준으로 DELETE
delete // Entity를 기준으로 DELETE
데이터베이스의 데이터를 조회할 때 더 복잡한 조회를 하기 위해 JpaRespository
에 추가적인 메서드를 정의할 수 있다.
메서드 이름 규칙 [공식 문서]
findBy
로 시작한 뒤, SQL 쿼리문을 작성하듯이 WHERE
, ORDER BY
로 뒷부분 표기JpaRespository
를 정의할 때 Entity
를 정해주었기 때문에 FROM
절은 신경쓸 필요 없다.SELECT ORDER BY
/*
SELECT * FROM student ORDER BY name;
: student 테이블에서 모든 열을 선택하고, 그 결과를 name 열을 기준으로 오름차순 정렬
*/
List<Student> findAllByOrderByName();
/*
SELECT * FROM student ORDER BY age DESC;
: student 테이블에서 모든 열을 선택하고, 그 결과를 age 열을 기준으로 내림차순 정렬
*/
List<Student> findAllByOrderByAgeDesc();
SELECT WHERE
/*
SELECT * FROM student WHERE age <= ?;
: student 테이블에서 age 가 특정 값(?) 이하인 모든 행을 선택하는 쿼리
-> 매개변수 '?' 는 쿼리 실행시 동적으로 실제값이 입력됨
*/
List<Student> findAllByAgeLessThanEqual(Integer age);
SELECT LIKE
/*
SELECT * FROM student WHERE email LIKE '%?';
: student 테이블에서 email 이 특정 패턴('?')으로 끝나는 모든 행을 선택하는 쿼리
*/
List<Student> findAllByEmailEndingWith(String email)
/*
SELECT * FROM student WHERE phone LIKE '?%';
: student 테이블에서 phone 이 특정 패턴('?')으로 시작하는 모든 행을 선택하는 쿼리
*/
List<Student> findAllByPhoneStartingWith(String phone);
SELECT IN
/*
SELECT * FROM student WHERE age IN (10, 20, 30, 40, 50);
: student 테이블에서 age 가 10, 20, 30, 40, 50 인 모든 행을 선택하는 쿼리
-> IN 연산자는 주어진 값 목록 중 하나와 일치하는 행을 선택한다.
*/
List<Student> findAllByAgeIn(List<Integer> ages);
SELECT BETWEEN
/*
SELECT * FROM student WHERE age BETWEEN 30 AND 40;
: stduent 테이블에서 age 가 30 에서 40 사이인 모든 행을 선택하는 쿼리
*/
List<Student> findAllByAgeBetween(Integer over, Integer under);
참고 : 이 포스트는 "Techit Java Backend School 8기 교안"을 바탕으로 작성되었습니다.