MikroORM 5:Strengere, sikrere, smartere

Den neste store versjonen av MikroORM har nettopp blitt utgitt. Tittelen sier:Strengere, sikrere, smartere — hvorfor?

  • Stort forbedret typesikkerhet (f.eks. tips om fylling og delvis innlasting)
  • Automatisk skyllemodus (slik at vi aldri mister endringer i minnet)
  • Automatisk oppdatering av innlastede enheter (si farvel for å oppdatere:sant)
  • Omarbeidet skjema som varierer med støtte for automatisk nedmigrering
  • og mange mange flere...

I tilfelle du ikke vet...

Hvis du aldri har hørt om MikroORM, er det en TypeScript data-mapper ORM med Unit of Work og Identity Map. Den støtter MongoDB, MySQL, PostgreSQL og SQLite-drivere for øyeblikket. Nøkkelfunksjonene til ORM er:

  • Implisitte transaksjoner
  • ChangeSet-basert persistens
  • Identitetskart

Du kan lese hele den innledende artikkelen her (men merk at mange ting har endret seg siden det ble skrevet) eller bla gjennom dokumentene.

Rask oppsummering av 4.x-utgivelser

Før vi dykker ned i alle tingene v5, la oss oppsummere hva som skjedde i 4.x-utgivelser:

  • Resultatbuffer
  • Kontekst for automatiske transaksjoner
  • Nestede innebygde enheter og mange andre forbedringer i dette domenet
  • Bruke env vars for konfigurasjon

Men nok av historietimen, la oss snakke om fremtiden!

Forbedret typesikkerhet

La oss hoppe rett inn i den mest interessante funksjonen — streng skriving (nesten) overalt! em.create(), toJSON(), toObject(), fyll, delvis lasting og rekkefølge etter hint, alt dette (og enda mer!) er nå strengt skrevet.

La oss sjekke følgende eksempel:

Først bruker vi em.create() for å bygge hele enhetsgrafen i ett enkelt trinn. Det vil validere nyttelasten for både typer og tilleggsutstyr. Noen egenskaper på enheten kan ha standardverdier gitt via kroker eller databasefunksjoner — mens vi kanskje vil definere dem som nødvendige egenskaper, bør de fungere som valgfrie i konteksten av em.create(). For å håndtere dette problemet kan vi spesifisere slike egenskaper som bør anses som valgfrie via OptionalProps-symbolet:

Deretter laster vi inn alle forfatterenheter, fyller ut bøkene deres og bokmerkene. Alle Finn-alternativene her er strengt skrevet, dessuten kan vi til og med hoppe over fyllehintet, da det automatisk kan utledes fra feltalternativet.

Vi kan fortsatt trenge en type casting for DTO-er. Den serialiserte formen til en enhet kan være svært uforutsigbar — det er mange variabler som definerer hvordan en enhet skal serialiseres, f.eks. lastet relasjon vs referanse, egenskapsserialiserere, late egenskaper, tilpasset enhetsserialiserer/toJSON-metode, ivrig lasting, rekursjonssjekker, … Derfor anses alle relasjoner på EntityDTO-typen som lastet, dette gjøres hovedsakelig for å tillate bedre DX som om vi hadde alle relasjoner skrevet som Primær | EntityDTO (f.eks. nummer | EntityDTO), ville det være umulig å dra nytte av intellisense/autosuggestions. Tenk deg dette scenariet:

Valideringsforbedringer

I tillegg til kompileringstidsvalideringen får vi også en kjøretidsvalidering rett før innsettingsspørringer utløses, for å sikre at nødvendige egenskaper har sine verdier. Dette er hovedsakelig viktig i mongo, hvor vi ikke har valgmuligheter på skjemanivå.

Når vi prøver å bruke CLI uten å installere den lokalt, får vi også en advarsel. Og hva om vi glemmer å oppdatere noen av ORM-pakkene og ender opp med versjonsfeil og flere installerte kjernepakker? Det bekrefter vi nå også!

Omarbeidet skjemaforskjell

