SpringJPA

공통 인터페이스

  • JPA는 간단한 CRUD를 제공한다. JpaRepository 라는 이름으로 말이다.
  • JpaRepository는 PagingAndSortingRepository, CrudRepository, Repository를 계층 구조로 상속 받는다.
  • 이는 spring data가 제공하는 공통 인터페이스다. JpaRepository는 JPA에 더 특화된 기능을 제공한다.
  • @Query로 직접 궈리를 작성할 수도 있다.
  • 메소드 이름으로 쿼리를 작성할 수도 있다.
  • @Entity@NamedQuery로 이름을 부여에 사용하는 방법도 있다.
  • springDataJpa는 쿼리 메소드에 페이징, 정렬을 사용할 수 있도록 두 가지 파라미터를 제공한다.
    1. org.springframework.data.domain.Sort
    2. org.springframework.data.domain.Pageable

힌트

  • JPA 힌트를 사용하려면 org.springframework.data.jpa.repository.QueryHints를 사용하면 된다.
  • SQL힌트가 아니라 JPA 힌트다.

Lock

  • 쿼리시 락을 걸고 싶다면 @Lock을 이용하면 된다.

Spring Data JPA가 사용하는 구현체

  • Spring Data JPA가 사용하는 구현체는 org.springframework.data.jpa.repository.support.SimpleJpaRepository가 구현한다.
