Középhaladó Olvasási idő: ~11 perc

Entity Mapping

@Entity, relationship mapping, inheritance, @Embeddable, @Converter, equals/hashCode

Entity Mapping

A JPA entity mapping a Java objektumok és relációs adatbázis táblák közötti leképezés szabályait határozza meg: annotációk, kapcsolatok, öröklődési stratégiák és értékobjektumok.


1. Definíció

  • Mi ez? — Az entity mapping az a folyamat, amelyben Java osztályokat JPA annotációkkal látunk el, hogy a Hibernate (vagy más JPA provider) automatikusan szinkronizálja őket relációs adatbázis táblákkal. Ez az ORM (Object-Relational Mapping) alapja.
  • Miért létezik? — Manuális SQL írás és ResultSet feldolgozás helyett a fejlesztő Java objektumokkal dolgozik, a JPA pedig automatikusan generálja az SQL-t. Ez csökkenti a boilerplate kódot és lehetővé teszi az adatbázis-portabilitást.
  • Hol helyezkedik el? — Az entity mapping a Spring Data JPA stack legalsó rétege. A spring-boot-starter-data-jpa starter automatikusan konfigurálja a Hibernate-et, a DataSource-t és az EntityManagerFactory-t.
Java Object ←→ JPA Annotations ←→ Hibernate ←→ SQL DDL/DML ←→ Database
   @Entity         @Column            ORM          CREATE TABLE    users

2. Alapfogalmak

Az @Entity annotáció és követelmények

Egy JPA entity az alábbi követelményeknek kell megfeleljen:

  • @Entity annotáció az osztályon
  • @Id annotációval jelölt elsődleges kulcs
  • No-arg konstruktor (lehet protected)
  • Az osztály nem lehet final (CGLIB proxy-k miatt)
  • A persistent mezők nem lehetnek final
@Entity
@Table(name = "users", uniqueConstraints = {
    @UniqueConstraint(columnNames = {"email"})
})
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
    @SequenceGenerator(name = "user_seq", sequenceName = "user_sequence", allocationSize = 50)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(unique = true, nullable = false)
    private String email;

    @Enumerated(EnumType.STRING)
    @Column(length = 20)
    private UserStatus status;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createdAt;

    protected User() {} // JPA számára
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

@Column testreszabás

Attribútum Alapértelmezés Leírás
name mező neve Oszlop neve a DB-ben
nullable true NULL megengedett
unique false Egyedi constraint
length 255 VARCHAR hossz
precision/scale 0/0 BigDecimal-hez
columnDefinition Nyers DDL (pl. "TEXT")
insertable/updatable true Részt vesz-e INSERT/UPDATE-ben

ID generálási stratégiák

Stratégia Működés Batch insert Megjegyzés
IDENTITY DB auto-increment Egyszerű, de nem batch-elhető
SEQUENCE DB sequence ✅ (allocationSize) Best practice relációs DB-nél
TABLE Szekvencia tábla Lassú lock-ok miatt kerülendő
UUID Alkalmazás generálja Nincs DB roundtrip
// SEQUENCE — ajánlott production-ben
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq")
@SequenceGenerator(name = "order_seq", sequenceName = "order_seq", allocationSize = 50)
private Long id;

// UUID — elosztott rendszerekhez
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

Relationship típusok

Annotáció Kardinalitás Alapértelmezett fetch
@OneToOne 1:1 EAGER
@OneToMany 1:N LAZY
@ManyToOne N:1 EAGER
@ManyToMany N:M LAZY

⚠️ Golden rule: A @ManyToOne és @OneToOne fetch-et mindig állítsd LAZY-re explicit módon!


3. Gyakorlati használat

Bidirectionális @OneToMany / @ManyToOne

@Entity
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id", nullable = false)
    private Customer customer;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    // ✅ Kényelmi metódusok — KÖTELEZŐ bidirectionális kapcsolatnál
    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
    }

    public void removeItem(OrderItem item) {
        items.remove(item);
        item.setOrder(null);
    }
}

@Entity
public class OrderItem {
    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id", nullable = false)
    private Order order;

    private String productName;
    private int quantity;
}

