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-jpastarter 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:
@Entityannotáció az osztályon@Idannotá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
@Ida mezőn van): közvetlenül reflection-nel olvassa a mezőket - Property access (ha
@Ida 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