Intellij에서는 자바프로젝트해서 하면 되지만
sts에서는 다르게 한다.
Project: JPATest (starter 프로젝트로 만들면 안된다. -- 메인이 하나만 잡히므로)
Gradle로 만든다.
Gradle 6.6.1
JDK 11 버전으로 맞춰줘야한다.
이렇게 안하면 프로젝트와 진짜 프로젝트를 짜야할 lib가 두 개로 쪼개진다.
두 개로 쪼개지면서 build.gradle이 안 만들어져서 우리가 수동으로 만들어야한다.
그래서 하나로 통합해서 만들 때는 Gradle 버전은 내리고 JDK는 11버전으로 맞춰야한다.
인텔리제이는 그냥 자바 프로젝트로 만들어도 된다 !!
설치가 안 되는 사람들은 마켓 플레이스에서 저거 깔아주기 !!
https://mvnrepository.com/
총 4개 깔아주기 + Lombok
MySQL Connector Java 8.0.28
Hibernate Core Relocation 6.5.2 Final
Javax Persistence API ---> Jakarta Persistence API 3.2
JAXB API ---> Jakarta XML Binding API 4.0.2
Lombok 설치
nnotationProcessor 'org.projectlombok:lombok'
이것도 추가로 넣어주기 !!
JPATest
src/main/java
src/main/resources
META-INF
persistence.xml
persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
<persistence-unit name="entitytest">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Seoul"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="1234"/>
</properties>
</persistence-unit>
</persistence>
<persistence-unit name="entitytest">
</persistence-unit>
이거 이름은 맘대로 지어도 된다.
DROP TABLE IF EXISTS emp, dept, locations;
-- 1. locations 테이블 생성
CREATE TABLE LOCATIONS (
LOC_CODE CHAR(2) ,
CITY VARCHAR(20),
PRIMARY KEY (LOC_CODE)
);
INSERT INTO LOCATIONS VALUES ('A1','SEOUL');
INSERT INTO LOCATIONS VALUES ('B1','DALLAS');
INSERT INTO LOCATIONS VALUES ('C1','CHICAGO');
INSERT INTO LOCATIONS VALUES ('D1','BOSTON');
-- 2. dept 테이블 생성
CREATE TABLE DEPT (
DEPTNO INT PRIMARY KEY,
DNAME VARCHAR(20),
LOC_CODE CHAR(2),
FOREIGN KEY (LOC_CODE) REFERENCES LOCATIONS(LOC_CODE)
);
INSERT INTO DEPT VALUES (10,'ACCOUNTING','A1');
INSERT INTO DEPT VALUES (20,'RESEARCH','B1');
INSERT INTO DEPT VALUES (30,'SALES','C1');
INSERT INTO DEPT VALUES (40,'OPERATIONS','A1');
INSERT INTO DEPT VALUES (50,'INSA',null);
-- 3. emp 테이블 생성
CREATE TABLE EMP (
EMPNO INT PRIMARY KEY,
ENAME VARCHAR(14),
JOB VARCHAR(30),
MGR INT,
HIREDATE DATE,
SAL INT,
COMM INT,
DEPTNO INT,
FOREIGN KEY (DEPTNO) REFERENCES DEPT(DEPTNO)
);
INSERT INTO EMP VALUES
(7369,'SMITH','CLERK',7902,'1980-12-17',800,null,20);
INSERT INTO EMP VALUES
(7499,'ALLEN','SALESMAN',7698,'1981-02-20',1600,300,30);
INSERT INTO EMP VALUES
(7521,'WARD','SALESMAN',7698,'1981-02-22',1250,200,30);
INSERT INTO EMP VALUES
(7566,'JONES','MANAGER',7839,'1981-04-02',2975,30,20);
INSERT INTO EMP VALUES
(7654,'MARTIN','SALESMAN',7698,'1981-09-28',1250,300,30);
INSERT INTO EMP VALUES
(7698,'BLAKE','MANAGER',7839,'1981-04-01',2850,null,30);
INSERT INTO EMP VALUES
(7782,'CLARK','MANAGER',7839,'1981-06-01',2450,null,10);
INSERT INTO EMP VALUES
(7788,'SCOTT','ANALYST',7566,'1982-10-09',3000,null,20);
INSERT INTO EMP VALUES
(7839,'KING','PRESIDENT',null,'1981-11-17',5000,3500,10);
INSERT INTO EMP VALUES
(7844,'TURNER','SALESMAN',7698,'1981-09-08',1500,0,30);
INSERT INTO EMP VALUES
(7876,'ADAMS','CLERK',7788,'1983-01-12',1100,null,null);
INSERT INTO EMP VALUES
(7900,'JAMES','CLERK',7698,'1981-10-03',950,null,30);
INSERT INTO EMP VALUES
(7902,'FORD','ANALYST',7566,'1981-10-3',3000,null,20);
INSERT INTO EMP VALUES
(7934,'MILLER','CLERK',7782,'1982-01-23',1300,null,10);
위에처럼 하나하나 ctrl + enter 하는 방법 말고 !! 한 번에 하는 방법을 알려줄게 !!!
다 DROP 하구 !!
C:\Program Files\MySQL\MySQL Server 8.0\bin
위의 위치에서 터미널을 열어준다 !!
.\mysql -u root -p
테이블 잘 생성된 것을 확인할 수 있다 !!
JPA 프로그래밍
- JDBC만을 이용한 영속 계층
JDBC(Java Database Connectivity)는 Java에서 데이터베이스에 접속하고 데이터 처리 구현에 사용 되는 Java API 이다. JDBC의 장점은 각각의 DBMS가 제공하는 JDBC 드라이버를 이용하여 DBMS의 종류와 상관없이 하나의 JDBC API를 이용하여 DB 작업을 처리할 수 있다는 것이다. 그러나 JDBC만 을 이용하여 개발 할 경우에는 불편한 점이 꽤 많다.
- 간단한 SQL을 실행함에도 중복된 코드를 반복적으로 사용
- DB에 따라 일관성 없는 정보를 가진채로 Checked Exception(SQLException) 처리
- 연결과 같은 공유 자원을 제대로 반환하지 않으면 시스템의 자원 부족 현상 발생
그래서 이런 복잡하고 번거로운 작업을 해결하기 위해 데이터베이스와 연동되는 시스템을 빠르게 개발할 수 있는 Persistence Framework를 사용하기 시작하였다.
- SQL Mapper을 이용한 영속 계층
SQL Mapper는 객체와 SQL을 매핑하여 데이터를 객체화 하는 Persistence Framework 이다.
즉, 테이블과 객체 간의 관계를 매핑하는 것이 아닌
직접 작성한 SQL문의 결과와 객체의 필드를 매핑하는 것이 메인 컨셉이다.
SQL Mapper를 이용하면 기존에 JDBC 만을 이용했을 때보다 코드가 간결해 지고 유지보수성이 향상된다.
그러나 SQL Mapper가 JDBC 만을 사용했을 때보다 많은 불편함을 해소해 주었음에도 여전히 문제가 있다.
- SQL을 직접 작성하기에 특정 DB에 종속적
- 비슷한 CRUD SQL 작성 및 DAO를 반복적으로 개발
- 테이블 필드 변경 시 유지보수하기 힘듦
즉, 코드상에서는 SQL과 JDBC API를 분리했어도 논리적으로는 강한 의존성을 가지고 있게 되며
SQL에 의존적인 개발을 해야 하는 문제는 여전히 발생한다.
- ORM을 이용한 영속 계층
ORM이란 Object Relational Mapping, 객체-관계 매핑의 줄임말로
OOP에서 객체를 구현한 클래스 와 RDBMS에서 사용하는 테이블을 자동으로 매핑하는 것을 의미한다.
ORM은 객체 간의 관계를 바탕으로 정해진 메서드를 호출하면 관련 SQL문을 자동으로 생성하여
직관적으로 데이터를 조작하게 하는 방법으로 개발자의 불편함을 해소한다
Java ORM의 대표적인 기술은 바로 JPA가 있다.
원래 JPA 기술이 나오기 전까지는 EJB(Entity Bean) 이라는 Java 표준 기술이 있었다.
하지만 워낙 복잡하다 보니 Gavin King이라는 개발자가 하이버네이트라는 오픈 소스를 만들어냈고
Java 진영에서 Gavin King을 영입하여 하이버네이트 기반의 Java ORM 표준인 JPA를 만들었다.
JPA 란?
데이터베이스는 객체 구조와는 다른 데이터 중심의 구조를 가지므로
객체를 데이터베이스에 직접 저장하거나 조회할 수 없다.
따라서 개발자가 객체지향 애플리케이션과 데이터베이스 중간에서 SQL과 JDBC API를 사용해서
변환 작업 (객체 → 데이터베이스)을 직접 해주어야 한다.
JPA는 Java Persistence API 의 약자로서,
자바 객체와 관계형 데이터베이스의 데이터(테이블)를 매핑하기 위해 사용되는
ORM(Object-Relational Mapping) 기술의 Java 표준 명세이다.
클래스와 테이블은 기존부터 호환 가능성을 두고 만들어진 것이 아니므로 불일치가 발생하는데
이를 ORM을 통해서 객체 간의 관계를 바탕으로 SQL문을 자동 생성하여 불일치를 해결한다.
ORM을 사용하면 SQL문을 구현할 필요 없이 객체를 통해 간접적으로 데이터베이스를 조작할 수 있게 된다.
JPA 는 Java ORM에 대한 API 표준 명세이며, 인터페이스의 모음이다.
따라서 구현체가 없으므로, 사용하기 위해서는 ORM프레임워크를 선택해야 한다.
다양한 프레임워크가 존재하지만 가장 대중적 인 것은 하이버네이트이다.
Entity 란 DB 테이블의 데이터를 OOP스럽게 다루기 위해 사용하는 DB 테이블의 모델링 클래스이다.
DB 테이블의 컬럼들에 매핑되는 필드를 선언하고 기본 생성자, setter, getter 등의 메서드
그리고 Primary Key에 매핑되는 Identity 역할의 Key 필드(@Id)를 선언한다.
Persistence Framework
JDBC 프로그래밍에서 경험하게 되는 복잡함이나 번거로움 없이
간단한 작업만으로 데이터베이스와 연동되는 시스템을 빠르게 개발할 수 있다.
일반적으로 SQL Mapper와 ORM으로 나눠진다.
[ SQL Mapper ]
직접 작성한 SQL 문장으로 데이터베이스 데이터를 다룬다.
Mybatis, JdbcTemplates(Spring)
[ ORM ]
객체를 통해서 간접적으로 데이터베이스의 데이터를 다룬다.
객체와 관계형 데이터베이스의 데이터를 자동으로 맵핑시킨다.
JPA, Hibernate 등
JPA는 애플리케이션과 JDBC 사이에서 동작한다. JPA 내부에서 JDBC API를 사용하여 SQL을 호출하여 DB와 통신한다. 개발자가 ORM 프레임워크에 저장하면 적절한 INSERT SQL을 생성해 데이터베이스에 저장해주고,
검색을 하면 적절한 SELECT SQL을 생성해 결과를 객체에 매핑하고 전달한다.
EntityManager 객체 생성과 Entity 클래스 정의
1. persistence.xml 파일을 통해서 JPA 관련 정보를 설정한다.
2. EntityManagerFactory 객체를 생성한다.( 태그의 이름 정보 지정)
3. EntityManager 객체를 생성하여 Entity 객체를 영속성 컨텍스트(Persistence Context)를 통해 관리한다.
(Spring Boot랑 연결하게 되면 Persistence 필요없어진다 !!)
EntityManagerFactory
데이터베이스와 상호 작용을 위한 EntityManager 객체를 생성하기 위해 사용되는 객체로서
애플리케이션에서 한 번만 객체를 생성하고 공유해서 사용한다.
Thread-Safe 하므로 여러 스레드에서 동시에 접근해도 안전하다.
EntityManagerFactory 객체를 통해 생성되는 모든 EntityManager 객체는 동일한 데이터베이스에 접속한다.
EntityManager
Entity 객체를 관리하는 객체이다
데이터베이스에 대한 CRUD 작업은 모두 영속성 컨텍스트를 사용하는 EntityManager 객체를 통해 이루어진다.
동시성의 문제가 발생할 수 있으니 여러 스레드가 공유하지 않는다.
모든 데이터 변경은 트랜잭션 안에서 이루어져야 한다.(트랜잭션을 시작하고 처리해야 함)
-------- 데이터베이스의 논리적인 작업 단위
flush()
영속성 컨텍스트(Persistence Context)의 변경 내용을 데이터베이스에 반영한다.
일반적으로는 flush() 메서드를 직접 사용하지는 않고,
Java 애플리케이션에서 커밋 명령이 들어왔을 때 자동으로 실행된다.
detach()
특정 Entity를 준영속 상태(영속 컨텍스트의 관리를 받지 않음)로 바꾼다.
clear()
Persistence Context를 초기화한다.
close()
Persistence Context를 종료한다.
merge()
준영속 상태의 엔티티를 이용해서 새로운 영속 상태의 엔티티를 반환한다.
find()
식별자 값(Key 필드)을 통해 Entity를 찾는다.(DB 테이블의 데이터 또는 행을 찾는다.)
persist()
생성된 Entity 객체를 영속성 컨텍스트(Persistence Context)에 저장한다.
remove()
식별자 값을 통해 영속성 컨텍스트(Persistence Context)에서 Entity 객체를 삭제한다.
Entity
JPA에서 엔티티란 DB 테이블에 대응하는 클래스이고 엔티티 객체는 DB 테이블의 데이터(하나의 행) 이다.
@Entity 어노테이션이 붙은 클래스를 JPA에서는 엔티티라고 부르며,
이 엔티티는 영속성 컨텍 스트에 담겨 EntityManager에 의해서 관리된다.
@Entity
클래스 레벨에 적용한다.
해당 클래스를 테이블과 매핑한다고 JPA에게 알려주는 역할을 한다.
해당 어노테이션이 적용된 클래스를 엔티티 클래스라고 한다
@Table
클래스 레벨에 적용한다.
엔티티 클래스에 매핑할 테이블 이름을 JPA에게 알려주는 역할을 한다.
@Table(name = "테이블이름")
해당 어노테이션을 생략하면 클래스 이름에서 모든 글자를 소문자로 변경하여 테이블을 생성한다.
@Id
필드 레벨에 적용한다.
엔티티 클래스의 필드를 테이블의 기본 키 (Primary Key)에 매핑한다.
해당 어노테이션이 적용된 필드를 식별자 필드라고 한다.
@Column
필드 레벨에 적용한다.
해당 필드를 테이블의 지정된 컬럼에 매핑한다.
name 속성으로 매핑할 컬럼의 이름을 지정하며 nullable, unique, length 등도 설정 가능하다.
@Column(name = "매핑할 컬럼명")
name 속성을 생략하거나 @Column 어노테이션을 생략하면 필드명을 그대로 사용해서 컬럼명으로 매핑한다.
이름이 age인 필드에 매핑 정보를 생략하면, age 컬럼으로 자동 매핑한다.
대소문자를 구분하는 DB를 사용한다면 @Column 을 사용하여 명시적으로 매핑해야 한다.
JPATest
src/main/java
exam.app
HelloJPA01.java --- main 메서드
src/main/resources
META-INF
persistence.xml
HelloJPA01.java
package exam.app;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
public class HelloJPA01 {
public static void main(String[] args) {
EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest");
System.out.println("EntityManagerFactory = " + factory.getClass().getName());
EntityManager manager = factory.createEntityManager();
System.out.println("EntityManager = " + manager.getClass().getName());
factory.close();
manager.close();
}
}
JPATest
src/main/java
exam.app
HelloJPA01.java --- main 메서드
exam.entity.Emp
Emp.java
src/main/resources
META-INF
persistence.xml
@Entity
클래스 레벨에 적용한다.
해당 클래스를 테이블과 매핑한다고 JPA에게 알려주는 역할을 한다.
해당 어노테이션이 적용된 클래스를 엔티티 클래스라고 한다
package exam.entity.Emp;
import jakarta.persistence.Entity;
@Entity
public class Emp {
}
영속성 컨텍스트에서 관련 SQL을 데이터베이스에 전달할 때는 데이터베이스별 Dialect(방언)을 적용한다.
Dialect 관련 설정은 persistence.xml 에 다음 내용을 작성한다.
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
<persistence-unit name="entitytest">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Seoul"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="1234"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
<property name="hubernate.show_sql" value="true"/>
<property name="hubernate.format_sql" value="true"/>
<property name="hubernate.use_sql_comments" value="true"/>
<property name="hubernate.hbm2ddl.auto" value="update"/>
</properties>
</persistence-unit>
</persistence>
JPATest
src/main/java
exam.app
HelloJPA01.java --- main 메서드
exam.entity.Emp -- 이거 지워 !!
Emp.java
exam.entity
EntityTest01.java
src/main/resources
META-INF
persistence.xml
EntityTest01.java
package exam.entity;
import java.time.LocalDateTime;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
@Data
@Entity
@Table(name="entitytesttbl")
public class EntityTest01 {
@Id
private int id;
private String name;
private int age;
private LocalDateTime birthday;
}
persistence.xml
<class>exam.entity.EntityTest01</class> -- 추가하기
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
<persistence-unit name="entitytest">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>exam.entity.EntityTest01</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Seoul"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="1234"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
</properties>
</persistence-unit>
</persistence>
JPATest
src/main/java
exam.app
HelloJPA01.java --- main 메서드
EntityTestApp01.java
exam.entity
EntityTest01.java
src/main/resources
META-INF
persistence.xml
EntityTestApp01.java
package EntityTestApp01;
import java.time.LocalDateTime;
import exam.entity.EntityTest01;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
public class EntityTestApp01 {
public static void main(String[] args) {
EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest");
EntityManager manager = factory.createEntityManager();
EntityTest01 entityTest01;
manager.getTransaction().begin();
entityTest01 = new EntityTest01();
entityTest01.setId(10);
entityTest01.setName("홍길동");
entityTest01.setAge(25);
entityTest01.setBirthday(LocalDateTime.now());
manager.persist(entityTest01); //1차 cash 메모리에 저장했다가
manager.getTransaction().commit(); // commit 시점에 들어오는 것이다 !
manager.close();
factory.close();
}
}
영속성 컨텍스트(persistence context)
- 어플리케이션과 데이터베이스 사이에 존재하는 논리적인 개념으로 엔티티를 저장하고 관리하는 영역이다.
- EntityManager 객체당 한 개의 영속성 컨텍스트가 사용되며 EntityManager 객체를 통해서만 접근이 가능하다
- 영속성 컨텍스트는 엔티티들을 식별자(id) 값으로 구분하며 1차 캐시에 보관한다.
- 쓰기 지연을 지원하여 커밋하기 전까지는 DB 테이블에 저장하지 않고 SQL 저장소에 관련 SQL문만 보관하며
트랜잭션을 커밋하는 시점에 영속성 컨텍스트를 플러시하여
보관되어 있던 SQL 문들을 DB 서버에 전송한 후 커밋 처리한다.
- 플러시한 다음에는 엔티티 상태들을 복사하여 저장한 스냅샷이 존재하며 스냅샷과 엔티티를 비교해
변경된 엔티티를 찾고, 있다면 수정 쿼리를 SQL 저장소에 보관한다.
- 영속성 컨텍스트에 존재하는 엔티티들의 플러시 처리 시점
- entityManger.flush() 로 플러시 기능을 직접 호출
- 트랜젝션 커밋(commit) 시 entityManger.flush() 메서드 자동 호출됨
- JPQL 쿼리 실행 시 entityManger.flush() 메서드가 자동 호출됨
엔티티 생명주기
Entity에는 아래와 같은 4가지의 상태가 존재한다.
1. 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
2. 영속(managed): 영속성 컨텍스트에 저장된 상태
3. 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
4. 삭제(removed): 삭제된 상태
엔티티 생성과 저장
1. Java 어플리케이션에서 어떤 엔티티 객체를 생성하여 JPA에게 데이터베이스 저장을 요구하면,
2. 만들어진 엔티티는 1차적으로 영속성 컨텍스트에 저장된다.(1차 캐시)
그리고, 저장한 엔티티를 데이터베이스에 저장하기 위한 쿼리문을 생성시켜 쓰기 지연 SQL 저장소에 저장한다.
계속해서 엔티티를 넘기면 엔티티들과 쿼리문들은 차곡차곡 영속성 컨텍스트에 저장된다.
3. Java 어플리케이션에서 커밋 명령이 내려지면 영속 컨텍스트에는 자동으로 flush( )가 호출되고,
4. 영속성 컨텍스트의 변경내용을 데이터베이스와 동기(flush)화 한다. -> SQL 저장소의 쿼리를 실행시킨다.
5. 마지막으로 데이터베이스에게 commit 쿼리문을 명령한다.
엔티티 조회
1. Java 어플리케이션에서 JPA에게 데이터베이스 조회를 부탁하면, 1차적으로 영속성 컨텍스트에서 엔티티를 찾는다.
2. 있으면 Java 어플리케이션에 엔티티를 넘긴다.
3. 영속성 컨텍스트에 없는 엔티티 조회를 요구하면
4. 쿼리문을 사용해 데이터베이스에서 찾아와
5. 영속성 컨텍스트에 엔티티로 저장하고
6. Java 어플리케이션에 그 엔티티를 넘긴다.
엔티티 변경
JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 현재의 상태를 복사해서 저장해 두는데,
이것을 스냅샷이라 한다.
1. Java 어플리케이션에서 커밋 명령이 들어오면, 영속 컨텍스트에는 자동으로 flush( )가 호출되고,
2. 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
3. 변경된 엔티티가 있으면 데이터베이스에 변경사항을 저장하기 위해 쿼리를 생성하고,
4. 영속성 컨텍스트의 변경내용을 데이터베이스와 동기(flush)화 한다(SQL 저장소의 쿼리를 실행시 킨다).
5. 마지막으로 데이터베이스에게 commit 쿼리문을 명령한다.
이렇게 엔티티의 변경사항을 데이터베이스에 자동으로 반영하는 기능을 변경감지(Dirty Checking) 라 한다.
엔티티 삭제
앞의 과정과 마찬가지로, Java 어플리케이션에서 엔티티 삭제 명령이 들어오면,
엔티티를 찾고 쓰기 지연 SQL 저장소에 delete 쿼리를 생성한다.
그리고 Java 어플리케이션에서 커밋 명령이 들어오면, 자동으로 flush( )가 호출되고,
영속성 컨텍스트의 변경내용을 데이터베이스와 동기(flush)화 한다(SQL 저장소의 쿼리를 실행시킨다).
마지막으로 데이터베이스에게 commit 쿼리문을 명령한다
트랜잭션 관리
- JPA를 사용하면, 항상 트랜잭션 안에서 데이터를 변경(CUD)해야 한다.
- 트랜잭션 없이 데이터를 변경하면 예외가 발생한다.
- 트랜잭션을 시작하려면, 엔티티 매니저에서 트랜잭션 API를 받아와야 한다.
JPA 객체지향 쿼리 언어
- JPQL
- JPA Criteria
- QueryDSL
- 네이티브 SQL
- JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 과 함께 사용
JPQL이란?
JPA 에서 엔티티 객체를 조회할 때 사용하는 객체지향 쿼리이다.
문법은 SQL과 비슷하고 ANSI 표준 SQL이 제공하는 기능을 유사하게 지원한다.
SQL은 데이터베이스 테이블을 대상으로 JPQL은 엔티티 객체를 대상으로 쿼리한다.
JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않으며 실행 시 SQL로 변환된다
JPA 기본키 매핑
기본키(primary key)를 매핑하는 방법은 2가지로 직접 할당과 자동 생성이 있다.
직접 할당은 엔티티에 @Id 어노테이션만 사용해서 직접 할당하는 것이다.
자동 생성은 엔티티에 @Id와 @GeneratiedValue를 추가하고 원하는 키 생성 전략을 선택한다.
자동 생성 같은 경우에는 MySQL의 AUTO_INCREMENT 같은 기능으로 생성된 값을 기본키로 사용 하는 것이다.
- 기본키 자동 생성 전략 - IDENTITY
기본키 생성을 DB에 위임하는 전략이다.
MySQL, PostgreSQL, SQL Server, DB2에서 사용한다.
(MySQL의 AUTO_INCREMENT)
- 기본키 자동 생성 전략 - SEQUENCE
유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트이다.
이 시퀀스를 사용해서 기본키를 생성하게 된다.
시퀀스를 지원하는 Oracle, PostgreSQL, DB2, H2 Database에서 사용할 수 있다
EntityTest01.java
package exam.entity;
import java.time.LocalDateTime;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
@Data
@Entity
@Table(name="entitytesttbl")
public class EntityTest01 {
@Id
private int id;
private String name;
private int age;
private LocalDateTime birthday;
}
여기서 id는 중복이 되면 안되므로 auto_increment를 걸어줘야한다 !!
그리고 우리는 persistence.xml에서 update로 해놨기 때문에 또 생성해도 된다
persistence.xml
create로 바꾸고 다시 실행해보기 !!
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
<persistence-unit name="entitytest">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>exam.entity.EntityTest01</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Seoul"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="1234"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="create"/>
</properties>
</persistence-unit>
</persistence>
기존의 데이터 날라간다 !
이제 id가 auto_increment가 되게 해볼 것이다 !!
EntityTest01.java
@GeneratedValue(strategy = GenerationType.IDENTITY)
package exam.entity;
import java.time.LocalDateTime;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
@Data
@Entity
@Table(name="entitytesttbl")
public class EntityTest01 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private int age;
private LocalDateTime birthday;
}
EntityTestApp01.java
package exam.app;
import java.time.LocalDateTime;
import exam.entity.EntityTest01;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Persistence;
public class EntityTestApp01 {
public static void main(String[] args) {
EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest");
EntityManager manager = factory.createEntityManager();
EntityTest01 entityTest01;
manager.getTransaction().begin();
// @GeneratedValue(strategy = GenerationType.IDENTITY) 설정하기 전
// entityTest01 = new EntityTest01();
// entityTest01.setId(10);
// entityTest01.setName("홍길동");
// entityTest01.setAge(25);
// entityTest01.setBirthday(LocalDateTime.now());
// @GeneratedValue(strategy = GenerationType.IDENTITY) 설정한 후
for(int i=1; i<=5; i++) {
entityTest01 = new EntityTest01();
entityTest01.setName("홍길동" + i);
entityTest01.setAge((int)(Math.random()*26+25));
entityTest01.setBirthday(LocalDateTime.now());
manager.persist(entityTest01); //1차 cash 메모리에 저장했다가 (for문 안으로 들어와야함 !)
}//for
manager.getTransaction().commit(); // commit 시점에 들어오는 것이다 !
manager.close();
factory.close();
}
}
JPATest
src/main/java
exam.app
HelloJPA01.java --- main 메서드
EntityTestApp01.java
EntityTestApp02.java
exam.entity
EntityTest01.java
EntityTest02.java
src/main/resources
META-INF
persistence.xml
EntityTest02.java
package exam.entity;
import java.time.LocalDateTime;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class EntityTest02 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private int num1;
private Integer num2;
}
EntityTestApp02.java
package exam.app;
import java.util.List;
import exam.entity.EntityTest01;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import jakarta.persistence.TypedQuery;
public class EntityTestApp02 {
public static void main(String[] args) {
EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest");
EntityManager manager = factory.createEntityManager();
TypedQuery<EntityTest01> tq = manager.createQuery("select t from EntityTest01 t", EntityTest01.class);
List<EntityTest01> list = tq.getResultList();
for(EntityTest01 et : list) {
System.out.println(et);
}
manager.close();
factory.close();
}
}
persistence.xml
update로 다시 변경 !!
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
<persistence-unit name="entitytest">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>exam.entity.EntityTest01</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Seoul"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="1234"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
</properties>
</persistence-unit>
</persistence>
stream을 사용해보자 !!
// for(EntityTest01 et : list) {
// System.out.println(et);
// }
list.stream().forEach(e -> System.out.println(e));
주석건 것과 같은 코드임을 확인 !!
JPATest
src/main/java
exam.app
HelloJPA01.java --- main 메서드
EntityTestApp01.java
EntityTestApp02.java
EntityTestApp03.java
exam.entity
EntityTest01.java
EntityTest02.java
Person.java
src/main/resources
META-INF
persistence.xml
EntityTestApp03.java
새로운 Person 테이블의 엔티티 객체를 생성한 다음 3개의 데이터를 입력 후 모두 조회하시오.
컬럼 : seq(기본키), name, age
데이터 : "홍길동", 25
"이제훈", 40
"김태리", 32
Person.java
package exam.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
@Data
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int seq;
private String name;
private int age;
}
EntityTestApp03.java
package exam.app;
import exam.entity.Person;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import jakarta.persistence.TypedQuery;
import java.util.List;
public class EntityTestApp03 {
public static void main(String[] args) {
EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest");
EntityManager manager = factory.createEntityManager();
manager.getTransaction().begin();
Person person1 = new Person();
person1.setName("홍길동");
person1.setAge(25);
Person person2 = new Person();
person2.setName("이제훈");
person2.setAge(40);
Person person3 = new Person();
person3.setName("김태리");
person3.setAge(32);
manager.persist(person1);
manager.persist(person2);
manager.persist(person3);
manager.getTransaction().commit();
TypedQuery<Person> query = manager.createQuery("select p from Person p", Person.class);
List<Person> list = query.getResultList();
list.stream().forEach(System.out::println);
manager.close();
factory.close();
}
}
3가지 방법 다 알고있기 !!
persistence.xml
<class>exam.entity.EntityTest01</class>
<class>exam.entity.EntityTest02</class>
<class>exam.entity.Person</class>
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
<persistence-unit name="entitytest">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>exam.entity.EntityTest01</class>
<class>exam.entity.EntityTest02</class>
<class>exam.entity.Person</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Seoul"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="1234"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
</properties>
</persistence-unit>
</persistence>
JPATest
src/main/java
exam.app
HelloJPA01.java --- main 메서드
EntityTestApp01.java -- 입력
EntityTestApp02.java -- 출력
EntityTestApp03.java -- 문제
EntityTestApp04.java -- flush하는거 보여주려다 실패
exam.entity
EntityTest01.java
EntityTest02.java
Person.java
src/main/resources
META-INF
persistence.xml
EntityTestApp04.java
package exam.app;
import java.time.LocalDateTime;
import java.util.List;
import exam.entity.EntityTest01;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import jakarta.persistence.TypedQuery;
public class EntityTestApp04 {
public static void main(String[] args) {
EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest");
EntityManager manager = factory.createEntityManager();
manager.getTransaction().begin();
EntityTest01 entityTest01;
for(int i=1; i<=5; i++) {
entityTest01 = new EntityTest01();
entityTest01.setName("또치" + i);
entityTest01.setAge((int)(Math.random()*26+25));
entityTest01.setBirthday(LocalDateTime.now());
manager.persist(entityTest01);
}//for
manager.getTransaction().commit();
TypedQuery<EntityTest01> tq = manager.createQuery("select t from EntityTest01 t", EntityTest01.class);
List<EntityTest01> list = tq.getResultList();
list.stream().forEach(System.out::println);
manager.close();
factory.close();
}
}
DELETE FROM entitytesttbl WHERE name LIKE '또치%';
flush( ) / rollback 하는거 보여주려 했는데 실패
package exam.app;
import java.time.LocalDateTime;
import java.util.List;
import exam.entity.EntityTest01;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import jakarta.persistence.TypedQuery;
public class EntityTestApp04 {
public static void main(String[] args) {
EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest");
EntityManager manager = factory.createEntityManager();
manager.getTransaction().begin();
EntityTest01 entityTest01;
for(int i=1; i<=5; i++) {
entityTest01 = new EntityTest01();
entityTest01.setName("또치" + i);
entityTest01.setAge((int)(Math.random()*26+25));
entityTest01.setBirthday(LocalDateTime.now());
manager.persist(entityTest01);
}//for
TypedQuery<EntityTest01> tq = manager.createQuery("select t from EntityTest01 t", EntityTest01.class);
List<EntityTest01> list = tq.getResultList();
list.stream().forEach(System.out::println);
manager.flush();
System.out.println("-----------------------");
tq = manager.createQuery("select t from EntityTest01 t", EntityTest01.class);
list = tq.getResultList();
list.stream().forEach(System.out::println);
manager.getTransaction().rollback();
manager.close();
factory.close();
}
}
JPATest
src/main/java
exam.app
HelloJPA01.java --- main 메서드
EntityTestApp01.java -- 입력
EntityTestApp02.java -- 출력
EntityTestApp03.java -- 문제
EntityTestApp04.java -- flush하는거 보여주려다 실패
EntityTestApp05.java -- 삭제
exam.entity
EntityTest01.java
EntityTest02.java
Person.java
src/main/resources
META-INF
persistence.xml
EntityTestApp05.java
package exam.app;
import java.util.List;
import exam.entity.EntityTest01;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import jakarta.persistence.TypedQuery;
public class EntityTestApp05 {
public static void main(String[] args) {
EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest");
EntityManager manager = factory.createEntityManager();
manager.getTransaction().begin();
System.out.println("엔티티 삭제");
EntityTest01 emEntityTest01 = manager.find(EntityTest01.class, 5);
manager.remove(emEntityTest01);
TypedQuery<EntityTest01> tq = manager.createQuery("select t from EntityTest01 t", EntityTest01.class);
List<EntityTest01> list = tq.getResultList();
list.stream().forEach(System.out::println);
manager.getTransaction().commit();
manager.close();
factory.close();
}
}
JPATest
src/main/java
exam.app
HelloJPA01.java --- main 메서드
EntityTestApp01.java -- 입력
EntityTestApp02.java -- 출력
EntityTestApp03.java -- 문제
EntityTestApp04.java -- flush하는거 보여주려다 실패
EntityTestApp05.java -- 삭제
exam.entity
EntityTest01.java
EntityTest02.java
Person.java
EntityTest03.java
src/main/resources
META-INF
persistence.xml
persistence.xml
<class>exam.entity.EntityTest03</class> 추가하기
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
<persistence-unit name="entitytest">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>exam.entity.EntityTest01</class>
<class>exam.entity.EntityTest02</class>
<class>exam.entity.Person</class>
<class>exam.entity.EntityTest03</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Seoul"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="1234"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
</properties>
</persistence-unit>
</persistence>
EntityTest03.java
package exam.entity;
import java.math.BigDecimal;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import lombok.Data;
@Data
@Entity
public class EntityTest03 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(nullable = false, length = 10)
private String name;
@Column(columnDefinition = "varchar(15) default '파랑'")
private String favoriteColor;
@Column(name="age", nullable = false)
private int num1;
@Column(name="score", precision = 6, scale = 2) //숫자가 6게, 소수이하 자리수 2자리, 9999.99
private BigDecimal num2;
@Lob
private byte[] content1;
@Lob
private char[] content2;
@Lob
private String content3;
}
JPATest
src/main/java
exam.app
HelloJPA01.java --- main 메서드
EntityTestApp01.java -- 입력
EntityTestApp02.java -- 출력
EntityTestApp03.java -- 문제
EntityTestApp04.java -- flush하는거 보여주려다 실패
EntityTestApp05.java -- 삭제
exam.entity
EntityTest01.java
EntityTest02.java
Person.java
EntityTest03.java
EntityTest04.java
src/main/resources
META-INF
persistence.xml
persistence.xml
<class>exam.entity.EntityTest04</class> 추가하기
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
<persistence-unit name="entitytest">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>exam.entity.EntityTest01</class>
<class>exam.entity.EntityTest02</class>
<class>exam.entity.Person</class>
<class>exam.entity.EntityTest03</class>
<class>exam.entity.EntityTest04</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Seoul"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="1234"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
</properties>
</persistence-unit>
</persistence>
EntityTest04.java
package exam.entity;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
@Entity
public class EntityTest04 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Temporal(TemporalType.TIME) //DB 타입이 time
private java.util.Date utilTime;
@Temporal(TemporalType.DATE) //DB 타입이 date
private java.util.Date utilDate;
@Temporal(TemporalType.TIMESTAMP) //DB 타입이 datetime
private java.util.Date utilTimestamp;
private java.util.Date utilPlainDate; //DB 타입이 datetime
private java.util.Date sqlPlainDate; //DB 타입이 date
@Column(columnDefinition = "TIME")
private LocalTime localTime1; //DB 타입이 TIME
//@Column이 없는 경우
private LocalTime localTime2; //DB 타입이 time(6)
@Column(columnDefinition = "DATE")
private LocalDate localDate1; //DB 타입이 DATE
//@Column이 없는 경우
private LocalDate localDate2; //DB 타입이 date
@Column(columnDefinition = "TIMESTAMP")
private LocalDateTime localDateTime1; //DB 타입이 TIMESTAMP null
//@Column이 없는 경우
private LocalDateTime localDateTime2; //DB 타입이 datetime(6)
}
'Spring Boot' 카테고리의 다른 글
DAY 90 - JPA (2024.11.13) - 쿼리메서드 / 메인화면, 회원가입폼 (2024.11.13) (0) | 2024.11.14 |
---|---|
DAY 89 - JPA (2024.11.12) (1) | 2024.11.12 |
DAY 86 - Spring Boot DB연결 + Thymeleaf (2024.11.07) (1) | 2024.11.08 |
DAY 85 - Thymeleaf (2024.11.06) (0) | 2024.11.06 |
DAY 84 - Thymeleaf (2024.11.05) (0) | 2024.11.06 |