12 Eylül 2019 Perşembe

SpringData Projections

Giriş
SpringData JPA 5 çeşit projection imkanı sağlar. Bunlar şöyle
- JPA Tuple Aryüzü ile Projection
- Interface-based Projections - Kullanın
- Record-based Projections - Kullanın
- Open Projections - Kullanmayın
- Class-based Projections (DTOs)
- Dynamic Projections

Bunların dışında JPA standardı da DTO Projection imkanı veriyor.

1. JPA Tuple Arayüzü İle Projections - Kullanmayın
Örnek
Şöyle yaparız. Burada org.springframework.data.jpa.repository.Query anotasyonu kullanılıyor
import javax.persistence.Tuple;

@Query("""
    select
        p.id as id,
        p.title as title,
        c.review as review
    from PostComment c
    join c.post p
    where p.title like :postTitle
    order by c.id
    """)
List<Tuple> findCommentTupleByTitle(
    @Param("postTitle") String postTitle
);
Okumak için şöyle yaparız
List<Tuple> commentTuples = postRepository
    .findCommentTupleByTitle(titleToken);
 
 
Tuple commentTuple = commentTuples.get(0);
 
assertEquals(
    Long.valueOf(1),
    ((Number) commentTuple.get("id")).longValue()
);
 
assertTrue(
    ((String) commentTuple.get("title"))
        .contains("Chapter nr. 1")
);
Bu yöntemde veriyi istenilen tipe cast etmek gerekiyor. Bu da iyi bir şey değil. Açıklaması şöyle
However, while the Tuple allows us to retrieve the column values by their column alias, we still need to do a type casting, and that's a major limitation since the customer will have to know upfront the actual type to cast to.

It would be much better if the projection container were type-safe.

2. Interface-based Projections - En Kolay Yöntem
Sadece çekmek istediğimiz veriyi temsil eden ve getter metodlara sahip bir arayüz tanımlarız. Repository sınıfımıza bu arayüzü döndüren bir sorgu metodu ekleriz. Bu yöntem için şöyle bir eksi taraf belirtilmiş.Yani arayüzden sınıf türetmek işini Spring yaptığı için equals() ve hashCode() metodlarını da Spring üretiyor ve değiştirmek mümkün değil. Ama bence bu durumda da çoğunlukla gerek yok.
However, there is also a downside to using the Proxy projection. We cannot provide a specific implementation for equals and hashCode, and this limits its usability.

Örnek
Elimizde şöyle bir kod olsun.
interface ProjectIdAndName{
  String getId();
  String getName();
}
Repository kodunda şöyle yaparız.
List<ProjectIdAndName> findAll();
Böylece aynı şu SQL cümlesi çalışıyor gibi olur.
"SELECT projectId, projectName FROM projects";
Örnek
Elimizde şöyle bir kod olsun.
public interface UserInformationNameProjection {
  String getUserName();
}
Repository sınıfında şöyle yaparız
@Query(value="select user_name from user_table where user_age > 
           :userAge", nativeQuery=true)
public UserInformationNameProjection getInfo(int userAge);
Örnek
Şöyle yaparız
public interface PostCommentSummary {
 
  Long getId();
 
  String getTitle();
 
  String getReview();
}

@Query("""
    select
        p.id as id,
        p.title as title,
        c.review as review
    from PostComment c
    join c.post p
    where p.title like :postTitle
    order by c.id
    """)
List<PostCommentSummary> findCommentSummaryByTitle(
  @Param("postTitle") String postTitle
);

List<PostCommentSummary> commentSummaries = postRepository
  .findCommentSummaryByTitle(titleToken);
Record-based Projections - Kullanın
Mantı olarak Interface-based Projection ile aynı

Örnek - fully qualified name olmadan
Açıklaması şöyle
Normally, we'd have to use the fully-qualified name of the PostCommentRecord Java class, but thanks to the ClassImportIntegrator offered by the Hibernate Types project, we can use the simple Class name in the constructor expression JPQL queries.

To benefit from this feature, you just need to provide the ClassImportIntegrator that registers all DTO and Record classes in your Java-based Spring configuration bean via the "hibernate.integrator_provider" Hibernate setting
Şöyle yaparız
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
  LocalContainerEntityManagerFactoryBean entityManagerFactoryBean =
    new LocalContainerEntityManagerFactoryBean();
     
  ...
     
  Properties properties = new Properties();
  properties.put(
    "hibernate.integrator_provider",
      (IntegratorProvider) () -> Collections.singletonList(
        new ClassImportIntegrator(
          List.of(
            PostCommentDTO.class,
            PostCommentRecord.class
          )
        )
      )
  );
  entityManagerFactoryBean.setJpaProperties(properties);
     
  return entityManagerFactoryBean;
}
Şöyle kullanırız
public record PostCommentRecord(
    Long id,
    String title,
    String review
) {}

