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 MyAbstractEntity , MetaData 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.