@Repository
@Transactional(
        readOnly = true
)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

  public SimpleJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
    this.escapeCharacter = EscapeCharacter.DEFAULT;
    Assert.notNull(entityInformation, "JpaEntityInformation must not be null");
    Assert.notNull(entityManager, "EntityManager must not be null");
    this.entityInformation = entityInformation;
    this.entityManager = entityManager;
    this.provider = PersistenceProvider.fromEntityManager(entityManager);
  }

  public SimpleJpaRepository(Class<T> domainClass, EntityManager entityManager) {
    this(JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager), entityManager);
  }



  @Transactional
  public void deleteById(ID id) {
    Assert.notNull(id, "The given id must not be null");
    this.findById(id).ifPresent(this::delete);
  }

  @Transactional
  public void delete(T entity) {
    Assert.notNull(entity, "Entity must not be null");
    if (!this.entityInformation.isNew(entity)) {
      if (this.entityManager.contains(entity)) {
        this.entityManager.remove(entity);
      } else {
        Class<?> type = ProxyUtils.getUserClass(entity);
        T existing = this.entityManager.find(type, this.entityInformation.getId(entity));
        if (existing != null) {
          this.entityManager.remove(this.entityManager.merge(entity));
        }

      }
    }
  }

  @Transactional
  public void deleteAllById(Iterable<? extends ID> ids) {
    Assert.notNull(ids, "Ids must not be null");
    Iterator var3 = ids.iterator();

    while(var3.hasNext()) {
      ID id = (Object)var3.next();
      this.deleteById(id);
    }

  }

  @Transactional
  public void deleteAllByIdInBatch(Iterable<ID> ids) {
    Assert.notNull(ids, "Ids must not be null");
    if (ids.iterator().hasNext()) {
      if (this.entityInformation.hasCompositeId()) {
        List<T> entities = new ArrayList();
        ids.forEach((id) -> {
          entities.add(this.getReferenceById(id));
        });
        this.deleteAllInBatch(entities);
      } else {
        String queryString = String.format("delete from %s x where %s in :ids", this.entityInformation.getEntityName(), this.entityInformation.getIdAttribute().getName());
        Query query = this.entityManager.createQuery(queryString);
        Collection<ID> idCollection = toCollection(ids);
        query.setParameter("ids", idCollection);
        this.applyQueryHints(query);
        query.executeUpdate();
      }

    }
  }

  @Transactional
  public void deleteAll(Iterable<? extends T> entities) {
    Assert.notNull(entities, "Entities must not be null");
    Iterator var3 = entities.iterator();

    while(var3.hasNext()) {
      T entity = (Object)var3.next();
      this.delete(entity);
    }

  }

  @Transactional
  public void deleteAllInBatch(Iterable<T> entities) {
    Assert.notNull(entities, "Entities must not be null");
    if (entities.iterator().hasNext()) {
      QueryUtils.applyAndBind(QueryUtils.getQueryString("delete from %s x", this.entityInformation.getEntityName()), entities, this.entityManager).executeUpdate();
    }
  }

  @Transactional
  public void deleteAll() {
    Iterator var2 = this.findAll().iterator();

    while(var2.hasNext()) {
      T element = (Object)var2.next();
      this.delete(element);
    }

  }

  @Transactional
  public void deleteAllInBatch() {
    Query query = this.entityManager.createQuery(this.getDeleteAllQueryString());
    this.applyQueryHints(query);
    query.executeUpdate();
  }

  public Optional<T> findById(ID id) {
    Assert.notNull(id, "The given id must not be null");
    Class<T> domainType = this.getDomainClass();
    if (this.metadata == null) {
      return Optional.ofNullable(this.entityManager.find(domainType, id));
    } else {
      LockModeType type = this.metadata.getLockModeType();
      Map<String, Object> hints = this.getHints();
      return Optional.ofNullable(type == null ? this.entityManager.find(domainType, id, hints) : this.entityManager.find(domainType, id, type, hints));
    }
  }

  /** @deprecated */
  @Deprecated
  public T getOne(ID id) {
    return this.getReferenceById(id);
  }

  /** @deprecated */
  @Deprecated
  public T getById(ID id) {
    return this.getReferenceById(id);
  }

  public T getReferenceById(ID id) {
    Assert.notNull(id, "The given id must not be null");
    return this.entityManager.getReference(this.getDomainClass(), id);
  }

  public boolean existsById(ID id) {
    Assert.notNull(id, "The given id must not be null");
    if (this.entityInformation.getIdAttribute() == null) {
      return this.findById(id).isPresent();
    } else {
      String placeholder = this.provider.getCountQueryPlaceholder();
      String entityName = this.entityInformation.getEntityName();
      Iterable<String> idAttributeNames = this.entityInformation.getIdAttributeNames();
      String existsQuery = QueryUtils.getExistsQueryString(entityName, placeholder, idAttributeNames);
      TypedQuery<Long> query = this.entityManager.createQuery(existsQuery, Long.class);
      this.applyQueryHints(query);
      if (!this.entityInformation.hasCompositeId()) {
        query.setParameter((String)idAttributeNames.iterator().next(), id);
        return (Long)query.getSingleResult() == 1L;
      } else {
        Iterator var8 = idAttributeNames.iterator();

        while(var8.hasNext()) {
          String idAttributeName = (String)var8.next();
          Object idAttributeValue = this.entityInformation.getCompositeIdAttributeValue(id, idAttributeName);
          boolean complexIdParameterValueDiscovered = idAttributeValue != null && !query.getParameter(idAttributeName).getParameterType().isAssignableFrom(idAttributeValue.getClass());
          if (complexIdParameterValueDiscovered) {
            return this.findById(id).isPresent();
          }

          query.setParameter(idAttributeName, idAttributeValue);
        }

        if ((Long)query.getSingleResult() == 1L) {
          return true;
        } else {
          return false;
        }
      }
    }
  }

  public List<T> findAll() {
    return this.getQuery((Specification)null, (Sort)Sort.unsorted()).getResultList();
  }

  public List<T> findAllById(Iterable<ID> ids) {
    Assert.notNull(ids, "Ids must not be null");
    if (!ids.iterator().hasNext()) {
      return Collections.emptyList();
    } else if (!this.entityInformation.hasCompositeId()) {
      Collection<ID> idCollection = toCollection(ids);
      ByIdsSpecification<T> specification = new ByIdsSpecification(this.entityInformation);
      TypedQuery<T> query = this.getQuery(specification, (Sort)Sort.unsorted());
      return query.setParameter(specification.parameter, idCollection).getResultList();
    } else {
      List<T> results = new ArrayList();
      Iterator var4 = ids.iterator();

      while(var4.hasNext()) {
        ID id = (Object)var4.next();
        this.findById(id).ifPresent(results::add);
      }

      return results;
    }
  }

  public List<T> findAll(Sort sort) {
    return this.getQuery((Specification)null, (Sort)sort).getResultList();
  }

  public Page<T> findAll(Pageable pageable) {
    return (Page)(pageable.isUnpaged() ? new PageImpl(this.findAll()) : this.findAll((Specification)null, (Pageable)pageable));
  }

  public Optional<T> findOne(Specification<T> spec) {
    try {
      return Optional.of(this.getQuery(spec, Sort.unsorted()).setMaxResults(2).getSingleResult());
    } catch (NoResultException var2) {
      return Optional.empty();
    }
  }

  public List<T> findAll(Specification<T> spec) {
    return this.getQuery(spec, Sort.unsorted()).getResultList();
  }

  public Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable) {
    TypedQuery<T> query = this.getQuery(spec, pageable);
    return (Page)(pageable.isUnpaged() ? new PageImpl(query.getResultList()) : this.readPage(query, this.getDomainClass(), pageable, spec));
  }

  public List<T> findAll(@Nullable Specification<T> spec, Sort sort) {
    return this.getQuery(spec, sort).getResultList();
  }

  public boolean exists(Specification<T> spec) {
    CriteriaQuery<Integer> cq = this.entityManager.getCriteriaBuilder().createQuery(Integer.class).select(this.entityManager.getCriteriaBuilder().literal(1));
    this.applySpecificationToCriteria(spec, this.getDomainClass(), cq);
    TypedQuery<Integer> query = this.applyRepositoryMethodMetadata(this.entityManager.createQuery(cq));
    return query.setMaxResults(1).getResultList().size() == 1;
  }

  @Transactional
  public long delete(@Nullable Specification<T> spec) {
    CriteriaBuilder builder = this.entityManager.getCriteriaBuilder();
    CriteriaDelete<T> delete = builder.createCriteriaDelete(this.getDomainClass());
    if (spec != null) {
      Predicate predicate = spec.toPredicate(delete.from(this.getDomainClass()), builder.createQuery(this.getDomainClass()), builder);
      if (predicate != null) {
        delete.where(predicate);
      }
    }

    return (long)this.entityManager.createQuery(delete).executeUpdate();
  }

  public <S extends T, R> R findBy(Specification<T> spec, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) {
    Assert.notNull(spec, "Specification must not be null");
    Assert.notNull(queryFunction, "Query function must not be null");
    return this.doFindBy(spec, this.getDomainClass(), queryFunction);
  }

  private <S extends T, R> R doFindBy(Specification<T> spec, Class<T> domainClass, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) {
    Assert.notNull(spec, "Specification must not be null");
    Assert.notNull(queryFunction, "Query function must not be null");
    FluentQuerySupport.ScrollQueryFactory scrollFunction = (sort, scrollPosition) -> {
      Specification<T> specToUse = spec;
      if (scrollPosition instanceof KeysetScrollPosition keyset) {
        KeysetScrollSpecification<T> keysetSpec = new KeysetScrollSpecification(keyset, sort, this.entityInformation);
        sort = keysetSpec.sort();
        specToUse = specToUse.and(keysetSpec);
      }

      TypedQuery<T> query = this.getQuery(specToUse, domainClass, sort);
      if (scrollPosition instanceof OffsetScrollPosition offset) {
        if (!offset.isInitial()) {
          query.setFirstResult(Math.toIntExact(offset.getOffset()) + 1);
        }
      }

      return query;
    };
    Function<Sort, TypedQuery<T>> finder = (sort) -> {
      return this.getQuery(spec, domainClass, sort);
    };
    FetchableFluentQueryBySpecification.SpecificationScrollDelegate<T> scrollDelegate = new FetchableFluentQueryBySpecification.SpecificationScrollDelegate(scrollFunction, this.entityInformation);
    FetchableFluentQueryBySpecification<?, T> fluentQuery = new FetchableFluentQueryBySpecification(spec, domainClass, finder, scrollDelegate, this::count, this::exists, this.entityManager, this.getProjectionFactory());
    return queryFunction.apply(fluentQuery);
  }

  public <S extends T> Optional<S> findOne(Example<S> example) {
    try {
      return Optional.of(this.getQuery(new ExampleSpecification(example, this.escapeCharacter), example.getProbeType(), (Sort)Sort.unsorted()).setMaxResults(2).getSingleResult());
    } catch (NoResultException var2) {
      return Optional.empty();
    }
  }

  public <S extends T> long count(Example<S> example) {
    return executeCountQuery(this.getCountQuery(new ExampleSpecification(example, this.escapeCharacter), example.getProbeType()));
  }

  public <S extends T> boolean exists(Example<S> example) {
    Specification<S> spec = new ExampleSpecification(example, this.escapeCharacter);
    CriteriaQuery<Integer> cq = this.entityManager.getCriteriaBuilder().createQuery(Integer.class).select(this.entityManager.getCriteriaBuilder().literal(1));
    this.applySpecificationToCriteria(spec, example.getProbeType(), cq);
    TypedQuery<Integer> query = this.applyRepositoryMethodMetadata(this.entityManager.createQuery(cq));
    return query.setMaxResults(1).getResultList().size() == 1;
  }

  public <S extends T> List<S> findAll(Example<S> example) {
    return this.getQuery(new ExampleSpecification(example, this.escapeCharacter), example.getProbeType(), (Sort)Sort.unsorted()).getResultList();
  }

  public <S extends T> List<S> findAll(Example<S> example, Sort sort) {
    return this.getQuery(new ExampleSpecification(example, this.escapeCharacter), example.getProbeType(), (Sort)sort).getResultList();
  }

  public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
    ExampleSpecification<S> spec = new ExampleSpecification(example, this.escapeCharacter);
    Class<S> probeType = example.getProbeType();
    TypedQuery<S> query = this.getQuery(new ExampleSpecification(example, this.escapeCharacter), probeType, (Pageable)pageable);
    return (Page)(pageable.isUnpaged() ? new PageImpl(query.getResultList()) : this.readPage(query, probeType, pageable, spec));
  }

  public <S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) {
    Assert.notNull(example, "Example must not be null");
    Assert.notNull(queryFunction, "Query function must not be null");
    ExampleSpecification<S> spec = new ExampleSpecification(example, this.escapeCharacter);
    Class<S> probeType = example.getProbeType();
    return this.doFindBy(spec, probeType, queryFunction);
  }

  public long count() {
    TypedQuery<Long> query = this.entityManager.createQuery(this.getCountQueryString(), Long.class);
    this.applyQueryHintsForCount(query);
    return (Long)query.getSingleResult();
  }

  public long count(@Nullable Specification<T> spec) {
    return executeCountQuery(this.getCountQuery(spec, this.getDomainClass()));
  }

  @Transactional
  public <S extends T> S save(S entity) {
    Assert.notNull(entity, "Entity must not be null");
    if (this.entityInformation.isNew(entity)) {
      this.entityManager.persist(entity);
      return entity;
    } else {
      return this.entityManager.merge(entity);
    }
  }

  @Transactional
  public <S extends T> S saveAndFlush(S entity) {
    S result = this.save(entity);
    this.flush();
    return result;
  }

  @Transactional
  public <S extends T> List<S> saveAll(Iterable<S> entities) {
    Assert.notNull(entities, "Entities must not be null");
    List<S> result = new ArrayList();
    Iterator var4 = entities.iterator();

    while(var4.hasNext()) {
      S entity = (Object)var4.next();
      result.add(this.save(entity));
    }

    return result;
  }

  @Transactional
  public <S extends T> List<S> saveAllAndFlush(Iterable<S> entities) {
    List<S> result = this.saveAll(entities);
    this.flush();
    return result;
  }

  @Transactional
  public void flush() {
    this.entityManager.flush();
  }
}
  • 길지만 많은 구현이 있기에 넣었다. 눈여겨 볼 점은 @Transactional이 붙었다는 점 @Repository가 붙었다는 점이다.
    1. @Repository: JPA 예외를 스프링이 추상화한 예외로 변환
    2. @Transactional
    1. readOnly = true : 플러시를 생략해서 약간의 성능 향상을 얻을 수 있다.