İçeriğe geç

JPA Projection ve DTO — Üç Alan İçin Ne Kadar Veri?

Orta 8 dk Çok sık karşılaşılır

Önce şunu oku: JPA N+1 Problemi , Persistence Context — Hibernate'in Hafızası

30 saniyede özet

Sadece bir listede isim ve fiyat göstereceksen bütün nesneyi yüklemene gerek yok. Projection ya da record DTO yalnızca gereken sütunları getirir. Ama @Value ile yazılan açık projection nesnenin tamamını yine yükler.

Bir müşterinin telefon numarasını öğrenmek için arşivden bütün dosyasını getirttiğini düşün. Numara ilk sayfada yazıyor.

Bütün dosya mı, yoksa yalnızca gereken satır mı?
Adım adım oku
  1. Liste ekranı yalnızca isim ve fiyat gösteriyor, ama entity yüklemek bütün sütunları getirir.
  2. Uzun notlar metni de taşınır; Hibernate ayrıca her nesnenin bir karşılaştırma kopyasını saklar.
  3. Projection ile sorgu yalnızca isim ve fiyat sütunlarını ister.
  4. Daha az sütun taşınır, kopya saklanmaz, bellek ve ağ rahatlar.
  1. Bayt: Sipariş listesi yavaşlayınca entity yerine bir projection yazdık.

  2. Sen: Hızlandı mı?

  3. Bayt: Hayır! Sorgu sayısı da taşınan veri de aynı kaldı.

  4. Bayt: Projection yazmak her zaman yalnızca gerekeni mi getirir? Önce bir tahmin yapalım.

Sipariş listesi yavaşlayınca ekip entity yerine bir “projection” yazdı. Beklenen hızlanma gelmedi: sorgu sayısı aynı kaldı.

Bütün dosyayı taşımak

Bir entity yüklemek bütün sütunlarını getirmek demektir, büyük bir notes metni dahil. Entity persistence context’e girer ve Hibernate onun bir fotoğrafını dirty checking için saklar.

LAZY bir ilişkiye dokunmak her satırda bir sorgu daha üretir. Fetch join bunu tek sorguya indirir, ama sütunları ve izlenen nesneleri azaltmaz.

Kafam karıştı, daha basit anlat

Bir müşterinin yalnızca adını soracaksan bütün dosyasını getirme. Entity yüklemek dosyanın tamamını masaya koymaktır.

Hızlı kontrolBaşlangıç

Liste ekranında sipariş no, müşteri adı ve tutar gösteriliyor; kullanıcı bu ekranda hiçbir şeyi değiştirmiyor. Sorgu ne döndürmeli?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

JOIN FETCH ile N+1 çözüldü. Aynı liste ekranında DTO projection'a geçmek ne kazandırır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sadece gerekeni iste

Bir projectionSorgudan entity yerine yalnızca gereken alanları taşıyan bir tip döndürmek: interface ya da record. Seçilen sütunlar azalır, persistence context'e hiçbir şey girmez.Sözlükte gör →, sorgudan entity yerine yalnızca gereken alanları döndürür:

  • Kapalı interface projection: getter’lar alanlara ya da sorgudaki alias’lara karşılık gelir.
  • Record DTO: select new OrderRow(o.id, c.name, o.total) ile doğrudan oluşturulur.
  • Dinamik projection: <T> List<T> findByStatus(Status s, Class<T> type), dönüş tipini çağıran seçer.
Interface projection'da getCustomerName() @Value('#{target.customer.name}') ile işaretli. Spring Data kaç sütun seçer? Cevabı göster

Hepsini. SpEL ifadesi entity’nin kendisine (target) erişir; Spring Data hangi alanın gerekeceğini bilemez. Entity yüklenir ve LAZY customer her satırda ayrı sorguyla gelir.

Buna open projectionGetter'ı @Value ile bir SpEL ifadesine bağlanan interface projection. İfade entity'nin kendisine eriştiği için Spring Data bütün entity'yi yükler.Sözlükte gör → denir: kolaylık sağlar, performans sağlamaz.

Kafam karıştı, daha basit anlat

Projection, dosyadan yalnızca istediğin satırların fotokopisini çekmektir. Daha hafiftir ve Hibernate onu izlemek zorunda kalmaz.

Hızlı kontrolOrta

Aşağıdaki projection'ı kullanan bir sorgu için Spring Data ne seçer? interface OrderRow { Long getId(); @Value("#{target.customer.name}") String getCustomerName(); }

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Spring Data JPA repository'sinde aynı sorguyu bazen OrderRow, bazen OrderDetail olarak döndürmek istiyorsun. Hangi özellik bunu sağlar?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin gör

Üç alan için ne kadar veri çekiyorsun?

Tohum 708145

Oynat ya da adımla.

Hız
Adım 0

