공부를 기록하면서 만들었지만, 1년간 하루에 3포스팅씩 만들니까, 너무많은 공부를 하고있어서, 정리가 안됩니다.
더이상 복습하기에는 시간이 너무 아까워서 스프링 관련한 내용을 올립니다.
예제:
java
코드 복사
// 비영속 상태의 엔티티 객체 생성
Entity entity = new Entity();
entity.setName("Example");
// 이 상태에서 데이터베이스에 저장되지 않음
2. 연속(Persistent)
설명: 엔티티가 EntityManager에 의해 관리되고 있는 상태입니다. 이 상태에서는 데이터베이스에 저장되거나, 데이터베이스에서 조회된 객체이며, 이후 변경된 사항이 자동으로 데이터베이스에 반영됩니다.
예제:
java
코드 복사
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
Entity entity = new Entity();
entity.setName("Example");
// 연속 상태로 전환
em.persist(entity); // 데이터베이스에 저장됨
em.getTransaction().commit(); // 트랜잭션 커밋
예제:
java
코드 복사
em.getTransaction().begin();
Entity entity = em.find(Entity.class, 1L);
em.getTransaction().commit();
// 트랜잭션 종료 후, EntityManager가 닫힘
em.close();
// entity는 중영속 상태가 됨
entity.setName("New Name");
// 하지만 변경사항은 자동으로 반영되지 않음
예제:
java
코드 복사
em.getTransaction().begin();
Entity entity = em.find(Entity.class, 1L);
// 삭제 상태로 전환
em.remove(entity); // 데이터베이스에서 삭제됨
em.getTransaction().commit();
요약
비영속(Transient): EntityManager에 관리되지 않고, 데이터베이스에 저장되지 않은 상태.
연속(Persistent): EntityManager에 의해 관리되며, 데이터베이스에 저장된 상태.
중영속(Detached): 데이터베이스에는 저장되어 있지만, EntityManager에 의해 더 이상 관리되지 않는 상태.
삭제(Removed): 데이터베이스에서 삭제될 예정이거나 이미 삭제된 상태.
각 상태는 JPA가 엔티티 객체를 어떻게 다루는지에 대한 중요한 개념이므로, JPA를 사용한 애플리케이션 개발에서 중요한 역할을 합니다.
기본 개념
엔티티(Entity) 클래스:
데이터베이스의 테이블과 매핑되는 자바 클래스입니다. 각 엔티티 클래스는 보통 데이터베이스의 한 테이블에 대응됩니다.
엔티티 클래스는 @Entity 어노테이션을 통해 선언됩니다.
컬럼(Column):
엔티티 클래스의 필드는 데이터베이스 테이블의 컬럼과 매핑됩니다.
필드와 컬럼 간의 매핑은 @Column 어노테이션을 사용하여 설정할 수 있습니다.
주요 어노테이션과 매핑 방법
1. @Entity
설명: 클래스를 엔티티로 선언하며, 해당 클래스는 데이터베이스의 테이블에 매핑됩니다.
예제:
java
코드 복사
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
}
예제:
java
코드 복사
@Entity
@Table(name = "users") // "users" 테이블과 매핑
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
}
속성들:
name: 매핑할 테이블의 컬럼 이름을 지정합니다.
nullable: 컬럼이 NULL 값을 허용할지 여부를 설정합니다.
unique: 컬럼의 값이 고유해야 하는지를 지정합니다.
length: 문자열 타입의 컬럼에 대해 최대 길이를 설정합니다.
columnDefinition: SQL의 컬럼 정의를 직접 지정할 수 있습니다.
예제:
java
코드 복사
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false, unique = true)
private String name;
@Column(name = "user_email", nullable = false, length = 100)
private String email;
}
java
코드 복사
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
예제:
java
코드 복사
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
예제:
java
코드 복사
public enum Status {
ACTIVE, INACTIVE
}
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
private Status status;
}
예제: 간단한 엔티티 매핑
다음은 User 엔티티가 데이터베이스의 users 테이블에 매핑되는 예입니다.
java
코드 복사
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false, unique = true)
private String name;
@Column(name = "user_email", nullable = false, length = 100)
private String email;
@Enumerated(EnumType.STRING)
@Column(name = "user_status")
private Status status;
// Getters and Setters
}
위 예제에서 User 클래스는 users라는 데이터베이스 테이블과 매핑됩니다. name, email, status 필드는 각각 username, user_email, user_status 컬럼과 매핑되며, 각 컬럼의 속성은 @Column 어노테이션을 통해 정의되었습니다.
매핑 방법:
@OneToMany 어노테이션을 사용하여 설정합니다.
@JoinColumn을 사용하여 외래 키를 지정할 수 있습니다.
기본적으로, 1대N 관계는 연관된 엔티티를 List, Set 등의 컬렉션으로 표현합니다.
예제:
java
코드 복사
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "user")
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String product;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
설명: 위 예제에서 User 엔티티는 Order 엔티티와 1대N 관계를 가집니다. 즉, 한 명의 사용자는 여러 개의 주문을 가질 수 있습니다. Order 엔티티의 user 필드는 @ManyToOne 관계로, 주문이 한 명의 사용자에 종속됨을 나타냅니다.
. N대1 관계 (Many-to-One)
설명: 여러 개의 엔티티가 하나의 엔티티에 연관될 때 사용됩니다. 예를 들어, 여러 주문이 하나의 사용자에 속할 수 있는 관계입니다.
매핑 방법:
@ManyToOne 어노테이션을 사용하여 설정합니다.
일반적으로 외래 키가 있는 쪽에 @ManyToOne을 사용합니다.
예제:
java
코드 복사
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String product;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
설명: 이 예제에서는 Order 엔티티가 User 엔티티와 N대1 관계를 가집니다. 즉, 여러 개의 주문이 하나의 사용자에 연관될 수 있습니다. @JoinColumn을 사용하여 user_id라는 외래 키 컬럼을 지정합니다.
java
코드 복사
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "student")
private List<StudentCourse> studentCourses = new ArrayList<>();
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@OneToMany(mappedBy = "course")
private List<StudentCourse> studentCourses = new ArrayList<>();
}
@Entity
public class StudentCourse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
}
N+1 문제 해결 방법
Fetch Join 사용:
@Query나 JPQL을 사용할 때 fetch join을 통해 연관된 엔티티를 한 번에 가져오도록 합니다.
java
코드 복사
@Query("SELECT s FROM Student s JOIN FETCH s.studentCourses sc JOIN FETCH sc.course WHERE s.id = :id")
Student findStudentWithCourses(@Param("id") Long id);
Entity Graph 사용:
@EntityGraph를 사용하여 연관된 엔티티를 함께 가져올 수 있습니다.
java
코드 복사
@EntityGraph(attributePaths = {"studentCourses", "studentCourses.course"})
@Query("SELECT s FROM Student s WHERE s.id = :id")
Student findStudentWithCourses(@Param("id") Long id);
Batch Size 설정:
hibernate.default_batch_fetch_size를 설정하여 N+1 문제를 완화할 수 있습니다.
properties
코드 복사
spring.jpa.properties.hibernate.default_batch_fetch_size=10
JSON Ignore로 순환 참조 방지:
@JsonIgnore를 사용하여 양방향 관계에서 순환 참조(Circular Reference)를 방지할 수 있습니다.
java
코드 복사
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "student")
@JsonIgnore
private List<StudentCourse> studentCourses = new ArrayList<>();
}
@Entity
public class StudentCourse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
}
설명:
를 사용하여 JSON 직렬화 중에 특정 필드를 무시하도록 할 수 있습니다. 이는 엔티티 간의 순환 참조를 방지하는 데 유용합니다.
이렇게 하면 N+1 문제는 해결되지 않지만, JSON 직렬화 과정에서의 무한 루프는 방지할 수 있습니다.
단일 테이블 전략은 상속 구조에 있는 모든 엔티티를 하나의 테이블에 매핑하는 방법입니다. 즉, 부모 클래스와 자식 클래스의 모든 필드를 포함하는 하나의 테이블이 생성됩니다.
각 레코드는 어느 자식 클래스에 해당하는지 식별하기 위해 구분 컬럼(Discriminator Column)이 사용됩니다.
장점:
성능이 빠릅니다. 조회 시 하나의 테이블만 참조하면 되므로, 조인 연산이 필요하지 않습니다.
구조가 단순합니다. 모든 데이터를 하나의 테이블에 저장하므로 관리하기 쉽습니다.
단점:
불필요한 데이터가 많아질 수 있습니다. 자식 클래스에 따라 사용되지 않는 컬럼들이 존재하게 됩니다.
테이블의 컬럼 수가 많아질 수 있습니다.
예제:
java
코드 복사
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Vehicle {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
@Entity
public class Car extends Vehicle {
private int numberOfDoors;
}
@Entity
public class Motorcycle extends Vehicle {
private boolean hasSidecar;
}
조인 전략은 부모 클래스에 해당하는 테이블과 각 자식 클래스에 해당하는 테이블을 따로 생성하고, 이들을 JOIN을 통해 결합하여 데이터를 조회하는 방법입니다.
부모 클래스의 공통 속성들은 부모 테이블에 저장되고, 자식 클래스의 속성들은 자식 클래스별로 각각의 테이블에 저장됩니다.
장점:
정규화된 테이블 구조를 가집니다. 각 테이블은 해당 엔티티에 필요한 속성만을 가집니다.
자식 클래스가 많아져도 테이블 구조가 복잡해지지 않습니다.
단점:
조인 연산이 필요하므로 조회 성능이 저하될 수 있습니다.
설계와 관리가 상대적으로 복잡합니다.
예제:
java
코드 복사
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Vehicle {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
@Entity
public class Car extends Vehicle {
private int numberOfDoors;
}
@Entity
public class Motorcycle extends Vehicle {
private boolean hasSidecar;
}
구체 클래스 전략은 각 자식 클래스가 독립적인 테이블에 매핑되고, 부모 클래스에 해당하는 테이블이 없습니다.
자식 클래스는 부모 클래스의 모든 속성을 포함한 테이블로 매핑되며, 각 자식 클래스는 독립적인 테이블에 저장됩니다.
예제:
java
코드 복사
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Vehicle {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
@Entity
public class Car extends Vehicle {
private int numberOfDoors;
}
@Entity
public class Motorcycle extends Vehicle {
private boolean hasSidecar;
}
설명:
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)로 구체 클래스 전략을 사용함을 나타냅니다.
부모 클래스 Vehicle에 대한 테이블은 생성되지 않으며, 자식 클래스 Car와 Motorcycle 각각에 대해 독립적인 테이블이 생성됩니다.
주요 Cascade 유형
JPA에서는 다음과 같은 케스케이드 옵션을 제공합니다:
CascadeType.PERSIST: 부모 엔티티가 저장될 때 연관된 자식 엔티티도 함께 저장됩니다.
CascadeType.MERGE: 부모 엔티티가 병합될 때(업데이트될 때) 연관된 자식 엔티티도 함께 병합됩니다.
CascadeType.REMOVE: 부모 엔티티가 삭제될 때 연관된 자식 엔티티도 함께 삭제됩니다.
CascadeType.REFRESH: 부모 엔티티가 새로고침될 때 자식 엔티티도 함께 새로고침됩니다.
CascadeType.DETACH: 부모 엔티티가 영속성 컨텍스트에서 분리될 때 연관된 자식 엔티티도 함께 분리됩니다.
CascadeType.ALL: 모든 케스케이드 작업을 포함합니다. (PERSIST, MERGE, REMOVE, REFRESH, DETACH)
Cascade의 사용 예제
1. CascadeType.PERSIST
java
코드 복사
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<>();
// Getters and Setters
}
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Getters and Setters
}
java
코드 복사
Parent parent = new Parent();
Child child1 = new Child();
Child child2 = new Child();
parent.getChildren().add(child1);
parent.getChildren().add(child2);
entityManager.persist(parent);
임베디드 타입의 기본 개념
임베디드 타입을 정의하면, JPA는 해당 타입의 모든 필드를 엔티티의 테이블에 포함시킵니다. 이는 @Embeddable 애노테이션을 사용하여 정의된 클래스를 엔티티의 필드로 사용함으로써 이루어집니다.
주요 애노테이션
@Embeddable
임베디드 타입으로 사용할 클래스를 정의할 때 사용합니다.
이 클래스는 데이터베이스 테이블에 매핑되지 않고, 다른 엔티티 클래스의 필드로서 매핑됩니다.
@Embedded
임베디드 타입을 엔티티의 필드로 포함할 때 사용합니다.
@AttributeOverrides 및 @AttributeOverride
임베디드 타입의 속성에 대해 추가적인 매핑 구성을 할 수 있습니다.
임베디드 타입 사용 예제
1. 임베디드 타입 정의
java
코드 복사
@Embeddable
public class Address {
private String street;
private String city;
private String zipCode;
// Getters and Setters
}
@Embeddable을 사용하여 Address 클래스를 임베디드 타입으로 정의합니다. 이 클래스는 자체적으로 데이터베이스 테이블에 매핑되지 않고, 다른 엔티티 클래스의 필드로 사용됩니다.
2. 임베디드 타입 사용
java
코드 복사
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
private Address address;
// Getters and Setters
}
@Embedded를 사용하여 User 엔티티에 Address 임베디드 타입을 포함시킵니다.
3. 임베디드 타입의 속성 오버라이드
때때로 임베디드 타입의 속성을 엔티티의 테이블에서 다르게 매핑하고 싶을 때가 있습니다. 이럴 때는 @AttributeOverrides 및 @AttributeOverride를 사용할 수 있습니다.
java
코드 복사
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "home_street")),
@AttributeOverride(name = "zipCode", column = @Column(name = "postal_code"))
})
private Address address;
// Getters and Setters
}
위 예제에서 Address 임베디드 타입의 street과 zipCode 필드를 각각 home_street과 postal_code로 매핑하였습니다.
임베디드 타입의 장점
재사용성: 동일한 임베디드 타입을 여러 엔티티에서 재사용할 수 있습니다.
코드의 재사용과 관리 용이: 공통적으로 사용되는 필드를 그룹화하여 관리할 수 있습니다.
테이블 구조 단순화: 복잡한 엔티티 구조를 보다 간단하게 정의할 수 있습니다.
임베디드 타입의 주의사항
변경 감지: 임베디드 타입의 속성 값을 수정하면, JPA는 전체 객체를 변경된 것으로 간주하고 관리합니다. 따라서 임베디드 타입의 필드가 많거나 복잡한 경우 성능에 영향을 줄 수 있습니다.
비즈니스 로직: 임베디드 타입은 비즈니스 로직을 포함하지 않도록 하는 것이 좋습니다. 비즈니스 로직은 엔티티 자체에 포함시켜야 합니다.
예제:
java
코드 복사
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.List;
public class UserService {
private final EntityManager entityManager;
public UserService(EntityManager entityManager) {
this.entityManager = entityManager;
}
public List<User> getUsers(int pageNumber, int pageSize) {
TypedQuery<User> query = entityManager.createQuery("SELECT u FROM User u", User.class);
query.setFirstResult((pageNumber - 1) * pageSize); // 시작 위치
query.setMaxResults(pageSize); // 페이지 사이즈
return query.getResultList();
}
}
설명:
setFirstResult 메서드는 결과 리스트에서 조회를 시작할 첫 번째 위치를 설정합니다. (pageNumber - 1) * pageSize를 통해 시작 위치를 계산합니다.
setMaxResults 메서드는 한 페이지에서 반환할 최대 결과 수를 설정합니다.
2. 서브쿼리 (Subquery)
서브쿼리는 쿼리 내에서 또 다른 쿼리를 사용하는 방법입니다. JPA의 JPQL에서는 서브쿼리를 SELECT, WHERE, HAVING 절에서 사용할 수 있습니다.
예제:
java
코드 복사
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.List;
public class UserService {
private final EntityManager entityManager;
public UserService(EntityManager entityManager) {
this.entityManager = entityManager;
}
public List<User> getUsersWithMostPosts() {
String jpql = "SELECT u FROM User u WHERE (SELECT COUNT(p) FROM Post p WHERE p.author = u) > 5";
TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
return query.getResultList();
}
}
설명:
위 JPQL 쿼리는 User 엔티티에서, 게시글 수가 5개를 초과하는 사용자만 조회합니다. 서브쿼리는 WHERE 절에서 사용되어 각 사용자에 대해 게시글 수를 계산합니다.
3. JPQL 함수
JPQL에서는 여러 유용한 함수들을 제공하여 쿼리 작성 시 편리함을 더합니다. 주요 JPQL 함수는 다음과 같습니다:
집계 함수 (Aggregation Functions)
COUNT(): 행의 수를 계산합니다.
SUM(): 합계를 계산합니다.
AVG(): 평균을 계산합니다.
MAX(): 최대 값을 찾습니다.
MIN(): 최소 값을 찾습니다.
예제:
java
코드 복사
public Long getUserCount() {
String jpql = "SELECT COUNT(u) FROM User u";
TypedQuery<Long> query = entityManager.createQuery(jpql, Long.class);
return query.getSingleResult();
}
문자열 함수 (String Functions)
CONCAT(): 문자열을 결합합니다.
SUBSTRING(): 문자열의 일부를 추출합니다.
LENGTH(): 문자열의 길이를 계산합니다.
TRIM(): 문자열의 공백을 제거합니다.
LOWER(), UPPER(): 문자열을 소문자 또는 대문자로 변환합니다.
예제:
java
코드 복사
public List<User> findUsersWithNameLike(String name) {
String jpql = "SELECT u FROM User u WHERE LOWER(u.name) LIKE LOWER(:name)";
TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
query.setParameter("name", "%" + name.toLowerCase() + "%");
return query.getResultList();
}
날짜 및 시간 함수 (Date and Time Functions)
CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP: 현재 날짜, 시간, 타임스탬프를 반환합니다.
DATE(), TIME(), TIMESTAMP(): 날짜, 시간, 타임스탬프를 추출합니다.
예제:
java
코드 복사
public List<User> findUsersCreatedAfter(Date date) {
String jpql = "SELECT u FROM User u WHERE u.creationDate > :date";
TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
query.setParameter("date", date);
return query.getResultList();
}
수학 함수 (Mathematical Functions)
ABS(): 절댓값을 반환합니다.
MOD(): 나머지 값을 반환합니다.
SQRT(): 제곱근을 계산합니다.
예제:
java
코드 복사
public List<Post> findPostsWithTitleLengthGreaterThan(int length) {
String jpql = "SELECT p FROM Post p WHERE LENGTH(p.title) > :length";
TypedQuery<Post> query = entityManager.createQuery(jpql, Post.class);
query.setParameter("length", length);
return query.getResultList();
}
요약
페이징: setFirstResult와 setMaxResults를 사용하여 데이터를 페이지 단위로 조회할 수 있습니다.
서브쿼리: JPQL의 SELECT, WHERE, HAVING 절에서 서브쿼리를 사용하여 복잡한 조회를 구현할 수 있습니다.
JPQL 함수: 집계 함수, 문자열 함수, 날짜/시간 함수, 수학 함수 등을 사용하여 쿼리를 보다 효율적으로 작성할 수 있습니다.
이러한 기능들을 적절히 활용하면 JPA를 사용하여 데이터베이스와의 상호작용을 보다 효율적이고 유연하게 수행할 수 있습니다.
Named Query
Named Query는 JPQL 쿼리를 사전에 정의하고 이를 이름으로 참조하여 사용하는 방법입니다. 이는 코드의 가독성을 높이고, 쿼리를 재사용 가능하게 만들어 줍니다.
Named Query 정의 및 사용
엔티티 클래스에 Named Query 정의
@NamedQuery 또는 @NamedQueries 애노테이션을 사용하여 엔티티 클래스에 Named Query를 정의합니다.
java
코드 복사
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQuery;
@Entity
@NamedQuery(name = "User.findByName", query = "SELECT u FROM User u WHERE u.name = :name")
public class User {
@Id
private Long id;
private String name;
// Getters and Setters
}
Named Query 호출
EntityManager를 통해 정의된 Named Query를 호출할 수 있습니다.
java
코드 복사
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.List;
public class UserService {
private final EntityManager entityManager;
public UserService(EntityManager entityManager) {
this.entityManager = entityManager;
}
public List<User> findUsersByName(String name) {
TypedQuery<User> query = entityManager.createNamedQuery("User.findByName", User.class);
query.setParameter("name", name);
return query.getResultList();
}
}
벌크 연산 (Bulk Operations)
벌크 연산은 대량의 데이터를 효율적으로 처리할 때 사용하는 쿼리 작업입니다. JPQL에서는 UPDATE, DELETE 쿼리를 사용하여 벌크 연산을 수행할 수 있으며, 이를 통해 성능을 개선할 수 있습니다.
벌크 연산 예제
벌크 업데이트
벌크 업데이트는 데이터베이스에 저장된 여러 레코드를 동시에 업데이트하는 작업입니다. EntityManager의 createQuery를 사용하여 JPQL을 실행합니다.
java
코드 복사
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
public class UserService {
private final EntityManager entityManager;
public UserService(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Transactional
public int updateUserStatus(String newStatus) {
String jpql = "UPDATE User u SET u.status = :status WHERE u.status = :oldStatus";
return entityManager.createQuery(jpql)
.setParameter("status", newStatus)
.setParameter("oldStatus", "inactive")
.executeUpdate();
}
}
설명:
executeUpdate() 메서드는 업데이트 작업을 수행하고, 영향을 받은 레코드의 수를 반환합니다.
벌크 삭제
벌크 삭제는 데이터베이스의 여러 레코드를 동시에 삭제하는 작업입니다.
java
코드 복사
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
public class UserService {
private final EntityManager entityManager;
public UserService(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Transactional
public int deleteInactiveUsers() {
String jpql = "DELETE FROM User u WHERE u.status = :status";
return entityManager.createQuery(jpql)
.setParameter("status", "inactive")
.executeUpdate();
}
}
설명:
DELETE 쿼리는 삭제할 레코드 수를 반환합니다. 벌크 삭제는 성능을 높이지만, 엔티티의 상태나 영속성 컨텍스트에 영향을 줄 수 있습니다.
Named Query와 벌크 연산 결합
Named Query와 벌크 연산을 결합하여 사용하면 쿼리를 재사용하면서 대량의 데이터에 대해 효율적인 작업을 수행할 수 있습니다.
Named Query 정의 (벌크 연산 포함)
java
코드 복사
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQuery;
@Entity
@NamedQuery(name = "User.updateStatus", query = "UPDATE User u SET u.status = :status WHERE u.status = :oldStatus")
public class User {
@Id
private Long id;
private String status;
// Getters and Setters
}
Named Query 호출
java
코드 복사
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
public class UserService {
private final EntityManager entityManager;
public UserService(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Transactional
public int updateUserStatus(String newStatus, String oldStatus) {
return entityManager.createNamedQuery("User.updateStatus")
.setParameter("status", newStatus)
.setParameter("oldStatus", oldStatus)
.executeUpdate();
}
}
주의사항
영속성 컨텍스트: 벌크 연산은 JPA의 영속성 컨텍스트와 직접 상호작용하지 않기 때문에, 영속성 컨텍스트에 있는 엔티티는 자동으로 동기화되지 않습니다. 따라서 벌크 연산 후에는 영속성 컨텍스트를 수동으로 갱신하거나 초기화할 필요가 있을 수 있습니다.
트랜잭션 관리: 벌크 연산은 데이터베이스의 상태를 변경하므로, 트랜잭션 내에서 수행해야 합니다. @Transactional 애노테이션을 사용하여 트랜잭션을 관리합니다.
JPQL 쿼리 정의
@Query 애노테이션을 사용하여 직접 JPQL 쿼리를 정의할 수 있습니다.
예제:
java
코드 복사
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// JPQL 쿼리로 사용자 이름으로 검색
@Query("SELECT u FROM User u WHERE u.name = :name")
List<User> findByName(@Param("name") String name);
// JPQL 쿼리로 사용자 상태 업데이트
@Modifying
@Transactional
@Query("UPDATE User u SET u.status = :status WHERE u.status = :oldStatus")
int updateUserStatus(@Param("status") String newStatus, @Param("oldStatus") String oldStatus);
// JPQL 쿼리로 사용자 삭제
@Modifying
@Transactional
@Query("DELETE FROM User u WHERE u.status = :status")
int deleteUsersByStatus(@Param("status") String status);
}
설명:
@Query 애노테이션으로 JPQL 쿼리를 정의합니다.
@Param 애노테이션으로 쿼리 파라미터를 바인딩합니다.
@Modifying 애노테이션은 데이터베이스의 상태를 변경하는 쿼리에 사용합니다.
@Transactional 애노테이션은 트랜잭션을 관리합니다.
3. 엔티티 그래프 사용하기
EntityGraph를 사용하여 엔티티의 연관 관계를 미리 로딩할 수 있습니다. JPA Repository 인터페이스에서 이를 사용하려면 EntityManager를 통해 접근해야 합니다.
예제:
java
코드 복사
import javax.persistence.EntityManager;
import javax.persistence.EntityGraph;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<User> findUsersWithDetails() {
EntityGraph<User> graph = entityManager.createEntityGraph(User.class);
graph.addAttributeNodes("profile"); // 연관된 엔티티를 미리 로딩하도록 설정
TypedQuery<User> query = entityManager.createQuery("SELECT u FROM User u", User.class);
query.setHint("javax.persistence.fetchgraph", graph); // 그래프를 쿼리 힌트로 사용
return query.getResultList();
}
}
설명:
EntityManager를 사용하여 EntityGraph를 정의하고, 이를 쿼리 힌트로 사용하여 연관 관계를 미리 로딩합니다.
UserRepositoryCustom는 사용자 정의 메서드를 정의할 수 있는 인터페이스입니다.
4. 데이터베이스 락 (Database Lock)
데이터베이스 락을 사용하여 동시성 문제를 처리할 수 있습니다. @Lock 애노테이션을 사용하여 메서드 수준에서 락을 설정할 수 있습니다.
예제:
java
코드 복사
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.data.jpa.repository.LockModeType;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 비관계 락을 사용하여 사용자 찾기
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT u FROM User u WHERE u.id = :id")
User findUserByIdForUpdate(@Param("id") Long id);
}
설명:
@Lock 애노테이션을 사용하여 비관계 락을 설정합니다.
LockModeType.PESSIMISTIC_WRITE는 쓰기 락을 설정하여 다른 트랜잭션에서 동시에 수정하지 못하도록 합니다.