Skjemaforskjell har vært en av de svakeste punktene. Ofte ble det produsert flere forespørsler, eller det var til og med umulig å komme til en fullstendig synkronisert tilstand.

Schema diffing har blitt fullstendig omarbeidet for å løse alle kjente problemer, og legge til litt mer på toppen av det:

  • Forskjellige fremmednøkkelbegrensninger
  • Riktig indeksforskjell (før vi bare sammenlignet navn)
  • Egendefinerte indeksuttrykk
  • Kommentarforskjeller
  • Solonnelengdeforskjell (f.eks. numeric(10,2) eller varchar(100))
  • Endre primærnøkkeltyper
  • Skjema/navneområdeforskjell (kun Postgres)
  • Automatiske nedmigreringer (ingen SQLite-støtte ennå)
  • Sjekk støtte for begrensninger (kun Postgres)

Smartere migreringer

I produksjonsmiljøet vil vi kanskje bruke kompilerte migreringsfiler. Siden v5, bør dette fungere nesten ut av boksen, alt vi trenger å gjøre er å konfigurere migreringsbanen deretter. Utførte migrasjoner ignorerer nå filtypen, slik at vi kan bruke både node og ts-node på samme database. Dette gjøres på en bakoverkompatibel måte.

Oppretting av ny migrering vil nå automatisk lagre målskjemaet i migreringsmappen. Dette øyeblikksbildet vil deretter bli brukt hvis vi prøver å lage en ny migrering, i stedet for å bruke det gjeldende databaseskjemaet. Dette betyr at hvis vi prøver å opprette ny migrering før vi kjører de ventende, får vi fortsatt riktig skjemaforskjell (og ingen migrering vil bli opprettet hvis ingen ytterligere endringer ble gjort).

Automatisk skyllemodus

Frem til nå var spyling alltid en eksplisitt handling. Med v5 kan vi konfigurere flushing-strategien, på samme måte som hvordan JPA/dvalemodus fungerer. Vi har 3 skyllemoduser:

  • FlushMode.COMMIT – EntityManager prøver å utsette flush til den gjeldende transaksjonen er begått, selv om det også kan flushes for tidlig.
  • FlushMode.AUTO – Dette er standardmodusen, og den tømmer EntityManager bare hvis nødvendig.
  • FlushMode.ALWAYS – Tømmer EntityManager før hvert søk.

FlushMode.AUTO vil prøve å oppdage endringer på enheten vi spør etter, og flush hvis det er en overlapping:

Mer om skyllemoduser i dokumentene.

Automatisk oppdatering av innlastede enheter

Tidligere, når en enhet ble lastet inn og vi trengte å laste den inn på nytt, var det nødvendig med eksplisitt oppdatering:true i alternativene. Oppdatering av entitet hadde også en problematisk bieffekt — enhetsdataene (brukt til å beregne endringssett) ble alltid oppdatert basert på den nylig lastede enheten, og glemte derfor den forrige tilstanden (som resulterte i mulig tapte oppdateringer gjort på enheten før oppdatering).

Nå slår vi alltid sammen nylastede data med gjeldende tilstand, og når vi ser en oppdatert egenskap, beholder vi den endrede verdien i stedet. Dessuten, for em.findOne() med en primærnøkkelbetingelse, prøver vi å oppdage om det er fornuftig å laste inn en enhet på nytt, ved å sammenligne alternativene og allerede lastede egenskapsnavn. I dette trinnet tas feltene og fyllingsalternativene i betraktning for å støtte både delvis lasting og late egenskaper.

For komplekse forhold i em.findOne() og for alle spørringer via em.find(), gjør vi alltid spørringen uansett, men i stedet for å ignorere dataene i tilfelle en slik enhet ble lastet, slår vi dem sammen på samme måte.

Såmaskinpakke

MikroORM v5 har nå en ny pakke for seeding av databasen din med start- eller testdata. Den lar deg opprette enheter via samme EntityManager API som vanlig, legge til støtte for enhetsfabrikker og generere falske data via faker (den nylig utgitte fellesskapsversjonen).

