CH05 Spring Data JPA

JpaRepository.save()

save() ๋Š” ๋‹จ์ˆœํžˆ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ €์žฅํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹˜. ๊ฒฝ์šฐ์— ๋”ฐ๋ผ persist ๋˜๋Š” merge ๋กœ ๋™์ž‘ํ•œ๋‹ค.

  • Transient ์ƒํƒœ์˜ ๊ฐ์ฒด๋ผ๋ฉด EntityManager.persist()

  • Detached ์ƒํƒœ์˜ ๊ฐ์ฒด๋ผ๋ฉด EntityManager.merge()

persist

Persist() ๋ฉ”์†Œ๋“œ์— ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ธด(save ๋Œ€์ƒ) ๊ทธ ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด๋ฅผ Persistent ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค. save() ๊ฒฐ๊ณผ๋กœ ๋ฐ˜ํ™˜๋ฐ›์€ saved entity ๊ฐ€ ๊ณง ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ธด ๊ทธ save ๋Œ€์ƒ๊ณผ ๊ฐ™๋‹ค.

@DataJpaTest
public class PostRepositoryTest {

    @Autowired
    PostRepository postRepository;

    @PersistenceContext
    EntityManager entityManager;

    @Test
    void saveTest() {
        Post post = new Post();
        post.setName("name_1");
        post.setDescription("description_1");
        Post savedPost = postRepository.save(post);

        Assertions.assertThat(entityManager.contains(post)).isTrue();
        Assertions.assertThat(entityManager.contains(savedPost)).isTrue();
        Assertions.assertThat(post).isEqualTo(savedPost);
    }

}

merge

์ฃผ์„ ํ‘œ์‹œํ•œ ๊ฒƒ๋“ค์„ ์ž˜ ๊ธฐ์–ตํ•˜์ž. update ์‹œ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ธด entity๋Š” ์˜์†ํ™”๋˜์ง€ ์•Š๊ณ  ๊ทธ๊ฒƒ์˜ ๋ณต์‚ฌ๋ณธ์ด ์˜์†ํ™” ๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์˜์†ํ™”๋œ ๊ฐ์ฒด๊ฐ€ return ๋œ๋‹ค.

์ด ํฌ์ธํŠธ๊ฐ€ ์ฃผ๋Š” ์ค‘์š”ํ•œ ์‹œ์‚ฌ์ ์€ update ๋กœ์ง ์ฒ˜๋ฆฌ ์ดํ›„ ํ›„์† ์ž‘์—…์„ ํ•ด์•ผํ•  ๊ฒฝ์šฐ ๋ฐ˜๋“œ์‹œ return ๋œ ๊ทธ ๊ฐ’์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ์™œ๋ƒํ•˜๋ฉด ๊ทธ ๊ฐ์ฒด๊ฐ€ persistent ๋œ ๊ฐ์ฒด์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋ฐ˜๋Œ€๋กœ ๋งํ•˜๋ฉด save() ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ธด ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ด์„œ๋Š” ์•ˆ๋œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

์™œ ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ๋˜๋ƒ? managed ๊ฐ์ฒด๊ฐ€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ฆ‰, persistent ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ƒํƒœ๊ฐ€ ์ถ”์ ์ด ๋˜์ง€ ์•Š์•„์„œ dirty check ๋“ฑ JPA์˜ ์ด์ ์„ ๋ˆ„๋ฆฌ์ง€ ๋ชปํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

    @Test
    void updateTest() {
        Post post = new Post();
        post.setName("name_1");
        post.setDescription("description_1");
        postRepository.save(post);

        Post newPost = new Post();
        newPost.setId(1L);
        newPost.setName("new_name_1");
        newPost.setDescription("new_description_1");
        Assertions.assertThat(entityManager.contains(newPost)).isFalse();

        // merge ๋ฐœ์ƒ. ์ด ๋•Œ newPost์˜ ๋ณต์‚ฌ๋ณธ์ด ์˜์†ํ™”๋œ๋‹ค.
        Post updatedPost = postRepository.save(newPost);

        // ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ธด entity์ธ newPost ๋Š” detached
        Assertions.assertThat(entityManager.contains(newPost)).isFalse();

        // update ๊ฒฐ๊ณผ๋กœ ๋ฐ›์€ entity์ธ updatedPost๋Š” persistent
        Assertions.assertThat(entityManager.contains(updatedPost)).isTrue();
    }

