Setarea validării unicității în șine este ceva ce veți ajunge destul de des. Poate că chiar le-ați adăugat deja la majoritatea aplicațiilor dvs. Cu toate acestea, această validare oferă doar o bună interfață de utilizator și experiență. Acesta informează utilizatorul despre erorile care împiedică persistarea datelor în baza de date.

De ce validarea unicității nu este suficientă

Chiar și cu validarea unicității, datele nedorite sunt uneori salvate în baza de date. Pentru claritate, să aruncăm o privire la un model de utilizator prezentat mai jos:

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

Pentru a valida coloana nume de utilizator, rails interogă baza de date folosind SELECT pentru a vedea dacă numele de utilizator există deja. În caz contrar, imprimă „Nume de utilizator există deja”. În caz contrar, rulează o interogare INSERT pentru a păstra noul nume de utilizator în baza de date.

ad-banner
Sine Cum se seteaza constrangerea unica a indexului interschimbabil

Când doi utilizatori rulează același proces în același timp, baza de date poate salva uneori datele, indiferent de constrângerea de validare și acolo intră constrângerile bazei de date (index unic).

Dacă utilizatorul A și utilizatorul B încearcă să păstreze același nume de utilizator în baza de date în același timp, rails execută interogarea SELECT, dacă numele de utilizator există deja, îi informează pe ambii utilizatori. Cu toate acestea, dacă numele de utilizator nu există în baza de date, acesta execută interogarea INSERT pentru ambii utilizatori simultan, așa cum se arată în imaginea de mai jos.

1611036724 887 Sine Cum se seteaza constrangerea unica a indexului interschimbabil

Acum, că știți de ce este important indexul unic al bazei de date (constrângerea bazei de date), haideți să aflăm cum să-l setați. Este destul de ușor să setați indexuri unice ale bazei de date pentru orice coloană sau set de coloane în șine. Cu toate acestea, unele constrângeri de baze de date în șine pot fi dificile.

O privire rapidă asupra setării unui index unic pentru una sau mai multe coloane

Acest lucru este la fel de simplu ca executarea unei migrații. Să presupunem că avem un tabel de utilizatori cu numele de utilizator coloană și vrem să ne asigurăm că fiecare utilizator are un nume de utilizator unic. Pur și simplu creați o migrare și introduceți următorul cod:

add_index :users, :username, unique: true

Apoi rulați migrarea și gata. Baza de date asigură acum că niciun nume de utilizator similar nu este salvat în tabel.

Pentru mai multe coloane asociate, să presupunem că avem un tabel de cereri cu coloane sender_id și receiver_id. În mod similar, pur și simplu creați o migrare și introduceți următorul cod:

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

Si asta e? Uh oh, nu atât de repede.

Problema cu migrarea mai multor coloane de mai sus

Problema este că ID-urile, în acest caz, sunt interschimbabile. Aceasta înseamnă că, dacă aveți un sender_id de 1 și receiver_id de 2, tabelul de cereri poate salva în continuare un sender_id de 2 și receiver_id de 1, chiar dacă acestea au deja o cerere în așteptare.

Această problemă se întâmplă adesea într-o asociație auto-referențială. Aceasta înseamnă că atât expeditorul, cât și destinatarul sunt utilizatori, iar sender_id sau receiver_id este menționat din user_id. Un utilizator cu user_id (sender_id) de 1 trimite o cerere către un utilizator cu user_id (receiver_id) de 2.

Dacă receptorul trimite din nou o altă cerere și îi permitem să se salveze în baza de date, atunci avem două solicitări similare de la aceiași doi utilizatori (expeditor și receptor || receptor și expeditor) în tabelul de cereri.

Acest lucru este ilustrat în imaginea de mai jos:

1611036725 215 Sine Cum se seteaza constrangerea unica a indexului interschimbabil

Solutia comuna

Această problemă este adesea rezolvată cu pseudo-codul de mai jos:

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

Problema cu această soluție este că receiver_id și sender_id sunt schimbate de fiecare dată înainte de a le salva în baza de date. Prin urmare, coloana receiver_id va trebui să salveze sender_id și invers.

De exemplu, dacă un utilizator cu sender_id de 1 trimite o cerere către un utilizator cu receiver_id de 2, tabelul de solicitări va fi așa cum se arată mai jos:

1611036725 533 Sine Cum se seteaza constrangerea unica a indexului interschimbabil

Este posibil să nu pară o problemă, dar este mai bine dacă coloanele dvs. salvează datele exacte pe care doriți să le salveze. Acest lucru are numeroase avantaje. De exemplu, dacă trebuie să trimiteți o notificare către receptor prin receptor_id, atunci veți interoga baza de date pentru identificarea exactă din coloana receptor_id. Acest lucru a devenit deja mai confuz în momentul în care începeți să comutați datele salvate în tabelul de solicitări.

Remedierea corectă

Această problemă poate fi rezolvată în întregime vorbind direct cu baza de date. În acest caz, vă voi explica utilizarea PostgreSQL. Când rulați migrarea, trebuie să vă asigurați că constrângerea unică verifică atât (1,2), cât și (2,1) în tabelul de solicitări înainte de a salva.

Puteți face acest lucru executând o migrare cu codul de mai jos:

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

Explicarea codului

După crearea fișierului de migrare, reversibilul este să ne asigurăm că putem reveni la baza noastră de date ori de câte ori trebuie. dir.up este codul care trebuie să ruleze atunci când ne migrăm baza de date și dir.down va rula când vom migra în jos sau vom reveni la baza noastră de date.

connection.execute(%q(...)) este de a spune șinelor că codul nostru este PostgreSQL. Acest lucru ajută șinele să ruleze codul nostru ca PostgreSQL.

Deoarece „id-urile” noastre sunt întregi, înainte de a le salva în baza de date, verificăm dacă cele mai mari și cele mai mici (2 și 1) sunt deja în baza de date folosind codul de mai jos:

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

Apoi verificăm, de asemenea, dacă cel mai mic și cel mai mare (1 și 2) sunt în baza de date folosind:

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

Tabelul de solicitări va fi apoi exact cum intenționăm așa cum se arată în imaginea de mai jos:

1611036725 211 Sine Cum se seteaza constrangerea unica a indexului interschimbabil

Si asta e. Codificare fericită!

Referințe:

Edgeguides | Thoughtbot