Se seeder-dokumentene for flere eksempler.

Polymorfe innebygde enheter

Polymorfe embeddables lar oss definere flere klasser for en enkelt innebygd egenskap, og den riktige vil bli brukt basert på diskriminatorkolonnen, lik hvordan enkelttabellarv fungerer. Selv om dette for øyeblikket bare fungerer for innebygde enheter, vil støtte for polymorfe enheter sannsynligvis bli lagt til i en av 5.x-utgivelsene.

Sjekk ut dokumentasjonen for et fullstendig eksempel.

Det er mange andre små forbedringer i embeddables, i tillegg til at mange problemer ble tatt opp. To eksempler:

  • Støtte for mange-til-en-relasjoner (lagrer kun primærnøkkel og kan fylle ut relasjonen på samme måte som med vanlige enheter)
  • Støtte for onCreate og onUpdate egenskapsalternativer

Fylle ut late skalaregenskaper

Tidligere var den eneste måten å fylle ut en lat skalar-eiendom på under den første innlastingen av inneholdende entitet. Hvis en slik enhet allerede var lastet inn i identitetskartet (uten denne egenskapen), måtte vi oppdatere dens tilstand — og potensielt miste en del tilstand. MikroORM v5 tillater å fylle ut slike egenskaper via em.populate() også. Å gjøre det vil aldri overstyre eventuelle endringer i minnet vi kan ha gjort på enheten.

Opprette referanser uten EntityManager

Når vi ønsket å opprette en referanse, så en enhet som kun er representert av dens primærnøkkel, måtte vi alltid ha tilgang til gjeldende EntityManager-forekomst, siden en slik enhet alltid måtte administreres.

Takket være de nye hjelpemetodene på Reference-klassen, kan vi nå opprette entitetsreferanser uten tilgang til EntityManager. Dette kan være nyttig hvis du vil lage en referanse fra en intern enhetskonstruktør:

Dette vil opprette en uadministrert referanse, som deretter blir slått sammen med EntityManager når den eierende enheten blir tømt. Vær oppmerksom på at før vi tømmer det, vil ikke metoder som Reference.init() eller Reference.load() være tilgjengelige da de krever EntityManager-forekomsten.

Smartere uttrykkshjelp

Expr()-hjelperen kan brukes til å komme rundt streng skriving. Det var en identitetsfunksjon som ikke gjorde noe mer enn å returnere parameteren — alt den gjorde var å fortelle TypeScript at verdien faktisk er av en annen type (en generisk streng for å være nøyaktig).

Vi kan nå bruke hjelperen på ytterligere to måter:

  • Med en tilbakeringingssignatur for å tillate dynamisk aliasing av uttrykket
  • Med et matriseargument for å tillate sammenligning av tupler

Avventende QueryBuilder

QueryBuilder er nå klar over typen, og metodene getResult() og execute() skrives basert på den. Vi kan også avvente QueryBuilder-forekomsten direkte, som automatisk kjører QB og returnerer riktig svar. QB-forekomsten skrives nå basert på bruk av select/insert/update/delete/truncate-metoder til en av:

  • SelectQueryBuilder — venter på en rekke enheter av avkastning
  • CountQueryBuilder — avventer avkastningsnummer
  • InsertQueryBuilder — venter på avkastning QueryResult
  • UpdateQueryBuilder — venter på avkastning QueryResult
  • DeleteQueryBuilder — venter på avkastning QueryResult
  • TruncateQueryBuilder — venter på avkastning QueryResult

Jokertegnskjemaenheter

Frem til nå har vi kunnet definere entiteter i et spesifikt skjema, eller uten et skjema. Slike enheter brukte deretter skjemaet basert på ORM-konfigurasjon eller FindOptions. Dette tillot oss å lese enheter fra et spesifikt skjema, men vi manglet kraften til Unit of Work her.