entity์˜ ์ผ๋ถ€ ์ปฌ๋Ÿผ๋งŒ ๊ฐ€์ ธ์˜ค๋Š” ๊ธฐ๋Šฅ์ธ๋ฐ ๋‚˜๋Š” ์‹ค๋ฌด์—์„œ ์จ๋ณธ์ ์ด ์—†๊ฑฐ๋‹ˆ์™€ ์–ป๋Š” ์„ฑ๋Šฅ ํšจ์œจ ๋Œ€๋น„ ์ฝ”๋“œ ๋ณต์žก๋„๋งŒ ๋” ์ปค์ง€๋Š” ๋А๋‚Œ์ด๋‹ค. ์ผ๋‹จ์€ ์ด๋Ÿฐ๊ฒŒ ์žˆ๋‹ค ์ •๋„๋งŒ ์ธ์ง€ํ•ด๋‘”๋‹ค.

์•„๋ž˜๋Š” ๊ฐ•์˜ ๋…ธํŠธ ๊ทธ๋Œ€๋กœ ๋ฐœ์ทŒ.

  • ์ธํ„ฐํŽ˜์ด์Šค ๊ธฐ๋ฐ˜ ํ”„๋กœ์ ์…˜

    • Nested ํ”„๋กœ์ ์…˜ ๊ฐ€๋Šฅ.

    • Closed ํ”„๋กœ์ ์…˜

      • ์ฟผ๋ฆฌ๋ฅผ ์ตœ์ ํ™” ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ฐ€์ ธ์˜ค๋ ค๋Š” ์• ํŠธ๋ฆฌ๋ทฐํŠธ๊ฐ€ ๋ญ”์ง€ ์•Œ๊ณ  ์žˆ์œผ๋‹ˆ๊นŒ.

      • Java 8์˜ ๋””ํดํŠธ ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์—ฐ์‚ฐ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

    • Open ํ”„๋กœ์ ์…˜

      • @Value(SpEL)์„ ์‚ฌ์šฉํ•ด์„œ ์—ฐ์‚ฐ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค. ์Šคํ”„๋ง ๋นˆ์˜ ๋ฉ”์†Œ๋“œ๋„ ํ˜ธ์ถœ ๊ฐ€๋Šฅ. -์ฟผ๋ฆฌ ์ตœ์ ํ™”๋ฅผ ํ•  ์ˆ˜ ์—†๋‹ค. SpEL์„ ์—”ํ‹ฐํ‹ฐ ๋Œ€์ƒ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์—.

  • ํด๋ž˜์Šค ๊ธฐ๋ฐ˜ ํ”„๋กœ์ ์…˜

    • DTO

    • ๋กฌ๋ณต @Value๋กœ ์ฝ”๋“œ ์ค„์ผ ์ˆ˜ ์žˆ์Œ

Specification

query DSL ๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ query ๋ฅผ ํ”„๋กœ๊ทธ๋žจ์œผ๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค€๋‹ค. ์•„๋ž˜๋Š” JPA ๊ณต์‹ ๋ฌธ์„œ์—์„œ Specification ์— ๋Œ€ํ•ด ์„ค๋ช…ํ•œ ๊ธ€ ์ผ๋ถ€๋‹ค.

JPA 2 introduces a criteria API that you can use to build queries programmatically. By writing a criteria, you define the where clause of a query for a domain class. Taking another step back, these criteria can be regarded as a predicate over the entity that is described by the JPA criteria API constraints.

์‚ฌ์šฉ ๋ฐฉ๋ฒ•์€ ๊ณต์‹๋ฌธ์„œ์—๋„ ๋‚˜์™€์žˆ์ง€๋งŒ ๊ฐ„๋‹จํ•˜๋‹ค. repository ์—์„œ JpaSpecificationExecutor ๋ฅผ ์ƒ์† ๋ฐ›๊ณ  findAll() ์— ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ Specification ์„ ๋„˜๊ฒจ์ค€๋‹ค. pageable ๋„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ฒจ์ฃผ๋ฉด ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.

