Postavljanje provjere jedinstvenosti u šinama je nešto što ćete na kraju činiti prilično često. Možda ste ih već dodali u većinu svojih aplikacija. Međutim, ova provjera valjanosti daje samo dobro korisničko sučelje i iskustvo. Obavještava korisnika o pogreškama koje sprečavaju zadržavanje podataka u bazi podataka.
Zašto provjera jedinstvenosti nije dovoljna
Čak i uz provjeru jedinstvenosti, neželjeni se podaci ponekad spremaju u bazu podataka. Da bismo bili jasniji, pogledajmo dolje prikazani korisnički model:
class User validates :username, presence: true, uniqueness: true end
Za provjeru valjanosti stupca korisničkog imena, rails postavlja upit bazi podataka pomoću SELECT-a kako bi provjerio postoji li korisničko ime već. Ako se dogodi, ispisuje se "Korisničko ime već postoji". Ako se to ne dogodi, pokreće INSERT upit za zadržavanje novog korisničkog imena u bazi podataka.

Kada dva korisnika istodobno izvode isti postupak, baza podataka ponekad može spremiti podatke bez obzira na ograničenje provjere valjanosti i tu dolaze ograničenja baze podataka (jedinstveni indeks).
Ako i korisnik A i korisnik B pokušavaju istodobno zadržati isto korisničko ime u bazi podataka, rails pokreće SELECT upit, ako korisničko ime već postoji, obavještava oba korisnika. Međutim, ako korisničko ime ne postoji u bazi podataka, istodobno pokreće INSERT upit za oba korisnika, kao što je prikazano na donjoj slici.

Sad kad znate zašto je jedinstveni indeks baze podataka (ograničenje baze podataka) važan, idemo u to kako ga postaviti. Prilično je jednostavno postaviti jedinstvene indekse baze podataka za bilo koji stupac ili skup stupaca u tračnicama. Međutim, neka ograničenja baze podataka u šinama mogu biti nezgodna.
Kratki uvid u postavljanje jedinstvenog indeksa za jedan ili više stupaca
Ovo je jednostavno kao pokretanje migracije. Pretpostavimo da imamo tablicu korisnika s korisničkim imenom stupca i želimo osigurati da svaki korisnik ima jedinstveno korisničko ime. Jednostavno stvorite migraciju i unesete sljedeći kôd:
add_index :users, :username, unique: true
Zatim pokrenete migraciju i to je to. Baza podataka sada osigurava da se u tablicu ne spremaju slična korisnička imena.
Za više pridruženih stupaca, pretpostavimo da imamo tablicu zahtjeva sa stupcima sender_id i receiver_id. Slično tome, jednostavno kreirate migraciju i unesete sljedeći kôd:
add_index :requests, [:sender_id, :receiver_id], unique: true
I to je to? Uh, ne tako brzo.
Problem s gore navedenom višestrukom migracijom stupaca
Problem je u tome što su ID-ovi u ovom slučaju zamjenjivi. To znači da ako imate sender_id od 1, a receiver_id od 2, tablica zahtjeva i dalje može spremiti sender_id od 2 i receiver_id od 1, iako oni već imaju zahtjev na čekanju.
Ovaj se problem često događa u autoreferencijalnoj asocijaciji. To znači da su i pošiljatelj i primatelj korisnici, a na sender_id ili receiver_id referencuje se user_id. Korisnik s user_id (sender_id) od 1 šalje zahtjev korisniku s user_id (receiver_id) od 2.
Ako primatelj ponovo pošalje drugi zahtjev, a mi mu dopustimo spremanje u bazu podataka, u tablici zahtjeva imamo dva slična zahtjeva od ista dva korisnika (pošiljatelj i primatelj || primatelj i pošiljatelj).
To je prikazano na donjoj slici:

Uobičajeni popravak
Ovaj se problem često rješava pseudo-kodom u nastavku:
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
Problem s ovim rješenjem je u tome što se receiver_id i sender_id svaki put zamijene prije spremanja u bazu podataka. Stoga će stupac receiver_id morati spremiti ID pošiljatelja i obrnuto.
Na primjer, ako korisnik s ID-om pošiljatelja 1 pošalje zahtjev korisniku s ID-om prijemnika 2, tablica zahtjeva bit će prikazana dolje:

To možda ne zvuči kao problem, ali bolje je ako vaši stupci spremaju točne podatke koje želite da spreme. To ima brojne prednosti. Na primjer, ako trebate primatelju poslati obavijest putem Receiver_id, tada ćete u stupcu baze podataka upitati za točan ID. To je već postalo zbunjujuće onog trenutka kada počnete mijenjati podatke spremljene u tablici zahtjeva.
Ispravno rješenje
Ovaj se problem može u potpunosti riješiti izravnim razgovorom s bazom podataka. U ovom slučaju objasnit ću upotrebu PostgreSQL-a. Tijekom izvođenja migracije morate osigurati da jedinstveno ograničenje provjerava i (1,2) i (2,1) u tablici zahtjeva prije spremanja.
To možete učiniti pokretanjem migracije s donjim kodom:
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
Objašnjenje koda
Nakon stvaranja datoteke za migraciju, reverzibilno je osigurati da možemo vratiti našu bazu podataka kad god moramo. To dir.up
je kôd koji treba pokrenuti kada migriramo bazu podataka i dir.down
pokrenut će se kada migriramo prema dolje ili vratimo bazu podataka.
connection.execute(%q(...))
je reći šinama da je naš kod PostgreSQL. To pomaže šinama da pokrenu naš kod kao PostgreSQL.
Budući da su naši "id" cjelobrojni brojevi, prije spremanja u bazu podataka provjeravamo jesu li najveći i najmanji (2 i 1) već u bazi podataka pomoću donjeg koda:
requests(greatest(sender_id,receiver_id), least(sender_id,receiver_id))
Zatim također provjeravamo jesu li najmanje i najveće (1 i 2) u bazi podataka koristeći:
requests(least(sender_id,receiver_id), greatest(sender_id,receiver_id))
Tablica zahtjeva tada će biti točno onakva kakvu namjeravamo, kao što je prikazano na donjoj slici:

I to je to. Sretno kodiranje!
Reference:
Edgeguides | Thoughtbot