필드의 리스트 순서 보장 문제

JPA를 사용하고 있을 때 DB가 리스트의 순서를 계속 유지시켜 줄 것이라고 믿으면 안된다. @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class Room { @OneToMany(mappedBy = "room") private final List<User> users = new ArrayList<>(); ... } 이를 해결하기 위해, 유저가 방에 입장하는 시각을 따로 저장하여 그 순서를 보장하는 형태로 구현을 했다.

2024-09-15 · 1 min · 49 words

지연 로딩

즉시 로딩과 지연 로딩 @Entity public class Member { @ManyToOne @JoinColumn(name = "TEAM_ID") private Team teams; // ... } 즉시 로딩(@ManyToOne(fetch = FetchType.EAGER)): Member라는 엔티티를 조회할 때 연관된 엔티티인 Team 도 같이 조회된다. 지연 로딩(@ManyToOne(fetch = FetchType.LAZY)): Member라는 엔티티를 조회할 때 Team을 조회하지 않다가, 실제로 사용할 떄 조회한다. ex) member.getTeam() 지연 로딩의 동작 방법 만약, 위 예시에서 team이 지연로딩()을 사용 중이라면 member.getTeam() 을 호출했을 때의 Team은 프록시 객체다. 그리고 이 프록시 객체는 실제 사용될 때 까지 로딩을 미룬다....

2024-09-15 · 3 min · 572 words

영속성 관리

https://junroot.github.io/programming/JPA-시작하기/ 에서 어느 정도 영속성 컨텍스트와 엔티티의 생명주기를 간단하게 알아봤다. 이 영속성 컨텍스트의 특징을 좀 더 자세히 알아보도록 하자. 플러시 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다. 이 때 영속성 컨텍스트에 보관된 엔티티를 지우는 것은 아니다. 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화 하는 것이다. 플러시 과정 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교하여 수정된 엔티티를 찾는다. 수정된 엔티티는 수정 쿼리를 만들어서 쓰기 지연 SQL 저장소에 등록한다. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다....

2024-09-15 · 2 min · 265 words

엔티티에 일급 컬렉션 도입하기

@Embeddable public class RoomUsers { @OneToMany(mappedBy = "room") private final List<User> users = new ArrayList<>(); ... } @Entity public class Room { @Embedded private RoomUsers users; ... } @Embeddable 과 @Embedded 어노테이션을 붙이면 엔티티에 일급 컬렉션을 사용할 수 있다.

2024-09-15 · 1 min · 39 words

엔티티 생성 시각 저장하기

Application 클래스에 @EnableJpaAuditing 어노테이션 추가 저장하고자하는 엔티티에 @EntityListeners(AuditingEntityListener.class) 추가 참고 자료 https://stackoverflow.com/questions/40370709/createddate-annotation-does-not-work-with-mysql

2024-09-15 · 1 min · 12 words

배포마다 DB 테이블이 삭제되지 않도록 구성하기

properties 파일에 아래의 설정을 추가하여 해결했다. spring.jpa.hibernate.ddl-auto=validate 문제점: 새로운 not null 컬럼이 추가되었을 때 기존 row들은 어떻게 처리하는 것이 좋을까? 예를들어, 서비스가 확장되면서 User에 Age컬럼이 추가되었을 때, Age가 not null 이라면 기존의 사용자 데이터는 어떻게 처리하는 것이 좋을까? → 구글링 결과, 방법은 1. not null을 포기하거나 2. default value를 설정하거나 두 가지 방법 밖에없다. 상태로 컬럼을 추가하는 수밖에 없다. (어떻게 보면 당연하다 우리가 사용자의 나이를 추측할 순 없으니까) 1. not null 포기 기존 데이터는 null로 두고 이후에 생성되는 데이터에서는 null이 들어오지 못하도록 DTO에서 null 값체크를 하도록 구현한다....

2024-09-15 · 2 min · 403 words

단방향 OneToMany 연관관계 매핑의 문제점

단방향 연관관계 매핑을 사용하면 insert 이후에, FK를 저장하기 위해 update 쿼리를 한 번더 실행하는 문제가 있다. @Entity public class Menu { //... @OneToMany(cascade = CascadeType.PERSIST) @JoinColumn(name = "menu_id") private final List<MenuProduct> menuProducts; //... } @Entity public class MenuProduct { //... } 위 상황의 경우 MenuProduct 엔티티의 menu_id라는 FK가 저장된다. 하지만 Menu 를 저장할 때, MenuProduct에 FK 값을 지정하기 위해서 update 쿼리가 한 번더 실행되기 때문에 성능상 좋지않다. 단방향 OneToMany를 사용하는 것 보다는 양방향 ManyToOne을 사용하는 것이 좋다....

2024-09-15 · 1 min · 80 words

논리적 삭제를 하고 있는 Entity가 지연 로딩

문제 상황 @EntityListeners(AuditingEntityListener.class) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class Room { @OneToMany(mappedBy = "room") private final List<Session> sessions = new ArrayList<>(); //... } @EntityListeners(AuditingEntityListener.class) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class Session { @Column(nullable = false) private boolean deleted = false; //... public void delete() { if (user.isLinkedSession(this)) { user.unLinkSession(this); } if (room.containsSession(this)) { room.exitSession(this); } deleted = true; } } Session은 논리적 삭제를 사용하고 있는데, Room의 sessions필드가 지연로딩 될 때, 논리적 삭제를 사용하고 있는 것을 모르기 때문에 모든 session을 가지고 오는 문제가 있었다....

2024-09-15 · 1 min · 148 words

날짜 필드

@Temporal(TemporalType.DATE) private Date date; // date date 생성 @Temporal(TemporalType.TIME) private Date time; // time time 생성 @Temporal(TemporalType.TIMESTAMP) private Date timestamp; // timestamp timestamp 생성 @Temporal 를 생략할 경우 TIMESTAMP 로 컬럼을 만든다. Java 8의 경우에서 나온 LocalDate, LocalTime, LocalDateTime 등은 별도의 어노테이션을 붙일 필요가 없다. 참고 자료 https://www.baeldung.com/hibernate-date-time

2024-09-15 · 1 min · 48 words

Spring JPA로 논리적 삭제 사용하기

이전에 사용했던 @Where 어노테이션과 @SQLDelete 어노테이션을 사용하면 된다. @Entity @Table(name = "table_product") @SQLDelete(sql = "UPDATE table_product SET deleted = true WHERE id=?") @Where(clause = "deleted=false") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private double price; private boolean deleted = Boolean.FALSE; // setter getter method } @SQLDelete 도 실제로 DB에 쿼리를 보낼 때 적용하게 되므로, 영속성 컨텍스트에 남아 있는 엔티티는 findAll() 같은 메서드로 조회가 되니 유의해야 된다....

2024-09-15 · 1 min · 76 words

Spring data JPA 사용시 @Transactional을 붙이지 않으면 영속성 컨텍스트가 어떻게 동작할까

동작방식을 이해하기 위해서 JpaRepository의 기본 구현체를 확인해볼 필요가 있었다. 기본 구현체인 SimpleJpaRepository 코드를 확인해보니, class 레벨에 @Transactional(readOnly = true)가 설정되어 있고, 쓰기 메서드에 각각 @Transactional이 명시되어 있었다. 따라서, 기본적으로 각 메서드 호출마다 영속성 컨텍스트가 별개로 만들어져서 엔티티가 관리된다. 참고 자료 https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/support/SimpleJpaRepository.html https://www.baeldung.com/jpa-hibernate-persistence-context

2024-09-15 · 1 min · 42 words

setter 메서드로 변경 추적이 되지 않는 문제 해결

Room room = Room.builder() .id(1L) .game(game) .tags(tags) .build(); room.join(user); roomRepository.save(room); 위와 같은 코드가 있는데 roomRepository.getById(1L) 해도 user가 비어있다. @Transactional어노테이션을 붙이니 해결이 됐는데 원인을 모르겠다. 현재 추정: 트랜잭션과 변경 감지 roomRepository.save()를 호출하면 트랜잭션을 기준으로 저장되는 것으로 추정된다. 따라서 위의 코드는 @Transactional 이 없기 때문에 트랜잭션에 모아둔 커밋이 없기 때문에 단순하게 Room Entity 자체만 저장이 되는것같다. (users는 연관관계의 주인이 아니기때문에 저장되지 않는다.) JpaRepository의 코드를 보기로 했다. JpaRepository의 기본 구현체는 SimpleJpaRepository 로 이 부분에 save 메서드가 구현되어 있었다....

2024-09-15 · 1 min · 175 words

Repository를 사용하여 데이터 개수 제한하기

top이나 first를 사용하면된다. 따로 개수를 표기하지 않으면 1개의 데이터만 가져온다. User findFirstByOrderByLastnameAsc(); User findTopByOrderByAgeDesc(); Page<User> queryFirst10ByLastname(String lastname, Pageable pageable); Slice<User> findTop3ByLastname(String lastname, Pageable pageable); List<User> findFirst10ByLastname(String lastname, Sort sort); List<User> findTop10ByLastname(String lastname, Pageable pageable); 참고 자료 https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.limit-query-result

2024-09-15 · 1 min · 37 words

QueryDSL 시작하기

상황에 따라 쿼리문이 동적으로 변하는 기능이 필요했다. 이 기회에 QueryDSL을 사용해서 이를 구현해보고자 한다. 의존성 추가 QueryDSL을 사용하기 위해서는 의존성을 추가할 뿐만의 아니라 약간의 설정이 필요하다. QueryDSL은 프로젝트 내에 있는 Entity들을 스캔하여 JPAAnnotationProcessor 을 통해 Q 클래스를 생성한다. Q 클래스를 통해 엔티티의 필드 값을 통한 쿼리문을 쉽게 작성할 수 있게 해준다. @Entity 같은 JPA 관련 어노테이션을 탐지할 수 있도록 하기위해서 ‘javax.persistence-api’, ‘javax.annotation-api’를 함께 annotationProcessor에 추가해준다. sourceSets 부분은 개발환경에서 Q 클래스에 접근할 수 있도록 하기위해 설정해주는 부분이다....

2024-09-15 · 2 min · 340 words

JpaRepository에서 SQL 작성하기

@Query 어노테이션에 nativeQuery 필드를 true로 설정하면 JPQL이 아닌 SQL 문을 작성할 수 있다. @Query( value = "SELECT * FROM USERS u WHERE u.status = 1", nativeQuery = true) Collection<User> findAllActiveUsersNative(); 참고 자료 https://www.baeldung.com/spring-data-jpa-query

2024-09-15 · 1 min · 33 words

Enum을 JPA로 저장 하기

@Enumerated 구현 자체는 간단하다. 필드에 @Enumerated 어노테이션을 붙여주면 된다. @Entity public class Article { @Id private int id; private String title; @Enumerated(EnumType.ORDINAL) private Status status; } 하지만 속성 값을 어떻게 주냐에 따라 두 가지로 나뉜다. ORDINAL 위 코드 예시와 같이 ORDINAL을 속성으로 주면, Enum.ordinal() 값을 DB에 저장하는 방식이다. enum의 순서가 바뀌면 기존에 DB에 저장되어 있는 값과 충돌하는 문제가 있다. STRING @Enumerated(EnumType.STRING) 로 저장하게되면 Enum.name() 값을 DB에 저장하게 된다. enum의 순서가 바뀌어도 문제가 없다....

2024-09-15 · 2 min · 422 words