์œ ์šฉํ•˜์ง€๋งŒ ํƒ€์ž… ์„ธ์ดํ”„ํ•˜์ง€ ์•Š์•„์„œ ๊ทธ๋ƒฅ queryDsl ์“ฐ๋Š”๊ฒŒ ๋งˆ์Œ ํŽธํ•˜๋‹ค.

public interface PostRepository extends JpaRepository<Post, Long>, PostCustomRepository, QuerydslPredicateExecutor<Post>, JpaSpecificationExecutor<Post> {
}
    public static Specification<Post> getPostsByCustomSpecification(String name, String description) {
        return ((root, query, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>(); // javax.persistence.criteria.Predicate;
            if (name != null) {
                predicates.add(criteriaBuilder.equal(root.get("name"), name));
            }
            if (description != null) {
                predicates.add(criteriaBuilder.equal(root.get("description"), description));
            }
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        });
    }
    @Test
    void specificationTest() {
        Post post = new Post();
        final String NAME = "specificationPostName";
        final String DESCRIPTION = "specificationPostDescription";
        post.setName(NAME);
        post.setDescription(DESCRIPTION);
        postRepository.save(post);

        List<Post> targets = postRepository.findAll(Post.getPostsByCustomSpecification(NAME, DESCRIPTION));
        Assertions.assertThat(targets.size()).isEqualTo(1);
        Assertions.assertThat(targets.get(0).getName()).isEqualTo(NAME);
        Assertions.assertThat(targets.get(0).getDescription()).isEqualTo(DESCRIPTION);
    }
    select
        post0_.id as id1_0_,
        post0_.description as descript2_0_,
        post0_.name as name3_0_ 
    from
        post post0_ 
    where
        post0_.name=? 
        and post0_.description=?
# binding parameter [1] as [VARCHAR] - [specificationPostName]
# binding parameter [2] as [VARCHAR] - [specificationPostDescription]

๋งŒ์•ฝ ์—ฌ๊ธฐ์„œ ์•„๋ž˜์™€ ๊ฐ™์ด DESCRIPTION ์— null ์„ ๋„ฃ์„ ๊ฒฝ์šฐ ์ฟผ๋ฆฌ๊ฐ€ ์˜๋„ํ•œ๋Œ€๋กœ ๋‹ฌ๋ผ์ง„๋‹ค.

    @Test
    void specificationTest() {
        Post post = new Post();
        final String NAME = "specificationPostName";
        final String DESCRIPTION = "specificationPostDescription";
        post.setName(NAME);
        post.setDescription(DESCRIPTION);
        postRepository.save(post);

        List<Post> targets = postRepository.findAll(Post.getPostsByCustomSpecification(NAME, null));
        Assertions.assertThat(targets.size()).isEqualTo(1);
        Assertions.assertThat(targets.get(0).getName()).isEqualTo(NAME);
        Assertions.assertThat(targets.get(0).getDescription()).isEqualTo(DESCRIPTION);
    }
    select
        post0_.id as id1_0_,
        post0_.description as descript2_0_,
        post0_.name as name3_0_ 
    from
        post post0_ 
    where
        post0_.name=?

Auditing

ํŠน๋ณ„ํ•œ ๋‚ด์šฉ์€ ์—†์–ด์„œ ๊ฐ•์˜์ž๋ฃŒ๋งŒ ์ฒจ๋ถ€ํ•œ๋‹ค.

  • ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์œ„์— @EnableJpaAuditing ์ถ”๊ฐ€ (์Šคํ”„๋ง๋ถ€ํŠธ๊ฐ€ ์ž๋™์„ค์ • ํ•ด์ฃผ์ง€ ์•Š๋Š”๋‹ค)

  • ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์œ„์— @EntityListeners(AuditingEntityListener.class) ์ถ”๊ฐ€

    @CreatedDate
    private Date created;

    @LastModifiedDate
    private Date updated;

    @CreatedBy
    @ManyToOne
    private Account createdBy;

    @LastModifiedBy
    @ManyToOne
    private Account updatedBy;

Last updated