Med v5 har enhetsforekomster nå skjemanavn (som en del av WrappedEntity). Administrerte enheter vil ha skjemaet fra FindOptions eller metadata. Metoder som oppretter nye entitetsforekomster som em.create() eller em.getReference() har nå en alternativparameter for å tillate innstilling av skjemaet. Vi kan også bruke wrap(entity).getSchema() og wrap(entity).setSchema().

Entiteter kan nå spesifisere jokertegnskjema via @Entity({ schema:'*' }). På den måten vil de bli ignorert i SchemaGenerator med mindre skjemaalternativet er spesifisert.

  • Hvis vi spesifiserer skjema, eksisterer enheten bare i det skjemaet
  • Hvis vi definerer *-skjema, kan enheten eksistere i et hvilket som helst skjema, alltid kontrollert av parameteren
  • Hvis vi hopper over skjemaalternativet, vil verdien bli hentet fra global ORM-konfigurasjon

Mer om dette emnet finner du her.

Dyp tilordning av enheter

Et annet svakt punkt var å tildele nye verdier til eksisterende enheter. Mens wrap().assign() opprinnelig ble designet for å oppdatere en enkelt enhet og dens verdier, ønsket mange brukere å tilordne en enhetsgraf, og også oppdatere relasjoner i ett enkelt trinn.

Med v5 har måten EntityAssigner oppdager hvilken enhet som skal oppdateres på, endret seg. Tilordning av en dyp enhetsgraf bør være mulig som standard, uten noen ekstra alternativer. Den fungerer basert på samsvarende enhets primærnøkler, så hvis du vil utstede en oppdatering for en relasjon i stedet for å opprette en ny relasjon, sørg for at du først laster den og sender dens primærnøkkel til tildelingshjelperen:

Hvis vi alltid vil oppdatere enheten, selv uten at enheten PK er til stede i data, kan vi bruke updateByPrimaryKey:false:

Flere eksempler om dette emnet finner du i dokumentene.

Eksperimentell støtte for ES-moduler

Mens MikroORM v5 fortsatt er kompilert og publisert som CommonJS, la vi til flere forbedringer som skulle tillate bruk av den med ESM-prosjekter også. Vi bruker nemlig gen-esm-wrapper-pakken for å tillate bruk av navngitte importer, og vi bruker ett ekkelt triks for å holde dynamiske importer i stedet for å kompilere dem for å kreve uttalelser — for det må vi bruke MIKRO_ORM_DYNAMIC_IMPORTS env var. Dette skulle tillate oss å bruke mappebasert oppdagelse med ES-moduler, noe som tidligere ikke var mulig.

Andre bemerkelsesverdige endringer

  • Støtte for delvis lasting (felt) for sammenføyd lastestrategi
  • AsyncLocalStorage brukes som standard i RequestContext-hjelperen
  • onLoad-hendelse (som onInit, men tillater asynkronisering og utløser bare for innlastede enheter, ikke referanser)
  • Eksport av asynkrone funksjoner fra CLI-konfigurasjon
  • Konfigurerbar aliasingstrategi for SQL
  • Tillat å gi customLogger-forekomst
  • persist alternativet inem.create() ogpersistOnCreate global konfigurasjon
  • M:N-støtte i enhetsgenerator
  • Støtte for å spesifisere transaksjonsisolasjonsnivå
  • Kontrollerer hvor betingelsen for å fylle ut hint
  • Fornyede API-dokumenter
  • og mange mange mer, se hele endringsloggen her

Sørg også for å sjekke oppgraderingsveiledningen.

Hva er det neste?

Her er en liste over ting jeg ønsker å fokusere på fremover:

  • tillat å spesifisere pivotenhet for M:N-relasjoner (slik at vi kan ha flere kolonner der, men fortsatt kartlegge den som M:N for leseformål)
  • støtte for databasevisninger (eller kanskje bare enheter som representerer SQL-uttrykk)
  • flere drivere — nemlig better-sqlite3 og kakerlakk høres ut som lavthengende frukt, gitt at knex nå støtter de opprinnelig