Schienen: So legen Sie eine eindeutige Einschränkung für austauschbare Indizes fest

Das Festlegen der Überprüfung der Eindeutigkeit in Schienen wird am Ende häufig durchgeführt. Vielleicht haben Sie sie sogar bereits zu den meisten Ihrer Apps hinzugefügt. Diese Validierung bietet jedoch nur eine gute Benutzeroberfläche und Erfahrung. Es informiert den Benutzer über die Fehler, die verhindern, dass die Daten in der Datenbank gespeichert werden.

Warum die Validierung der Eindeutigkeit nicht ausreicht

Selbst bei der Überprüfung der Eindeutigkeit werden unerwünschte Daten manchmal in der Datenbank gespeichert. Schauen wir uns zur Verdeutlichung ein unten gezeigtes Benutzermodell an:

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

Um die Spalte mit dem Benutzernamen zu überprüfen, fragt Rails die Datenbank mit SELECT ab, um festzustellen, ob der Benutzername bereits vorhanden ist. Wenn dies der Fall ist, wird "Benutzername existiert bereits" ausgegeben. Wenn dies nicht der Fall ist, wird eine INSERT-Abfrage ausgeführt, um den neuen Benutzernamen in der Datenbank beizubehalten.

Wenn zwei Benutzer denselben Prozess gleichzeitig ausführen, kann die Datenbank die Daten manchmal unabhängig von der Validierungsbeschränkung speichern, und hier kommen die Datenbankeinschränkungen (eindeutiger Index) ins Spiel.

Wenn Benutzer A und Benutzer B gleichzeitig versuchen, denselben Benutzernamen in der Datenbank beizubehalten, führt Rails die SELECT-Abfrage aus. Wenn der Benutzername bereits vorhanden ist, werden beide Benutzer informiert. Wenn der Benutzername jedoch nicht in der Datenbank vorhanden ist, wird die INSERT-Abfrage für beide Benutzer gleichzeitig ausgeführt (siehe Abbildung unten).

Nachdem Sie nun wissen, warum der eindeutige Datenbankindex (Datenbankeinschränkung) wichtig ist, werden wir uns mit dessen Festlegung befassen. Es ist ziemlich einfach, eindeutige Datenbankindizes für eine Spalte oder einen Satz von Spalten in Schienen festzulegen. Einige Datenbankbeschränkungen in Rails können jedoch schwierig sein.

Ein kurzer Blick auf das Festlegen eines eindeutigen Index für eine oder mehrere Spalten

Dies ist so einfach wie das Ausführen einer Migration. Nehmen wir an, wir haben eine Benutzertabelle mit einem Spaltenbenutzernamen und möchten sicherstellen, dass jeder Benutzer einen eindeutigen Benutzernamen hat. Sie erstellen einfach eine Migration und geben den folgenden Code ein:

add_index :users, :username, unique: true 

Dann führen Sie die Migration aus und fertig. Die Datenbank stellt jetzt sicher, dass keine ähnlichen Benutzernamen in der Tabelle gespeichert werden.

Nehmen wir für mehrere zugeordnete Spalten an, dass wir eine Anforderungstabelle mit den Spalten sender_id und recept_id haben. Ebenso erstellen Sie einfach eine Migration und geben den folgenden Code ein:

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

Und das ist es? Oh, nicht so schnell.

Das Problem mit der obigen Migration mehrerer Spalten

Das Problem ist, dass die IDs in diesem Fall austauschbar sind. Dies bedeutet, dass die Anfragetabelle bei einer Absender-ID von 1 und einer Empfänger-ID von 2 weiterhin eine Absender-ID von 2 und eine Empfänger-ID von 1 speichern kann, obwohl bereits eine ausstehende Anforderung vorliegt.

Dieses Problem tritt häufig in einer selbstreferenziellen Zuordnung auf. Dies bedeutet, dass sowohl der Absender als auch der Empfänger Benutzer sind und auf die Absender-ID oder die Empfänger-ID von der Benutzer-ID verwiesen wird. Ein Benutzer mit der Benutzer-ID (Absender-ID) von 1 sendet eine Anforderung an einen Benutzer mit der Benutzer-ID (Empfänger-ID) von 2.

Wenn der Empfänger erneut eine Anfrage sendet und diese in der Datenbank speichern darf, haben wir zwei ähnliche Anfragen von denselben zwei Benutzern (Sender und Empfänger || Empfänger und Sender) in der Anforderungstabelle.

Dies ist im Bild unten dargestellt:

Die gemeinsame Lösung

Dieses Problem wird häufig mit dem folgenden Pseudocode behoben:

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 

Das Problem bei dieser Lösung besteht darin, dass die Empfänger-ID und die Sender-ID jedes Mal ausgetauscht werden, bevor sie in der Datenbank gespeichert werden. Daher muss die Spalte Empfänger_ID die Absender_ID speichern und umgekehrt.

Wenn beispielsweise ein Benutzer mit der Absender-ID 1 eine Anforderung an einen Benutzer mit der Empfänger-ID 2 sendet, sieht die Anforderungstabelle wie folgt aus:

Dies mag nicht nach einem Problem klingen, aber es ist besser, wenn Ihre Spalten genau die Daten speichern, die sie speichern sollen. Dies hat zahlreiche Vorteile. Wenn Sie beispielsweise über die Empfänger-ID eine Benachrichtigung an den Empfänger senden müssen, fragen Sie die Datenbank nach der genauen ID aus der Spalte Empfänger-ID ab. Dies wurde bereits verwirrender, sobald Sie die in Ihrer Anforderungstabelle gespeicherten Daten wechseln.

Die richtige Lösung

Dieses Problem kann vollständig gelöst werden, indem Sie direkt mit der Datenbank sprechen. In diesem Fall erkläre ich die Verwendung von PostgreSQL. Wenn Sie die Migration ausführen, müssen Sie vor dem Speichern sicherstellen, dass die eindeutige Einschränkung sowohl (1,2) als auch (2,1) in der Anforderungstabelle überprüft.

Sie können dies tun, indem Sie eine Migration mit dem folgenden Code ausführen:

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 

Code-Erklärung

Nach dem Erstellen der Migrationsdatei soll das Reversible sicherstellen, dass wir unsere Datenbank jederzeit zurücksetzen können. Dies dir.upist der Code, der ausgeführt wird, wenn wir unsere Datenbank migrieren, und der ausgeführt dir.downwird, wenn wir unsere Datenbank herunterfahren oder zurücksetzen.

connection.execute(%q(...))soll Rails mitteilen, dass unser Code PostgreSQL ist. Dies hilft Rails, unseren Code als PostgreSQL auszuführen.

Da unsere "IDs" Ganzzahlen sind, überprüfen wir vor dem Speichern in der Datenbank, ob die größten und kleinsten (2 und 1) bereits in der Datenbank vorhanden sind, indem wir den folgenden Code verwenden:

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

Dann prüfen wir auch, ob die kleinsten und größten (1 und 2) in der Datenbank sind, indem wir:

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

Die Anforderungstabelle entspricht dann genau unserer Absicht, wie in der folgenden Abbildung dargestellt:

Und das ist es. Viel Spaß beim Codieren!

Verweise:

Edgeguides | Thoughtbot