İçeriğe geç

Kalıtım Stratejileri — Üç Ödeme Türü, Kaç Tablo?

Orta 9 dk Sık karşılaşılır

Önce şunu oku: Flush ve SQL Sırası — save() Satırında Neden Hiçbir Şey Olmuyor?

30 saniyede özet

Java'daki bir sınıf ağacını tabloya dökmenin dört yolu var: tek tablo, ortak tablo ve ekler, türe göre ayrı tablo, ya da yalnızca ortak alanları paylaşmak. Her biri bir soruyu ucuzlatır, bir başkasını pahalılaştırır.

Bankada üç tür ödeme var: kartla, havaleyle ve fatura ödemesi. Hepsinin bir tutarı ve bir sahibi var, ama her birinin kendine ait birkaç bilgisi de var. Bunları nasıl dosyalarsın?

  1. Bayt: Kodda çok kolay: Payment üst sınıf, altında üç tür. Bitti.

  2. Sen: Veritabanında da öyle mi?

  3. Bayt: Tablolarda üst sınıf diye bir şey yok ki!

  4. Bayt: O zaman bir karar vermemiz gerekiyor. Hangi soruyu daha sık soracağız?

Java’da kalıtım var, tabloda yok

Java’da kalıtımBir sınıfın başka bir sınıfı extends ile genişletip onun API'sini ve uygulamasını devralması. "B, bir A'dır" ilişkisini ifade eder.Sözlükte gör → sayesinde CardPayment bir Payment’tır. Bir listede üç türü karışık tutabilir, hepsine Payment diye bakabilirsin.

İlişkisel tabloların böyle bir kavramı yok. @Inheritance(strategy = …) ile Hibernate’e sınıf ağacını tablolara nasıl dökeceğini söylersin.

Kafam karıştı, daha basit anlat

Kodda bir aile ağacı var, veritabanında yalnızca tablolar. Strateji, aileyi tablolara nasıl yerleştireceğini seçmektir.

Hızlı kontrolBaşlangıç

@Inheritance anotasyonu Hibernate'e neyi söyler?

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

@Inheritance yazıp strateji belirtmezsen Hibernate hangisini kullanır?

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

Üç dolap düzeni

JOINED stratejisi, Payment'ın üç alt sınıfı var. Müşterinin bütün ödemelerini listeleyen sorguda kaç JOIN olur? Cevabı göster

Üç. Ortak bilgiler payments tablosunda, türe özel bilgiler her alt sınıfın kendi tablosunda. Satırın türünü bilmeden okuyacağın için hepsiyle LEFT JOIN yapılır.

Aynı formlar, üç dosyalama düzeni.
Adım adım oku
  1. SINGLE_TABLE: her şey tek çekmecede, her formda başka türlerin boş kutucukları var.
  2. JOINED: ortak bilgi bir dosyada, türe özel bilgi zımbalı bir ekte; okurken birleştirilir.
  3. TABLE_PER_CLASS: her türe ayrı dolap; hepsini görmek için bütün dolapları gezersin.
  4. Hangisi doğru? En sık sorduğun soruyu ucuzlatan.

SINGLE_TABLE varsayılandır. Bütün türler tek tabloda; satırın türünü bir discriminatorTek tablodaki bir satırın hangi alt sınıfa ait olduğunu söyleyen kolon. Varsayılan değer entity adıdır; @DiscriminatorValue ile sabitlenir.Sözlükte gör → kolonu söyler. Okuma hızlıdır ama kart satırında havale kolonları boş kalır.

JOINED ortak kolonları bir tabloda, türe özel kolonları ayrı tablolarda tutar. TABLE_PER_CLASS her türe bütün kolonlarıyla ayrı bir tablo verir.

Kafam karıştı, daha basit anlat

Tek tablo: hızlı ama boşluklu. JOINED: düzenli ama birleştirmeli. Türe göre tablo: tek türe hızlı, hepsine yavaş.

Hızlı kontrolOrta

SINGLE_TABLE'da kart satırının iban kolonu neden boştur?

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

JOINED stratejisinde bir CardPayment kaydedildi. Kaç INSERT çalışır?

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

Hangisini seçmeli?

Çoğu sorgu bütün türlere birlikte bakıyorsa ve alt sınıflar birkaç kolon ekliyorsa, SINGLE_TABLE iyi bir başlangıçtır. Türler çok farklıysa ve kısıtlar önemliyse JOINED daha temizdir.

Ortak alanları yalnızca tekrar yazmamak istiyorsan, örneğin id ve createdAt, kalıtım stratejisine gerek yok. @MappedSuperclassOrtak alanları alt entity'lere kopyalayan, kendisi entity olmayan üst sınıf. Tablosu yoktur; ona ilişki kurulamaz ve üzerinden sorgu yazılamaz.Sözlükte gör → bu alanları alt sınıflara kopyalar. Ama üst sınıf veritabanında yoktur.

Kafam karıştı, daha basit anlat

En sık soracağın soruyu düşün ve onu ucuzlatan düzeni seç. Yalnızca birkaç ortak alan paylaşıyorsan @MappedSuperclass yeter.

Hızlı kontrolOrta

Yalnızca id ve createdAt alanlarını her entity'de tekrar yazmamak istiyorsun. Hangisi uygun?

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

Kendin gör

Aynı sınıflar, dört tablo düzeni

Tohum 942428

Oynat ya da adımla.

Hız
Adım 0

Şu an ne oldu?

JOINED · Müşterinin bütün ödemeleri

Java tarafı hep aynı: Payment ve üç alt sınıfı. Değişen yalnızca tabloların düzeni.

Görevler0/3

  • Bütün ödemeleri tek tablodan, JOIN'siz okuaçık

    İpucu

    Hangi stratejide her şey aynı yerde?

  • Bütün ödemeler için bir UNION ALL ürettiraçık

    İpucu

    Ortak tablo olmayan ama polimorfik sorgulanabilen strateji.

  • Kart ödemesini iki satırla kaydet; card_last4 NOT NULL kalabilsinaçık

    İpucu

    Ortak tablo ve alt tablo.

Olay günlüğü (0)

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

  1. Varsayılanla oynat. JOINED’da bütün ödemeler için üç LEFT JOIN.
  2. Stratejiyi SINGLE_TABLE yap. Aynı liste tek tablodan, JOIN’siz geldi.
  3. İşi “kart ödemesi kaydet” yap. Havale ve fatura kolonları NULL gitti.
  4. TABLE_PER_CLASS’ta listeyi dene. UNION ALL çıktı.
  5. @MappedSuperclass’ta listeyi dene. Ortak sorgu yok, her tabloya ayrı gidildi.
Hızlı kontrolOrta

TABLE_PER_CLASS stratejisinde müşterinin bütün ödemelerini listeleyen sorgu neye dönüşür?

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

Tuzaklar

Tek tabloda NOT NULL. SINGLE_TABLE’da card_last4 NOT NULL olamaz, çünkü havale satırında boştur. Kuralı uygulama tarafında ya da bir CHECK kısıtıyla korursun.

TABLE_PER_CLASS’ta IDENTITY. Id’ler bütün tablolar boyunca benzersiz olmalı. Her tablonun kendi sayacı bunu garanti edemez; bir sequence kullan.

Discriminator değerini değiştirmek. Varsayılan değer sınıfın adıdır. Sınıfı yeniden adlandırırsan eski satırlar okunamaz; değeri @DiscriminatorValue ile sabitle.

Kafam karıştı, daha basit anlat

Tek tabloda boş kolon kuralını kendin koru, ayrı tablolarda sequence kullan, tür etiketini sınıf adına bırakma.

Hızlı kontrolOrta

TABLE_PER_CLASS kullanan bir hiyerarşide id üretimi için IDENTITY neden uygun değildir?

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

Aşağıdaki örnek bir bankanın ödeme modülünden. Ödemeler tek tabloda, tür etiketi sabit ve boş kalabilen kolonlar bir CHECK ile korunuyor.

Derinleş · Ödemeler: SINGLE_TABLE, sabit etiketler ve korunan kolonlar 6 dosya · ~111 satır · ilk okumada atlayabilirsin
Proje dosyaları

src/main/java/com/bank/common/ Audited.java Ortak alanlar: id ve oluşturulma zamanı, yalnızca kodda paylaşılıyor.

src/main/java/com/bank/common/Audited.java
// Only shares fields: not an entity, no table, nothing can reference it.
@MappedSuperclass
abstract class Audited {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
protected Long id;
protected Instant createdAt = Instant.now();
public Long id() {
return id;
}
}

src/main/java/com/bank/payment/ Payment.java Payment hiyerarşisi tek tabloda; etiket kolonu açıkça adlandırılmış.

src/main/java/com/bank/payment/Payment.java
@Entity
@Table(name = "payments")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "kind", length = 16)
abstract class Payment extends Audited {
protected long customerId;
protected BigDecimal amount;
protected Payment() {
}
protected Payment(long customerId, BigDecimal amount) {
this.customerId = customerId;
this.amount = amount;
}
abstract String describe();
}