@ManyToMany

@Entity
public class Student {
    @Id @GeneratedValue
    private Long id;

    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();
}

@Entity
public class Course {
    @Id @GeneratedValue
    private Long id;

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();
}

⚠️ @ManyToMany-nál mindig Set-et használj List helyett — a List problémás lehet Hibernate-ben (extra DELETE + INSERT az összekötő táblában).

@OneToOne megosztott primary key-jel

@Entity
public class UserProfile {
    @Id
    private Long id;  // nem generált, a User id-jával egyezik

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId  // a User PK-ját használja PK-ként
    @JoinColumn(name = "id")
    private User user;

    private String bio;
    private String avatarUrl;
}

4. Kód példák

Inheritance mapping — SINGLE_TABLE

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type", discriminatorType = DiscriminatorType.STRING)
public abstract class Payment {
    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    private BigDecimal amount;
    private LocalDateTime createdAt;
}

@Entity
@DiscriminatorValue("CARD")
public class CardPayment extends Payment {
    @Column(length = 4)
    private String lastFourDigits;
    private String cardBrand;
}

@Entity
@DiscriminatorValue("BANK_TRANSFER")
public class BankTransfer extends Payment {
    private String iban;
    private String bankName;
}

Inheritance mapping — JOINED

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Vehicle {
    @Id @GeneratedValue
    private Long id;
    private String manufacturer;
    private int year;
}

@Entity
public class Car extends Vehicle {
    private int numberOfDoors;
    private String fuelType;
}

@Entity
public class Truck extends Vehicle {
    private double payloadCapacity;
    private int numberOfAxles;
}
Stratégia Polimorf query NULL oszlopok Séma
SINGLE_TABLE ✅ Leggyorsabb ❌ Sok 1 tábla
JOINED ⚠️ JOIN szükséges ✅ Nincs N+1 tábla
TABLE_PER_CLASS ❌ UNION ALL ✅ Nincs N tábla

@Embeddable — Value Object leképezés

@Embeddable
public class Address {
    private String street;
    private String city;
    @Column(name = "zip_code")
    private String zipCode;
    private String country;

    protected Address() {}
    public Address(String street, String city, String zipCode, String country) {
        this.street = street;
        this.city = city;
        this.zipCode = zipCode;
        this.country = country;
    }
}

@Entity
public class Customer {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @Embedded
    private Address shippingAddress;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "street", column = @Column(name = "billing_street")),
        @AttributeOverride(name = "city", column = @Column(name = "billing_city")),
        @AttributeOverride(name = "zipCode", column = @Column(name = "billing_zip")),
        @AttributeOverride(name = "country", column = @Column(name = "billing_country"))
    })
    private Address billingAddress;
}

@Converter — egyedi típus leképezés

@Converter(autoApply = true)
public class MoneyConverter implements AttributeConverter<Money, Long> {

    @Override
    public Long convertToDatabaseColumn(Money money) {
        return money == null ? null : money.getAmountInCents();
    }

    @Override
    public Money convertToEntityAttribute(Long cents) {
        return cents == null ? null : Money.ofCents(cents);
    }
}

// Használat — a converter automatikusan alkalmazódik minden Money típusú mezőre
@Entity
public class Product {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private Money price;  // autoApply=true → automatikus konverzió
}

5. Trade-offok

Szempont Előny Hátrány
ORM mapping Automatikus SQL generálás, kevesebb boilerplate A generált SQL nem mindig optimális
Relationship Természetes Java objektum navigáció Cascade, orphanRemoval bonyolulttá válhat
Inheritance Polimorfizmus DB szinten Minden stratégiának kompromisszumai vannak
@Embeddable DDD value object → tiszta domain modell @AttributeOverride bőbeszédű több embed-nél
@Converter Egyedi típusok transzparens leképezése Debug-olásnál nem látszik a konverzió
DB-agnosztikus Dialect-váltással adatbázis portábilis DB-specifikus feature-ök (JSON, array) elvesznek

Mikor @Embeddable vs külön @Entity?

  • @Embeddable: Nincs saját identity (value object) — Address, Money, DateRange
  • Külön @Entity: Saját lifecycle és ID szükséges — Category, Tag

