
@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Table(name = "memo") // 매핑할 테이블의 이름을 지정, 안 하면 Entity class와 동명의 테이블
public class Memo {
@Id // 기본키를 지정, PK는 필수적으로 달아 주자
private Long id;
// nullable: null 허용 여부
// unique: 중복 허용 여부 (false 일때 중복 허용)
// 데이터베이스와 필드명을 매핑
@Column(name = "username", nullable = false, unique = true)
private String username;
// length: 컬럼 최대 길이 지정 (문자열 길이 지정)
@Column(name = "contents", nullable = false, length = 500)
private String contents;
}
Entity와 연동 시 기본 생성자가 있어야 한다
이미 만들어있는 테이블과 매핑할 때는 서로 충돌하지 않게 더더욱 주의하자
java 파일을 보고 자동으로 테이블을 생성한다
create table memo (
id bigint not null auto_increment,
contents varchar(500) not null,
username varchar(255) not null,
primary key (id)
) engine=InnoDB
src > main > resources > META-INF > persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="memo">
<class>com.sparta.entity.Memo</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="{비밀번호}"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/memo"/>
<property name="hibernate.hbm2ddl.auto" value="create" />
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
</properties>
</persistence-unit>
</persistence>
id bigint not null auto_increment : auto_increment 조건이 추가된 것을 확인할 수 있습니다.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="memo">
<class>com.sparta.entity.Memo</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="{비밀번호}"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/memo"/>
<property name="hibernate.hbm2ddl.auto" value="create" />
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
</properties>
</persistence-unit>
</persistence>EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
EntityManager em = emf.createEntityManager();
EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");EntityManager em = emf.createEntityManager(); 코드를 호출하면 EntityManagerFactory를 사용하여 EntityManager를 생성할 수 있습니다.
트랜색션 내에서 commit을 하기 전까지는 DB에 작업이 반영되지 않는다.
JPA에서도 영속성 컨텍스트로 관리하고 있는 객체들의 정보 (변경이 발생한 것들)을 지연 저장소에 가지고 있다가 SQL을 한 번에 DB에 요청해 변경을 반영한다.
@Test
@DisplayName("EntityTransaction 성공 테스트")
void test1() {
EntityTransaction et = em.getTransaction(); // EntityManager 에서 EntityTransaction 을 가져옵니다.
et.begin(); // 트랜잭션을 시작합니다.
try { // DB 작업을 수행합니다.
Memo memo = new Memo(); // 저장할 Entity 객체를 생성합니다.
memo.setId(1L); // 식별자 값을 넣어줍니다.
memo.setUsername("Robbie");
memo.setContents("영속성 컨텍스트와 트랜잭션 이해하기");
em.persist(memo); // EntityManager 사용하여 memo 객체를 영속성 컨텍스트에 저장합니다.
et.commit(); // 오류가 발생하지 않고 정상적으로 수행되었다면 commit 을 호출합니다.
// commit 이 호출되면서 DB 에 수행한 DB 작업들이 반영됩니다.
} catch (Exception ex) {
ex.printStackTrace();
et.rollback(); // DB 작업 중 오류 발생 시 rollback 을 호출합니다.
} finally {
em.close(); // 사용한 EntityManager 를 종료합니다.
}
emf.close(); // 사용한 EntityManagerFactory 를 종료합니다.
}


JPA가 트랜잭션처럼 SQL을 모아서 한 번에 DB에 반영할 때 사용하는 저장소
em.flush() : 영속성 컨텍스트의 변화를 DB에 반영, et.commit() 시 자동 수행됨 EntityTransaction et = em.getTransaction();
et.begin();
하지 않고 플러시 메서드를 호출하면 no transaction is in progress 메시지와 함께 TransactionRequiredException 오류가 발생합니다.
- Insert, Update, Delete 즉, 데이터 변경 SQL을 DB에 요청 및 반영하기 위해서는 트랜잭션이 필요합니다.
- Select 시에는 특정 케이스 이외에 필수적이진 않다.
em.persist(객체); 메서도 호출 시 캐시 저장소에 저장
데이터의 변경이 발생하지 않으므로 트랜잭션이 없어도 조회 가능
em.find(Memo.class, 1); 호출 시 캐시 저장소를 확인 한 후 해당 값이 없다면?
- '1차 캐시' 사용의 장점
- DB 조회 횟수를 줄임
- '1차 캐시'를 사용해 DB row 1개 당 객체 1개가 사용되는 것을 보장 (객체 동일성 보장)
- 객체 동일성 보장 (같은 id 값으로 조회할 경우 같은 객체가 리턴됨)
em.remove(객체); 를 수행@Test
@DisplayName("변경 감지 확인")
void test8() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
System.out.println("변경할 데이터를 조회합니다.");
Memo memo = em.find(Memo.class, 4);
System.out.println("memo.getId() = " + memo.getId());
System.out.println("memo.getUsername() = " + memo.getUsername());
System.out.println("memo.getContents() = " + memo.getContents());
System.out.println("\n수정을 진행합니다.");
memo.setUsername("Update");
memo.setContents("변경 감지 확인");
System.out.println("트랜잭션 commit 전");
et.commit(); // 이때 DB에 변경 사항 반영된다
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
.png?id=363c92ee-3042-4b80-a092-321666dc5f8c&table=block&spaceId=83c75a39-3aba-4ba4-a792-7aefe4b07895&width=1240&userId=&cache=v2)
Transient : 아직 영속성 컨텍스트에 저장되지 않았기 때문에 JPA의 관리를 받지 않습니다. (객체 생성 new, 값 세팅만 했을 때)
debugging 조회 시 entitiesByKey=null
JPA가 관리하지 못해서 해당 데이터 변경해도 변경 감지 X
Managed : em.persist(memo); 비영속 Entity를 EntityManager를 통해 영속성 컨텍스트에 저장하여 관리되고 있는 상태로 만듭니다.
Detached (= 준영속 상태) : 영속성 컨텍스트에 저장되어 관리되다가 분리된 상태를 의미합니다.
-em.detach(memo); 특정 Entity만 준영속 상태로 전환, 영속성 컨텍스트의 기능 사용 불가
-em.clear(); : 영속성 컨텍스트를 완전히 초기화, 틀은 유지되어 계속해서 이용 가능
-em.close(); : 영속성 컨텍스트를 종료, 이후 사용 불가
Merge : em.merge(memo); 전달받은 Entity를 사용하여 새로운 영속 상태의 Entity를 반환
준영속 상태에서 다시 영속 상태로 변경 가능, 비영속 상태도 파라미터로 받을 수 있다.
- 파라미터로 전달된 Entity의 식별자 값으로 영속성 컨텍스트를 조회
- 해당 Entity가 영속성 컨텍스트에 없다면?
- DB에서 새롭게 조회합니다.
- 조회한 Entity를 영속성 컨텍스트에 저장합니다.
- 전달 받은 Entity의 값을 사용하여 병합합니다.
- Update SQL이 수행됩니다. (수정)
- 만약 DB에서도 없다면 ?
- 새롭게 생성한 Entity를 영속성 컨텍스트에 저장합니다.
즉, 해당 entity는 계속 비영속 상태, 새로운 entity만 영속- Insert SQL이 수행됩니다. (저장)
.
Entity Transaction을 따로 부여할 필요 없이 Method나 Class에 걸어주면 된다
em.persist(객체); 사용이 되지 않아 DB에 저장되지 않는다.@Transactional(readOnly = true)
@Transactional 을 걸어서 편집 가능@PersistenceContext

메소드 뿐만 아니라 Service, Repsitory까지 제어할 수 있도록 트랜잭션 전파 기능을 제공하고 있다

@SpringBootTest
public class TransactionTest {
@PersistenceContext
EntityManager em;
@Autowired
MemoRepository memoRepository;
@Test
@Transactional
@Rollback(value = false) // 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
@DisplayName("메모 생성 성공")
void test1() {
Memo memo = new Memo();
memo.setUsername("Robbert");
memo.setContents("@Transactional 테스트 중!");
em.persist(memo); // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}
@Test
@Transactional
@Rollback(value = false)
@DisplayName("트랜잭션 전파 테스트")
void test3() {
memoRepository.createMemo(em);
System.out.println("테스트 test3 메서드 종료");
}
}

public interface MemoRepository extends JpaRepository<Memo, Long> {
}
제공된 interface의 클래스들을 사용하여 원하는 값을 얻을 수 있다.
대신 return 타입을 잘 확인해야 한다.
public MemoResponseDto createMemo(MemoRequestDto requestDto) {
// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// DB 저장 > 이 메서드에 @Transactional 적용되어 있다.
Memo saveMemo = memoRepository.save(memo);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(saveMemo);
return memoResponseDto;
}
1) findAll
public List<MemoResponseDto> getMemos() {
// DB 조회
return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
}
2) findById
private Memo findMemo(Long id) {
return memoRepository.findById(id).orElseThrow(() ->
new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
);
}
find 메서드의 반환 타입은 Optional이다.
처리 방안은
1) Optinal<Entity타입> 을 return 타입으로 받은 뒤 null에 대한 사항 따로 체크
2) orElseThrow를 사용하여 return 값이 null일 경우 예외를 던지도록 처리
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class) // 있어야 자동 추가
public abstract class Timestamped {
// 이 자체를 객체로 사용할 일은 없으니까 abstract로 선언
@CreatedDate
@Column(updatable = false) // 최초 시간만 저장됨. 수정 불가
@Temporal(TemporalType.TIMESTAMP) // 연월일시분초 저장
private LocalDateTime createdAt; // 시간값 저장
@LastModifiedDate // 변경 시 적용됨
@Column
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime modifiedAt; // 변경시간이 변경될 때마다 반영됨
}
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@CreatedDate
updatable = false 옵션을 추가합니다.@LastModifiedDate
@Temporal
메모장 프로젝트에 JPA Auditing 적용
1. Timestamped 생성
2. @EnableJpaAuditing 애너테이션을 추가합니다.
3. 적용하고자하는 Entity 클래스에서 Timestamped를 상속받습니다.
이 부분 많이 사용한다고 하니까 추후 강의 다시 들어보자
package com.sparta.memo.repository;
import com.sparta.memo.entity.Memo;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface MemoRepository extends JpaRepository<Memo, Long> {
List<Memo> findAllByOrderByModifiedAtDesc(); //1
List<Memo> findAllByUsername(String username);//2
}
1: Memo 테이블에서 ModifiedAt 즉, 수정 시간을 기준으로 전체 데이터를 내림차순으로 가져오는 SQL을 실행하는 메서드
2: ByUsername 에 값을 전달해줘야하기 때문에 파라미터에 해당 값의 타입과 변수명을 선언해야 함
즉, Query Methods 는 메서드의 파라미터를 통해 SQL에 필요한 값을 동적으로 받아 처리할 수 있습니다.
==== 강의 자료에 debugging 상세하게 나와 있음 ====