src/main/java/com/bank/payment/ CardPayment.java Alt sınıflar: her etiket sabit, sınıf adı değişse de veri okunur.

src/main/java/com/bank/payment/CardPayment.java
@Entity
@DiscriminatorValue("CARD") // fixed: renaming the class will not orphan old rows
class CardPayment extends Payment {
private String cardLast4;
protected CardPayment() {
}
CardPayment(long customerId, BigDecimal amount, String cardLast4) {
super(customerId, amount);
this.cardLast4 = Objects.requireNonNull(cardLast4);
}
@Override
String describe() {
return "Kart •••• " + cardLast4;
}
}
@Entity
@DiscriminatorValue("TRANSFER")
class TransferPayment extends Payment {
private String iban;
protected TransferPayment() {
}
TransferPayment(long customerId, BigDecimal amount, String iban) {
super(customerId, amount);
this.iban = Objects.requireNonNull(iban);
}
@Override
String describe() {
return "Havale " + iban;
}
}

src/main/resources/db/migration/ V12__payments.sql Şema: türe özel kolonlar boş kalabilir ama CHECK her türün kendi kolonunu zorunlu tutar.

src/main/resources/db/migration/V12__payments.sql
create table payments (
id bigint primary key,
kind varchar(16) not null,
created_at timestamp not null,
customer_id bigint not null,
amount numeric(19, 2) not null,
card_last4 char(4), -- nullable: empty on transfer rows
iban varchar(34), -- nullable: empty on card rows
-- each kind still has to fill its own column
constraint payments_kind_columns check (
(kind = 'CARD' and card_last4 is not null) or
(kind = 'TRANSFER' and iban is not null)
)
);
create sequence payment_seq increment by 50; -- Hibernate 6 default name for the Payment root

src/main/java/com/bank/payment/ PaymentRepository.java Polimorfik sorgu ve tek türe sorgu: ikisi de JOIN'siz.

src/main/java/com/bank/payment/PaymentRepository.java
interface PaymentRepository extends JpaRepository<Payment, Long> {
// Polymorphic: card and transfer rows come back as their own classes, no JOIN.
List<Payment> findByCustomerIdOrderByCreatedAtDesc(long customerId);
// One kind: Hibernate adds "where kind = 'CARD'" by itself.
@Query("select c from CardPayment c where c.customerId = :customerId")
List<CardPayment> findCards(long customerId);
}

src/test/java/com/bank/payment/ PaymentMappingTest.java Test: kart ödemesi, havale kolonları boş olarak tek satırda saklanıyor.

src/test/java/com/bank/payment/PaymentMappingTest.java
@DataJpaTest
class PaymentMappingTest {
@Autowired PaymentRepository payments;
@Autowired JdbcTemplate jdbc;
@Test
void aCardPaymentIsOneRowWithTheTransferColumnsEmpty() {
payments.saveAndFlush(new CardPayment(7L, new BigDecimal("250.00"), "4242"));
Map<String, Object> row = jdbc.queryForMap("select kind, card_last4, iban from payments");
assertThat(row).containsEntry("kind", "CARD").containsEntry("card_last4", "4242");
assertThat(row.get("iban")).isNull();
}
}

Kendini sına

Şimşek turu1/4

SINGLE_TABLE, Hibernate'in varsayılan kalıtım stratejisidir.

Soru 1/3İleri

SINGLE_TABLE'da @DiscriminatorValue yazılmamış. CardPayment sınıfı CardTransaction olarak yeniden adlandırıldı. Ne olur?

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

Aklında kalacak üç şey

  1. 1 SINGLE_TABLE bütün türleri tek tabloda tutar: okuma JOIN'siz ve hızlıdır, ama alt sınıf kolonları NOT NULL olamaz.
  2. 2 JOINED her alt sınıfa kendi tablosunu verir: şema temizdir, ama bütün türlere bakan sorgu her alt tabloyla birleşir. TABLE_PER_CLASS'ta aynı sorgu UNION ALL olur.
  3. 3 @MappedSuperclass yalnızca ortak alanları paylaşır. Üst sınıf bir entity değildir; ona ilişki kurulamaz ve onun üstünden sorgu yazılamaz.
Sonraki kapı Müşteri hesabını kapattı, sen de sildin. Peki neden gece raporu hâlâ onu aktif sayıyor? Soft Delete — Silinen Hesap Gerçekten Gitti mi? · 9 dk

4 kart sonraki derste seni bekliyor

0/4 kart bu dersten toplandı