@Query("""
    select new PostCommentRecord(
        p.id as id,
        p.title as title,
        c.review as review
    )
    from PostComment c
    join c.post p
    where p.title like :postTitle
    order by c.id
    """)
List<PostCommentRecord> findCommentRecordByTitle(
    @Param("postTitle") String postTitle
);
Open Projections - SpEL Kullanır - Kullanmayın

Class veya DTO Based Projections

Açıklaması şöyle. Bu aslında Spring'e özel bir şey değil. JPA'nın kendisi DTO projection yapabiliyor.
Another way of defining projections is by using value type DTOs (Data Transfer Objects) that hold properties for the fields that are supposed to be retrieved. These DTO types can be used in exactly the same way projection interfaces are used, except that no proxying happens and no nested projections can be applied.
Örnek
Şöyle yaparız
@Query("SELECT new com.foo.MailingAddressDTO (fullName, address) FROM alo 
  CustomerEntity customer")
List<MailingAddressDTO> findMailingAddresses();
Örnek
Elimizde şöyle bir kod olsun. Ben burada kolaylık olsun diye Lombok kullandım. Yani aslında amacına aykırı bir şey yaptım
@Data
@RequiredArgsConstructor
public class PostCommentDTO {
 
  private final Long id;
 
  private final String title;
 
  private final String review;
   
}
Kullanmak için şöyle yaparız
@Query("""
    select new PostCommentDTO(
        p.id as id,
        p.title as title,
        c.review as review
    )
    from PostComment c
    join c.post p
    where p.title like :postTitle
    order by c.id
    """)
List<PostCommentDTO> findCommentDTOByTitle(
    @Param("postTitle") String postTitle
);
Örnek - hiyerarşik DTO
Elimizde şöyle bir kod olsun. PostDTO nesnesinin CommentDTO listesi var. Burada Hibernate kütüphanesindeki ResultTransformer kullanılıyor
public class CustomPostRepositoryImpl implements CustomPostRepository {
     
  @PersistenceContext
  private EntityManager entityManager;
 
  @Override
  public List<PostDTO> findPostDTOByTitle(
    @Param("postTitle") String postTitle) {

   return entityManager.createNativeQuery("""
     SELECT p.id AS p_id,
            p.title AS p_title,
            pc.id AS pc_id,
            pc.review AS pc_review
      FROM post p
      JOIN post_comment pc ON p.id = pc.post_id
      WHERE p.title LIKE :postTitle
      ORDER BY pc.id
      """)
      .setParameter("postTitle", postTitle)
      .unwrap(org.hibernate.query.Query.class)
      .setResultTransformer(new PostDTOResultTransformer())
      .getResultList();
  }
}
ResultTransformer kodu şöyle
public class PostDTOResultTransformer implements ResultTransformer {
 
  private Map<Long, PostDTO> postDTOMap = new LinkedHashMap<>();
 
  @Override
  public PostDTO transformTuple(Object[] tuple, String[] aliases) {
    Map<String, Integer> aliasToIndexMap = aliasToIndexMap(aliases);
         
    Long postId = AbstractTest.longValue(
      tuple[aliasToIndexMap.get(PostDTO.ID_ALIAS)]
    );
 
    PostDTO postDTO = postDTOMap.computeIfAbsent(postId,
      id -> new PostDTO(tuple, aliasToIndexMap));
    postDTO.getComments().add(
     new PostCommentDTO(tuple, aliasToIndexMap));
 
    return postDTO;
  }
 
  @Override
  public List<PostDTO> transformList(List collection) {
    return new ArrayList<>(postDTOMap.values());
  }
 
  private Map<String, Integer> aliasToIndexMap(String[] aliases) {
    Map<String, Integer> aliasToIndexMap = new LinkedHashMap<>();
         
    for (int i = 0; i < aliases.length; i++) {
      aliasToIndexMap.put(aliases[i].toLowerCase(Locale.ROOT),i);
    }
    return aliasToIndexMap;
  }
}


Hiç yorum yok:

Yorum Gönder