Rails: Sådan indstilles en unik udskiftelig indeksbegrænsning

Indstilling af unikke validering i skinner er noget, du ender med at gøre ganske ofte. Måske har du endda allerede tilføjet dem til de fleste af dine apps. Denne validering giver dog kun en god brugergrænseflade og oplevelse. Det informerer brugeren om de fejl, der forhindrer data i at blive vedvarende i databasen.

Hvorfor validering af unikhed ikke er nok

Selv med unikke validering gemmes uønskede data undertiden i databasen. Af hensyn til klarheden skal vi se på en brugermodel vist nedenfor:

class User validates :username, presence: true, uniqueness: true end 

For at validere brugernavnkolonnen forespørger rails databasen ved hjælp af SELECT for at se, om brugernavnet allerede findes. Hvis den gør det, udskrives "Brugernavn findes allerede". Hvis den ikke gør det, kører den en INSERT-forespørgsel for at fastholde det nye brugernavn i databasen.

Når to brugere kører den samme proces på samme tid, kan databasen nogle gange gemme dataene uanset valideringsbegrænsningen, og det er her, databasebegrænsninger (unikt indeks) kommer ind.

Hvis bruger A og bruger B begge forsøger at opretholde det samme brugernavn i databasen på samme tid, kører rails SELECT-forespørgslen, hvis brugernavnet allerede findes, informerer det begge brugere. Men hvis brugernavnet ikke findes i databasen, kører det INSERT-forespørgslen for begge brugere samtidigt som vist på billedet nedenfor.

Nu hvor du ved, hvorfor det unikke databaseindeks (databasebegrænsning) er vigtigt, lad os komme ind på, hvordan du indstiller det. Det er ret nemt at indstille database (r) unikke indeks (er) for enhver kolonne eller et sæt kolonner i skinner. Imidlertid kan nogle databasebegrænsninger i skinner være vanskelige.

Et hurtigt kig på indstilling af et unikt indeks for en eller flere kolonner

Dette er lige så simpelt som at køre en migration. Lad os antage, at vi har en brugertabel med kolonne-brugernavn, og vi vil sikre, at hver bruger har et unikt brugernavn. Du opretter simpelthen en migration og indtaster følgende kode:

add_index :users, :username, unique: true 

Derefter kører du migrationen, og det er det. Databasen sikrer nu, at der ikke gemmes nogen lignende brugernavne i tabellen.

Lad os antage, at vi har en anmodningstabel med kolonner sender_id og receiver_id for flere tilknyttede kolonner. På samme måde opretter du simpelthen en migration og indtaster følgende kode:

add_index :requests, [:sender_id, :receiver_id], unique: true 

Og det er det? Uh, ikke så hurtigt.

Problemet med migrering af flere kolonner ovenfor

Problemet er, at id'erne i dette tilfælde er udskiftelige. Dette betyder, at hvis du har en sender_id på 1 og modtager_id på 2, kan anmodningstabellen stadig gemme en sender_id på 2 og modtager_id på 1, selvom de allerede har en ventende anmodning.

Dette problem sker ofte i en selvhenvisende forening. Dette betyder, at både afsender og modtager er brugere, og der henvises til sender_id eller receiver_id fra user_id. En bruger med user_id (sender_id) på 1 sender en anmodning til en bruger med user_id (receiver_id) på 2.

Hvis modtageren sender en anden anmodning igen, og vi tillader den at gemme i databasen, har vi to lignende anmodninger fra de samme to brugere (afsender og modtager || modtager og afsender) i anmodningstabellen.

Dette er illustreret i nedenstående billede:

Den almindelige løsning

Dette problem løses ofte med pseudokoden nedenfor:

def force_record_conflict # 1. Return if there is an already existing request from the sender to receiver # 2. If not then swap the sender and receiver end 

Problemet med denne løsning er, at receiver_id og sender_id byttes hver gang, før de gemmes i databasen. Derfor skal receiver_id-kolonnen gemme sender_id og omvendt.

For eksempel, hvis en bruger med sender_id på 1 sender en anmodning til en bruger med modtager_id på 2, vil anmodningstabellen være som vist nedenfor:

Dette lyder måske ikke som et problem, men det er bedre, hvis dine kolonner gemmer de nøjagtige data, du vil have dem til at gemme. Dette har adskillige fordele. For eksempel, hvis du har brug for at sende en underretning til modtageren via receiver_id, så spørger du databasen for det nøjagtige id fra receiver_id-kolonnen. Dette blev allerede mere forvirrende i det øjeblik, du begynder at skifte de data, der er gemt i din anmodningstabel.

Den rette løsning

Dette problem kan løses fuldstændigt ved at tale direkte til databasen. I dette tilfælde forklarer jeg ved hjælp af PostgreSQL. Når du kører migrationen, skal du sikre dig, at den unikke begrænsning kontrollerer både (1,2) og (2,1) i anmodningstabellen, før du gemmer.

Du kan gøre det ved at køre en migration med nedenstående kode:

class AddInterchangableUniqueIndexToRequests < ActiveRecord::Migration[5.2] def change reversible do |dir| dir.up do connection.execute(%q( create unique index index_requests_on_interchangable_sender_id_and_receiver_id on requests(greatest(sender_id,receiver_id), least(sender_id,receiver_id)); create unique index index_requests_on_interchangable_receiver_id_and_sender_id on requests(least(sender_id,receiver_id), greatest(sender_id,receiver_id)); )) end dir.down do connection.execute(%q( drop index index_requests_on_interchangable_sender_id_and_receiver_id; drop index index_requests_on_interchangable_receiver_id_and_sender_id; )) end end end end 

Kode forklaring

Efter oprettelse af migreringsfilen er det reversibelt at sikre, at vi kan vende tilbage til vores database, når vi har brug for det. Det dir.uper koden, der skal køres, når vi migrerer vores database, og den dir.downkører, når vi migrerer ned eller vender tilbage til vores database.

connection.execute(%q(...))er at fortælle skinner, at vores kode er PostgreSQL. Dette hjælper skinner med at køre vores kode som PostgreSQL.

Da vores “id'er” er heltal, kontrollerer vi, før de gemmes i databasen, om det største og mindste (2 og 1) allerede er i databasen ved hjælp af nedenstående kode:

requests(greatest(sender_id,receiver_id), least(sender_id,receiver_id)) 

Derefter kontrollerer vi også, om den mindste og største (1 og 2) er i databasen ved hjælp af:

requests(least(sender_id,receiver_id), greatest(sender_id,receiver_id)) 

Anmodningstabellen vil så være nøjagtigt, hvordan vi har til hensigt som vist i billedet nedenfor:

Og det er det. God kodning!

Referencer:

Edgeguides | Tankebot