6. Gyakori hibák

❌ mappedBy elfelejtése bidirectionálisnál

// ROSSZ — mindkét oldal owner → 2 foreign key tábla
@Entity
public class Order {
    @OneToMany(cascade = CascadeType.ALL)  // nincs mappedBy!
    private List<OrderItem> items;
}

// JÓ — az inverz oldal jelzi a mappedBy-t
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items;

❌ Bidirectionális szinkronizálás hiánya

// ROSSZ — csak az egyik oldalt állítjuk
order.getItems().add(item);
// item.order NULL → DB-ben a foreign key NULL!

// JÓ — helper metódus mindkét oldalt frissíti
public void addItem(OrderItem item) {
    items.add(item);
    item.setOrder(this);
}

❌ equals/hashCode generált ID-val

// ROSSZ — new entity id=null, Set/Map-ban problémás
@Override
public boolean equals(Object o) {
    return id != null && id.equals(((User) o).id);
}

// JÓ — business key (natural key) használata
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof User other)) return false;
    return email != null && email.equals(other.email);
}

@Override
public int hashCode() {
    return email != null ? email.hashCode() : 0;
}

❌ EAGER fetch @ManyToOne-on hagyva

// ROSSZ — @ManyToOne EAGER alapértelmezett
@ManyToOne
private Customer customer;  // Mindig betöltődik!

// JÓ — explicit LAZY
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;

❌ List @ManyToMany-nál Set helyett

A Hibernate a List-es @ManyToMany-nál a teljes join táblát törli és újraírja elem módosításkor. Set esetén hatékonyabb: egyedi DELETE + INSERT.

❌ CascadeType.ALL nem kívánt helyen

// ROSSZ — ha Customer-t törölsz, MINDEN Order is törlődik!
@ManyToOne(cascade = CascadeType.ALL)
private Customer customer;

// JÓ — cascade csak a „parent → child" irányban
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items;

7. Mélyebb összefüggések

Access strategy: field vs property

A JPA két helyen keresheti az annotációkat:

  • Field access (alapértelmezett ha @Id a mezőn van): közvetlenül reflection-nel olvassa a mezőket
  • Property access (ha @Id a getter-en van): getter/setter-en keresztül
@Entity
@Access(AccessType.FIELD)  // Explicit deklaráció
public class User {
    @Id
    private Long id;  // field access — a Hibernate a mezőt olvassa
}

Best practice: field access — nincs szükség getter/setter-re minden mezőhöz, a proxy is jobban működik.

@NaturalId — másodlagos keresőkulcs

@Entity
public class Country {
    @Id @GeneratedValue
    private Long id;

    @NaturalId
    @Column(nullable = false, unique = true, length = 2)
    private String code;  // "HU", "US"
}

// Használat:
Country hu = session.byNaturalId(Country.class)
    .using("code", "HU")
    .load();

A @NaturalId Hibernate-specifikus. Az L1 és L2 cache is támogatja, így hatékonyabb, mint sima findByCode().

@Formula — számított mező

@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    @Formula("(SELECT SUM(i.price * i.quantity) FROM order_items i WHERE i.order_id = id)")
    private BigDecimal totalAmount;
}

A @Formula minden SELECT-nél lefut (subquery), nem perzisztálódik. Alternatíva: @PostLoad callback-ben számolni.

Hibernate-specifikus annotációk

Annotáció Leírás
@DynamicUpdate Csak a módosult oszlopokat UPDATE-eli
@DynamicInsert Csak a nem-null oszlopokat INSERT-eli
@BatchSize(size=N) Lazy collection batch-elt betöltése
@Where(clause="...") Soft delete szűrő
@Filter Dinamikus, paraméterezett szűrő
@SQLRestriction Hibernate 6.3+ (régi @Where helyett)

8. Interjúkérdések

K: Mi a különbség a SINGLE_TABLE, JOINED és TABLE_PER_CLASS inheritance stratégiák között? V: A SINGLE_TABLE egyetlen táblába tesz mindent discriminator oszloppal — leggyorsabb, de sok NULL. A JOINED normalizált: minden osztálynak saját táblája, SELECT-nél JOIN kell. A TABLE_PER_CLASS osztályonként külön tábla, polimorf query UNION ALL-t igényel.

K: Miért fontos a bidirectionális helper metódus? V: A JPA csak az owner oldal foreign key-jét kezeli. Ha csak az inverz oldalon (mappedBy) adunk hozzá elemet, a DB-ben NULL lesz a foreign key. A helper metódus mindkét oldalt szinkronizálja.

K: Hogyan implementálnád az equals/hashCode-ot JPA entity-n? V: Business key (pl. email, ISBN) alapján, nem a generált ID alapján. Az ID persist előtt null, ami problémát okoz Set-ben és Map-ben. A @NaturalId Hibernate-specifikus megoldást ad.

K: Mi az @Embeddable és mikor használod @Entity helyett? V: Az @Embeddable value object-ekhez való (Address, Money) — nincs saját ID, a szülő táblába ágyazódik. Ha az objektumnak saját lifecycle-ra és ID-ra van szüksége, külön @Entity.

K: Miért nem batch-elhető az IDENTITY GenerationType? V: Mert az adatbázis auto-increment-je csak INSERT után adja vissza az ID-t. A Hibernate-nek minden INSERT-nél meg kell várnia az ID-t, így nem tudja összefogni batch-be.

K: Mi a @Converter és mikor használd? V: Az AttributeConverter interface egyedi Java típusokat (Money, enum, JSON) leképez DB oszloptípusra. Az autoApply=true automatikusan alkalmazza minden azonos típusú mezőre.


9. Szószedet

Fogalom Jelentés
Entity JPA-val felügyelt Java osztály, adatbázis sornak felel meg
@Table Az entity táblanév-hozzárendelése
@Column Oszlop-szintű testreszabás (név, nullable, unique, length)
@Id Elsődleges kulcs jelölő
@GeneratedValue ID generálási stratégia
@OneToMany Egy-a-többhöz kapcsolat (default LAZY)
@ManyToOne Több-az-egyhez kapcsolat (default EAGER!)
@ManyToMany Több-a-többhöz, join tábla szükséges
mappedBy Az inverz oldal jelzője bidirectionális kapcsolatban
@Embeddable Value object, saját identity nélkül
@Converter Egyedi típuskonverzió DB oszlop és Java típus között
@Inheritance Öröklődési stratégia (SINGLE_TABLE, JOINED, TABLE_PER_CLASS)
@DiscriminatorColumn A SINGLE_TABLE típusjelölő oszlopa
@NaturalId Hibernate-specifikus üzleti kulcs annotáció

10. Gyorsreferencia

ENTITY KÖVETELMÉNYEK:
  @Entity + @Id + no-arg constructor
  Nem final osztály, nem final persistent mezők

ID GENERÁLÁS:
  IDENTITY    auto-increment   ❌ nem batch-elhető
  SEQUENCE    DB sequence      ✅ allocationSize=50
  UUID        alkalmazás gen.  ✅ nincs DB roundtrip

RELATIONSHIP:
  @OneToOne    1:1   default EAGER → állítsd LAZY-re
  @ManyToOne   N:1   default EAGER → állítsd LAZY-re  
  @OneToMany   1:N   default LAZY  ✅
  @ManyToMany  N:M   default LAZY  ✅ → Set használata

BIDIRECTIONÁLIS SZABÁLYOK:
  owner oldal    → @JoinColumn (FK itt van)
  inverse oldal  → mappedBy = "ownerField"
  helper metódus → mindkét oldalt szinkronizálni

INHERITANCE:
  SINGLE_TABLE   1 tábla + discriminator (leggyorsabb)
  JOINED         N+1 tábla (legtisztább séma)
  TABLE_PER_CLASS  N tábla (polimorf query lassú)

@EMBEDDABLE:
  Value object, nincs ID
  Szülő táblájába ágyazódik
  @AttributeOverride több embed-nél

EQUALS/HASHCODE:
  ✅ Business key (email, code, ISBN)
  ❌ Generált @Id (null persist előtt)

🎮 Játékok

10 kérdés