Şu an ne oldu?

Açık projection (@Value SpEL)

Ekranda sipariş no, müşteri adı ve tutar var. Veritabanından ne kadarı geliyor?

Görevler0/3

  • Bir "projection" ile N+1 üretaçık

    İpucu

    SpEL'e bak.

  • Tek sorgu, ama 3 alan için 20 sütunaçık

    İpucu

    N+1'i entity'yi bırakmadan çöz.

  • 200 satırlık sayfayı sıfır entity ile getiraçık

    İpucu

    Record ya da kapalı projection.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

  1. Varsayılanla oynat. Açık projection: 21 sorgu, 20 sütun, 40 entity.
  2. “JOIN FETCH” seç. Tek sorgu, ama sütunlar ve entity’ler aynı.
  3. “record DTO” seç. Tek sorgu, 3 sütun, sıfır entity.
  4. Sayfayı 200 yap, sonra List<Order> seç. 201 sorgu.
Hızlı kontrolOrta

Sipariş listesi uç noktası yavaş ve JSON'da müşterinin parola hash'i görünüyor. Hatalı satır hangisi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Hatalı satıra dokun, sonra kontrol et.

OrderController.java
Java 21UTF-8LF

Her ihtiyacı en uygun veri çekme biçimine göre ayır.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Entity (+ fetch join gerekirse)

