Java >> Java tutorial >  >> Tag >> hibernate

Den bedste kortlægning til delte tekniske attributter med Hibernate

De fleste domænemodeller har nogle få tekniske egenskaber, der deles af de fleste enhedsklasser. Typiske eksempler er versionsattributten og tidsstemplet eller brugeren, der udførte den sidste opdatering eller fortsatte en enhed. I disse situationer spørger mange udviklere sig selv, hvad der er den bedste måde at modellere disse egenskaber på. Oleg gjorde det samme for nylig i kommentarerne her på bloggen, og jeg vil forklare de 2 mest populære muligheder i denne artikel.

Jeg har forberedt følgende tabelmodel for at vise dig de forskellige kortlægningsmuligheder. skakspillet og skakturneringen tabel indeholder begge kolonnerne version , lastModifiedDateTime og lastModifiedUser , som er typiske eksempler på delte tekniske kolonner.

Ved første øjekast, kortlægningen som en @MappedSuperclass og en @Embeddable synes at være en god mulighed. Men begge har deres ulemper, som jeg vil vise dig i de følgende afsnit.

@MappedSuperclass Mapping

@MappedSuperclass er en af ​​JPA's arvekortlægningsstrategier. Den fortæller din persistensudbyder at inkludere kortlægningsoplysningerne for den kortlagte superklasse i alle underklasser, der er kortlagt som entiteter. Men selve superklassen bliver ikke en enhed.

Jeg forklarer denne kortlægning mere detaljeret i indlægget Inheritance Strategies with JPA and Hibernate – The Complete Guide her på bloggen og i Inheritance Mapping-foredraget i Persistence Hub.

Her kan du se en @MappedSuperclass der definerer attributterne id , version , lastModifiedDateTime , og lastModifiedUser .

@MappedSuperclass
public class MyAbstractEntity {
    
    @Transient
    Logger  log = Logger.getLogger(this.getClass().getName());

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @Version
    protected int version;

    @UpdateTimestamp
    protected LocalDateTime lastModifiedDateTime;

    protected String lastModifiedUser;
	
	...
	
	@PrePersist
    @PreUpdate
    private void setLastModifiedUser() {
        log.info("Set lastModifiedUser");
        this.lastModifiedUser = "Thorben";
    }
}

FoI bruger en typisk primærnøgletilknytning til id attribut. Den beder Hibernate om at bruge en databasesekvens til at generere unikke primære nøgleværdier.

Jeg kommenterede versionen attribut med en @Version anmærkning. Det fortæller Hibernate at bruge denne egenskab til sin optimistiske låsealgoritme til at detektere samtidige ændringer.

@UpdateTimestamp annotation på lastModifiedDateTime attribut fortæller Hibernate at indstille dette tidsstempel, når enhver enhedsændring fjernes til databasen. Dette er en proprietær og meget behagelig måde at spore tidsstemplet for den sidste ændring.

Og jeg kommenterede setLastModifiedUser metode med livscyklus-tilbagekaldsannoteringerne @PrePersist og @PreUpdate . De beder Hibernate om at kalde denne metode, før de fortsætter eller opdaterer et enhedsobjekt. Dette gør det muligt for mig at indstille og fortsætte med lastModifiedUser attribut.

Skakturneringen klasse udvider MyAbstractEntity og arver dets attributter og deres kortlægningsdefinition.

@Entity
public class ChessTournament extends MyAbstractEntity {

    private String name;

    private LocalDate startDate;

    private LocalDate endDate;

    @Version
    private int version;

    @OneToMany
    private Set<ChessGame> games = new HashSet<>();
	
	...
}

Lad os bruge denne enhedsklasse i en simpel testcase, der fortsætter en ny ChessTournament enhed.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

ChessTournament t = new ChessTournament();
t.setName("World Chess Championship 2021");
t.setStartDate(LocalDate.of(2021, 11, 24));
t.setEndDate(LocalDate.of(2021, 12, 16));
em.persist(t);

em.flush();

assertThat(t.getLastModifiedDateTime()).isNotNull();
assertThat(t.getLastModifiedUser()).isNotNull();

em.getTransaction().commit();
em.close();

Som du kan se i log-outputtet, fungerer kortlægningen som forventet. Hibernate bruger alle attributtilknytninger defineret af @MappedSuperclass når du fortsætter Skakturneringen enhedsobjekt.

14:41:37,080 INFO  [com.thorben.janssen.TestMapping] - ==== testMappedSuperclass ====
Nov. 30, 2021 2:41:37 PM com.thorben.janssen.model.MyAbstractEntity setLastModifiedUser
INFO: Set lastModifiedUser
14:41:37,143 DEBUG [org.hibernate.SQL] - 
    select
        nextval('tournament_seq')
14:41:37,149 DEBUG [org.hibernate.SQL] - 
    select
        nextval('tournament_seq')
14:41:37,179 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        ChessTournament
        (endDate, lastModifiedDateTime, lastModifiedUser, name, startDate, version, id) 
    values
        (?, ?, ?, ?, ?, ?, ?)

Styrker og svagheder ved @MappedSuperclass-kortlægningen

Som du så i det forrige eksempel, @MappedSuperclass kortlægning giver en meget naturlig tilgang til at definere kortlægningen af ​​delte attributter. Det understøtter alle kortlægningsannoteringer, og du kan endda modellere attributter med specifik semantik, f.eks. primærnøgler og versionsattributter, på din superklasse. Som du vil se i næste afsnit, er det ikke tilfældet, hvis du bruger en @Embeddable .

Men jeg vil også påpege, at denne kortlægningstilgang føles forkert, når man ser på den fra et modelleringsperspektiv.

Skakturneringen er ikke en AbstractEntity . Den deler kun attributter, der er defineret af den pågældende klasse. Når du analyserer din applikations domæne, noget som en AbstractEntity vil ikke komme op i analyseprocessen, fordi den ikke eksisterer i den virkelige verden.

Det er også ret usandsynligt, at vi aktivt vil bruge AbstractEntity i forretningskoden for at implementere enhver del af vores forretningslogik.

Den eneste grund til at introducere AbstractEntity  som en superklasse er at definere kortlægningen af ​​alle delte tekniske attributter på 1 sted. Baseret på objektorienterede designprincipper bør du bedre bruge komposition i stedet for arv for at opnå dette.

@Embeddable Mapping

@Embeddable kortlægning anvender begrebet sammensætning til domænemodellen og kan betragtes som den bedre tilgang. Men det introducerer nogle begrænsninger for dine kortlægningsdefinitioner.

Selve det indlejrbare objekt har ingen identitet i din persistenskontekst. Alle dens attributter og tilknytninger bliver en del af enheden og bliver knyttet til enhedens databasetabel. Du kan lære mere om denne kortlægning i foredraget om @Embeddables i Persistence Hub.

Her kan du se en @Embeddable kortlægning baseret på denne artikels eksempel. I modsætning til MyAbstractEntityMetaData  klasse definerer ikke id  og version egenskaber. Den simple grund til det er, at Hibernate ikke tillader dig at definere disse attributter på en @Embeddable . Du skal definere den primære nøgle og versionsattributterne på selve enhedsklassen.

@Embeddable
public class MetaData {
    
    @Transient
    Logger  log = Logger.getLogger(this.getClass().getName());
    
    private LocalDateTime lastModifiedDateTime;

    private String lastModifiedUser;

	...

    @PrePersist
    @PreUpdate
    private void setLastModified() {
        log.info("Set lastModifiedUser and lastModifiedDateTime");
        this.lastModifiedUser = "Thorben";
		this.lastModifiedDateTime = LocalDateTime.now();
    }
}

Jeg annoterer heller ikke lastModifiedDateTime attribut med et @UpdateTimestamp anmærkning. For hvis jeg gør det, kaster Hibernate 5 og 6 en NotYetImplementedException under udrulning.

jakarta.persistence.PersistenceException:[PersistenceUnit:my-persistence-unit] Unable to build Hibernate SessionFactory
...
Caused by: org.hibernate.cfg.NotYetImplementedException: Still need to wire in composite in-memory value generation

Men i stedet for at bruge @UpdateTimestamp annotering, kan du indstille lastModifiedDateTime attribut i livscyklus-tilbagekaldsmetoden setLastModified .

Efter du har modelleret @Embeddable , kan du bruge den som en attributtype i din enhedsklasse. Her kan du se Skakspillet enhed. Dens metadata attribut er af typen MetaData, og jeg annoterede den med en @Embedded anmærkning. Det fortæller Hibernate at inkludere alle attributter defineret af @Embeddable ind i Skakspillet enhed.

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

    private LocalDate date;

    private int round;

    @ManyToOne
    private ChessTournament chessTournament;

    @Embedded
    private MetaData metaData;
	
    @Version
    private int version;

    ...
}

Lad os bruge denne kortlægning i en simpel testcase, der fortsætter et nyt Skakspil enhedsobjekt.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

ChessGame g = new ChessGame();
g.setDate(LocalDate.of(2021, 11, 26));
g.setRound(1);
g.setMetaData(new MetaData());
em.persist(g);

assertThat(g.getMetaData().getLastModifiedDateTime()).isNotNull();
assertThat(g.getMetaData().getLastModifiedUser()).isNotNull();

em.getTransaction().commit();
em.close();

Som du kan se i log-outputtet, fungerede kortlægningen som forventet. Alle attributter for MetaData embeddable blev en del af ChessGame enhed, og Hibernate kortlagde dem til kolonner i skakspillet tabel.

15:04:51,692 INFO  [com.thorben.janssen.TestMapping] - ==== testEmbeddable ====
15:04:51,736 INFO  [com.thorben.janssen.model.MetaData] - Set lastModifiedUser and lastModifiedDateTime
15:04:51,742 DEBUG [org.hibernate.SQL] - 
    select
        nextval('ChessGame_SEQ')
15:04:51,749 DEBUG [org.hibernate.SQL] - 
    select
        nextval('ChessGame_SEQ')
15:04:51,807 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        ChessGame
        (chessTournament_id, date, lastModifiedDateTime, lastModifiedUser, round, version, id) 
    values
        (?, ?, ?, ?, ?, ?, ?)

Styrker og svagheder ved @Embeddable kortlægning

Som forklaret tidligere er @Embeddable kortlægning anvender begrebet komposition og er den bedre tilgang fra et objektorienteret designperspektiv.

Men som du så i eksemplet, introducerer det også flere kortlægningsrestriktioner. Selvom du kan bruge en @Embeddable med alle dens attributter som et @EmbeddedId , du kan ikke bruge det til kun at modellere én primær nøgle og flere andre attributter.

Du kan heller ikke bruge @Version eller @UpdateTimestamp annoteringer til at kortlægge attributterne for en indlejret klasse. Hibernate understøtter dem begge kun for enhedsklasser.

Hvis du ikke har brug for disse specifikke annoteringer, f.eks. fordi du kan levere al den nødvendige logik i en livscyklus-tilbagekaldsmetode, en @Embeddable er en fantastisk måde at modellere delte tekniske attributter på.

Oversigt

Næsten alle domænemodeller har tekniske egenskaber, der er en del af næsten alle enhedsklasser. Du kan selvfølgelig kortlægge dem på hver enhedsklasse individuelt. Men de fleste hold beslutter at bruge en @MappedSuperclass  kortlægning i stedet for. Selvom dette ofte føles som en forkert designbeslutning, er det den mere fleksible og kraftfulde kortlægning. Som du så i eksemplerne, er kortlægningen en @MappedSuperclass indfører ingen begrænsninger. Du kan bruge alle kortlægningsfunktioner, som du ellers ville bruge på en enhedsklasse.

Fra et objektorienteret designperspektiv, kortlægning af dette som en @Embeddable er den bedre tilgang. Den bruger begrebet sammensætning i stedet for arv. Men det introducerer et par kortlægningsbegrænsninger, der muligvis kræver et par løsninger.

Generelt anbefaler jeg at prøve @Embeddable kortlægning først. Det er den renere tilgang, og den fungerer rigtig godt, så længe du modellerer versionen eller primære nøgleattributter på dine enhedsklasser. Hvis du ikke ønsker at gøre det, skal du bruge en @MappedSuperclass kortlægning.


Java tag