[Spring Boot] Spring Data JPA

익선·2024년 7월 11일
0

스프링부트

목록 보기
1/8

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
    • Hibernate에서 실제로 실행한 SQL을 콘솔에서 확인
  • database-platform
    • Hibernate에서 사용할 SQL Dialect \to H2, SQLite

application.yaml 을 작성하였다면, 사용하고 있는 IDE에 datasource 추가 \to Main 실행 \to 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를 활용하고 싶을 수도 있다. 어떤 InstructorStudent 정보를 다 알고 싶다고 가정하면, 테이블을 기준으로 하면 해당 InstructorPK 기준으로 Student 테이블의 FK를 검색해 볼것이다. 그와 비슷하게 JpaRepositoryQueryMethod를 추가할 수 있다.
  • 반대쪽에 @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;
}

더미데이터 넣기 (resources 의 data.sql)

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 JPARespository 인터페이스이다.

우리가 만든 Entity 객체를 이용해 데이터베이스를 활용하고 싶다면 \to 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 메서드는 templatesstudent/create.html 을 템플릿으로 활용하며, 전체 Instructor 데이터를 Model 에 전달함으로서 사용자가 선택할 수 있는 Instructor 목록을 create.html 에서 확인할 수 있도록 해주고 있다.

create.htmlform 내의 select 요소는 전달받은 Instructor 데이터를 바탕으로 복수의 option 요소를 만든다. 이는 나중에 서버로 전송(<form action="/student/create" method="post">)될 때 advisor-id 의 이름으로 보내지며, 이를 컨트롤러에서 @RequestParam Long advisorId 으로 받아줄 수 있다.

전달받은 advisor-id 의 값을 바탕으로 서비스에서 Instructor 객체를 찾아낸다면, 해당 값을 student에 할당하여 관계를 설정할 수 있다.

  • 서비스에서 InstrutorRepo 주입
  • create()Long advisorId 파라미터 추가
  • 지도 교수 찾기(optional 사용)
  • 학생에 지도교수 할당
  • repositorysave() 메서드 호출
@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() 메서드를 통해 테이블에 저장된 데이터들을 사용자에게 보여주는 비즈니스 로직을 작성하자

전체 조회 : repositoryfindAll() 메서드 활용하면 데이터베이스에 저장된 특정 테이블의 모든 레코드를 조회할 수 있다.

@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

단일 조회 : repositoryfindById() 메서드 활용하면 데이터베이스에 저장된 특정 테이블의 특정 id 컬럼으로 검색한 레코드를 조회할 수 있다.

  • Optional
    • null 이 나올 가능성이 있을때 null 을 포함한 객체 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()

  1. 수정버튼을 누르면 GET 요청으로 컨트롤러의 updateView 메서드가 호출되어 해당 id 를 가진 Studentupdate-view 페이지로 넘어오게 된다.
  2. 그럼 이제 student 하위 update.html 이 보여지고, 데이터를 수정하고 나서 제출을 누르면 POST 요청으로 컨트롤러의 update() 메서드가 호출된다.
  3. update() 메서드가 실행되고나면 StudentServiceupdate() 메서드가 호출되어 student 객체를 수정 후 저장한다
  4. 그런 다음 해당 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기 교안"을 바탕으로 작성되었습니다.

profile
꾸준히 기록하는 사람

0개의 댓글