Değişecek; iş kuralı çalışacak.

    Projection / DTO

    Yalnızca okunacak, gösterilecek.

      Tuzaklar

      Entity’yi API’ye döndürmek. Jackson her getter’ı dolaşır: LAZY ilişkiler yüklenir, istemediğin alanlar JSON’a çıkar. API’nin sözleşmesi bir DTO olmalı.

      Projection’ı değiştirmeye çalışmak. Projection yönetilmez; değişikliği kimse kaydetmez. Değişiklik için id ile entity al ve domain metodunu çağır.

      Native sorguda alias’sız sütun. Interface projection sütunları adla eşler. full_name sütunu getCustomerName()’e kendiliğinden bağlanmaz; AS customerName yaz.

      Kafam karıştı, daha basit anlat

      Fotokopinin üzerine yazmak asıl dosyayı değiştirmez. Değiştirmek istiyorsan asıl dosyayı getir ve onun üzerinde çalış.

      Hızlı kontrolİleri

      Native bir SQL sorgusunun sonucunu interface projection'a eşlemek istiyorsun: SELECT o.id, c.full_name, o.total FROM … Projection'da getCustomerName() var ve hep null dönüyor. Neden?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Aşağıdaki örnek bir bankanın düzenli ödeme talimatları ekranından. Aynı entity üzerinde okuma ve yazma yolları ayrılıyor: liste ekranı projection ile, talimat iptali entity ve domain metoduyla.

      Derinleş · Düzenli ödeme talimatları: okuma projection, yazma entity 5 dosya · ~123 satır · ilk okumada atlayabilirsin
      Proje dosyaları

      src/main/java/com/bank/standingorder/ StandingOrder.java Entity: LAZY hesap, büyük değişiklik geçmişi alanı ve domain metodu. Yazma yolu bunu kullanır.

      src/main/java/com/bank/standingorder/StandingOrder.java
      // "Her ayın 1'inde kira için 15.000 TL gönder": a customer's recurring payment instruction.
      @Entity
      @Table(name = "standing_orders")
      public class StandingOrder {
      @Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
      private Long id;
      @ManyToOne(fetch = FetchType.LAZY, optional = false)
      private Account account;
      private String payeeName;
      private String payeeIban;
      private BigDecimal amount;
      private int dayOfMonth;
      @Enumerated(EnumType.STRING)
      private StandingOrderStatus status;
      private Instant cancelledAt;
      @Lob
      private String changeHistory; // large, never needed on the list screen
      public void cancel(Clock clock) {
      if (status == StandingOrderStatus.EXECUTING) {
      // Today's payment is already with the clearing system; cancelling now
      // would show "cancelled" while the money still leaves.
      throw new IllegalStateException("cannot cancel while today's payment is executing");
      }
      status = StandingOrderStatus.CANCELLED;
      cancelledAt = Instant.now(clock);
      }
      // getters omitted
      }

      src/main/java/com/bank/standingorder/ StandingOrderRow.java Liste ekranının satırı: yalnızca gereken alanlar, değiştirilemez.

      src/main/java/com/bank/standingorder/StandingOrderRow.java
      public record StandingOrderRow(Long id, String payeeName, BigDecimal amount, int dayOfMonth,
      StandingOrderStatus status) {
      }

      src/main/java/com/bank/standingorder/ StandingOrderRepository.java Dört sorgu biçimi: record DTO, kapalı interface projection, dinamik projection ve alias'lı native sorgu.

      src/main/java/com/bank/standingorder/StandingOrderRepository.java
      interface StandingOrderRepository extends JpaRepository<StandingOrder, Long> {
      // 1. Record DTO: one query, five columns, nothing in the persistence context.
      @Query("""
      select new com.bank.standingorder.StandingOrderRow(s.id, s.payeeName, s.amount, s.dayOfMonth, s.status)
      from StandingOrder s join s.account a
      where a.customerNo = :customerNo and s.status = :status
      """)
      Page<StandingOrderRow> findRows(String customerNo, StandingOrderStatus status, Pageable pageable);
      // 2. Closed interface projection: getters match the aliases.
      interface MonthlyOutflow {
      String getIban();
      BigDecimal getTotal();
      }
      @Query("""
      select a.iban as iban, sum(s.amount) as total
      from StandingOrder s join s.account a
      where s.status = com.bank.standingorder.StandingOrderStatus.ACTIVE
      group by a.iban
      """)
      List<MonthlyOutflow> findMonthlyOutflows();
      // 3. Dynamic projection: the caller picks the shape.
      <T> List<T> findByDayOfMonthAndStatus(int dayOfMonth, StandingOrderStatus status, Class<T> type);
      // 4. Native SQL: alias every column to the getter name, or the value stays null.
      @Query(value = """
      SELECT a.iban AS iban, SUM(s.amount) AS total
      FROM standing_orders s JOIN accounts a ON a.id = s.account_id
      WHERE s.status = 'ACTIVE' AND s.day_of_month = :day
      GROUP BY a.iban
      """, nativeQuery = true)
      List<MonthlyOutflow> findDueOnDayNative(int day);
      }

      src/main/java/com/bank/standingorder/ StandingOrderService.java Okuma servisi projection döndürür; yazma servisi entity yükleyip domain metodunu çağırır.

      src/main/java/com/bank/standingorder/StandingOrderService.java
      @Service
      class StandingOrderService {
      private final StandingOrderRepository standingOrders;
      private final Clock clock;
      StandingOrderService(StandingOrderRepository standingOrders, Clock clock) {
      this.standingOrders = standingOrders;
      this.clock = clock;
      }
      // Read path: a projection. No entities, no dirty-checking snapshots.
      @Transactional(readOnly = true)
      public Page<StandingOrderRow> list(String customerNo, StandingOrderStatus status, Pageable pageable) {
      return standingOrders.findRows(customerNo, status, pageable);
      }
      // Write path: load the entity, call the domain method, let dirty checking write it.
      @Transactional
      public void cancel(long id) {
      StandingOrder order = standingOrders.findById(id)
      .orElseThrow(() -> new StandingOrderNotFoundException(id));
      order.cancel(clock);
      }
      }

      src/main/java/com/bank/standingorder/ StandingOrderController.java API sözleşmesi DTO'dur; entity hiçbir zaman JSON'a çıkmaz.

      src/main/java/com/bank/standingorder/StandingOrderController.java
      @RestController
      @RequestMapping("/standing-orders")
      class StandingOrderController {
      private final StandingOrderService service;
      StandingOrderController(StandingOrderService service) {
      this.service = service;
      }
      @GetMapping
      Page<StandingOrderRow> list(@AuthenticationPrincipal Customer customer,
      @RequestParam(defaultValue = "ACTIVE") StandingOrderStatus status,
      @PageableDefault(size = 20, sort = "dayOfMonth") Pageable pageable) {
      return service.list(customer.number(), status, pageable);
      }
      @PostMapping("/{id}/cancel")
      ResponseEntity<Void> cancel(@PathVariable long id) {
      service.cancel(id); // ownership is checked with @PreAuthorize in the real code
      return ResponseEntity.noContent().build();
      }
      }

      Kendini sına

      Şimşek turu1/5

      Bir entity yüklemek, bütün sütunlarını getirmek demektir.

      Soru 1/2İleri

      1 milyon siparişi CSV'ye aktaran bir iş OutOfMemoryError veriyor. findAll() ile Order entity'leri okunuyor. En etkili değişiklik hangisi?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Aklında kalacak üç şey

      1. 1 Yalnızca gösterilecek veri için entity değil, gereken alanları taşıyan bir record ya da kapalı interface projection döndürülür.
      2. 2 @Value ile yazılan açık projection bütün entity'yi yükler ve tembel ilişkilerde N+1 üretebilir.
      3. 3 Projection salt okunurdur ve Hibernate tarafından izlenmez. Bir şey değiştirilecekse entity yüklenir.
      Sonraki kapı Gece 2'de bir havale başarısız oldu. Üç servisin logunda 9 satır '42' içeriyor. Hangisi gerçekten o müşterinin? Yapılandırılmış Loglama — Gece 2'de Doğru Satırı Bulmak · 8 dk

      4 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı