Rails: Πώς να ορίσετε μοναδικό εναλλάξιμο περιορισμό ευρετηρίου

Ο καθορισμός επικύρωσης μοναδικότητας σε ράγες είναι κάτι που καταλήγετε να κάνετε αρκετά συχνά. Ίσως, ακόμη και να τα έχετε προσθέσει στις περισσότερες εφαρμογές σας. Ωστόσο, αυτή η επικύρωση παρέχει μόνο μια καλή διεπαφή χρήστη και εμπειρία. Ενημερώνει τον χρήστη για τα σφάλματα που εμποδίζουν τη διατήρηση των δεδομένων στη βάση δεδομένων.

Γιατί δεν είναι αρκετή η επικύρωση της μοναδικότητας

Ακόμη και με την επικύρωση της μοναδικότητας, τα ανεπιθύμητα δεδομένα αποθηκεύονται μερικές φορές στη βάση δεδομένων. Για λόγους σαφήνειας, ας ρίξουμε μια ματιά σε ένα μοντέλο χρήστη που φαίνεται παρακάτω:

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

Για να επικυρώσετε τη στήλη Όνομα χρήστη, ζητά από τη βάση δεδομένων τη βάση δεδομένων χρησιμοποιώντας SELECT για να δείτε εάν υπάρχει ήδη το όνομα χρήστη. Εάν συμβαίνει αυτό, εκτυπώνει "Το όνομα χρήστη υπάρχει ήδη". Εάν δεν το κάνει, εκτελεί ένα ερώτημα ΕΙΣΑΓΩΓΗΣ για να διατηρήσει το νέο όνομα χρήστη στη βάση δεδομένων.

Όταν δύο χρήστες εκτελούν την ίδια διαδικασία ταυτόχρονα, η βάση δεδομένων μπορεί μερικές φορές να αποθηκεύσει τα δεδομένα ανεξάρτητα από τον περιορισμό επικύρωσης και εκεί έρχεται ο περιορισμός της βάσης δεδομένων (μοναδικός δείκτης).

Εάν ο χρήστης Α και ο χρήστης Β προσπαθούν και οι δύο να διατηρήσουν το ίδιο όνομα χρήστη στη βάση δεδομένων ταυτόχρονα, οι ράγες εκτελούν το ερώτημα SELECT, εάν το όνομα χρήστη υπάρχει ήδη, ενημερώνει και τους δύο χρήστες. Ωστόσο, εάν το όνομα χρήστη δεν υπάρχει στη βάση δεδομένων, εκτελεί το ερώτημα INSERT και για τους δύο χρήστες ταυτόχρονα, όπως φαίνεται στην παρακάτω εικόνα.

Τώρα που ξέρετε γιατί το μοναδικό ευρετήριο της βάσης δεδομένων (περιορισμός βάσης δεδομένων) είναι σημαντικό, ας δούμε πώς να το ρυθμίσουμε. Είναι πολύ εύκολο να ορίσετε μοναδικά ευρετήρια βάσης δεδομένων για οποιαδήποτε στήλη ή σύνολο στηλών σε ράγες. Ωστόσο, κάποιος περιορισμός της βάσης δεδομένων στις ράγες μπορεί να είναι δύσκολος.

Μια γρήγορη ματιά στη ρύθμιση ενός μοναδικού ευρετηρίου για μία ή περισσότερες στήλες

Αυτό είναι τόσο απλό όσο η εκτέλεση μιας μετεγκατάστασης. Ας υποθέσουμε ότι έχουμε έναν πίνακα χρηστών με όνομα χρήστη στήλης και θέλουμε να διασφαλίσουμε ότι κάθε χρήστης έχει ένα μοναδικό όνομα χρήστη. Δημιουργείτε απλά μια μετεγκατάσταση και εισάγετε τον ακόλουθο κώδικα:

add_index :users, :username, unique: true 

Τότε εκτελείτε τη μετεγκατάσταση και αυτό είναι. Η βάση δεδομένων διασφαλίζει τώρα ότι δεν αποθηκεύονται παρόμοια ονόματα χρήστη στον πίνακα.

Για πολλές συσχετισμένες στήλες, ας υποθέσουμε ότι έχουμε έναν πίνακα αιτημάτων με στήλες sender_id και receiver_id. Ομοίως, απλά δημιουργείτε μια μετεγκατάσταση και εισάγετε τον ακόλουθο κώδικα:

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

Και αυτό είναι; Ω, όχι τόσο γρήγορα.

Το πρόβλημα με τη μετεγκατάσταση πολλαπλών στηλών παραπάνω

Το πρόβλημα είναι ότι τα αναγνωριστικά, σε αυτήν την περίπτωση, είναι εναλλάξιμα. Αυτό σημαίνει ότι εάν έχετε ένα sender_id του 1 και το receiver_id του 2, ο πίνακας αιτήσεων μπορεί να αποθηκεύσει ένα sender_id του 2 και το receiver_id του 1, παρόλο που έχουν ήδη ένα εκκρεμές αίτημα.

Αυτό το πρόβλημα συμβαίνει συχνά σε μια σχέση αυτοαναφοράς. Αυτό σημαίνει ότι τόσο ο αποστολέας όσο και ο παραλήπτης είναι χρήστες και το sender_id ή το receiver_id αναφέρεται από το user_id. Ένας χρήστης με user_id (sender_id) 1 στέλνει ένα αίτημα σε έναν χρήστη με user_id (receiver_id) του 2.

Εάν ο παραλήπτης στέλνει ξανά ένα άλλο αίτημα και τον αφήσουμε να αποθηκεύσει στη βάση δεδομένων, τότε έχουμε δύο παρόμοια αιτήματα από τους ίδιους δύο χρήστες (αποστολέας και δέκτης || παραλήπτης και αποστολέας) στον πίνακα αιτήσεων.

Αυτό φαίνεται στην παρακάτω εικόνα:

Η κοινή επιδιόρθωση

Αυτό το πρόβλημα συχνά επιλύεται με τον ψευδοκώδικα παρακάτω:

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 

Το πρόβλημα με αυτήν τη λύση είναι ότι το receiver_id και sender_id ανταλλάσσονται κάθε φορά πριν από την αποθήκευση στη βάση δεδομένων. Ως εκ τούτου, η στήλη receiver_id θα πρέπει να αποθηκεύσει το sender_id και το αντίστροφο.

Για παράδειγμα, εάν ένας χρήστης με sender_id του 1 στέλνει ένα αίτημα σε έναν χρήστη με receiver_id του 2, ο πίνακας αιτημάτων θα είναι όπως φαίνεται παρακάτω:

Αυτό μπορεί να μην ακούγεται σαν πρόβλημα, αλλά είναι καλύτερο εάν οι στήλες σας αποθηκεύουν τα ακριβή δεδομένα που θέλετε να αποθηκεύσουν. Αυτό έχει πολλά πλεονεκτήματα. Για παράδειγμα, εάν πρέπει να στείλετε μια ειδοποίηση στον παραλήπτη μέσω του receiver_id, τότε θα ζητήσετε τη βάση δεδομένων για το ακριβές αναγνωριστικό από τη στήλη receiver_id. Αυτό έγινε ήδη πιο συγκεχυμένο τη στιγμή που αρχίζετε να αλλάζετε τα δεδομένα που έχετε αποθηκεύσει στον πίνακα αιτημάτων σας.

Η σωστή επιδιόρθωση

Αυτό το πρόβλημα μπορεί να επιλυθεί πλήρως μιλώντας απευθείας στη βάση δεδομένων. Σε αυτήν την περίπτωση, θα εξηγήσω τη χρήση του PostgreSQL. Κατά την εκτέλεση της μετεγκατάστασης, πρέπει να βεβαιωθείτε ότι ο μοναδικός περιορισμός ελέγχει και τα δύο (1,2) και (2,1) στον πίνακα αιτήσεων πριν από την αποθήκευση.

Μπορείτε να το κάνετε εκτελώντας μια μετεγκατάσταση με τον παρακάτω κώδικα:

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 

Επεξήγηση κώδικα

Μετά τη δημιουργία του αρχείου μετεγκατάστασης, το αναστρέψιμο είναι να διασφαλίσουμε ότι μπορούμε να επαναφέρουμε τη βάση δεδομένων μας όποτε πρέπει. Το dir.upείναι ο κωδικός για να τρέξει όταν θα μεταναστεύσουν βάση δεδομένων μας και dir.downθα τρέξει όταν μεταναστεύουν προς τα κάτω ή να επαναφέρει τη βάση δεδομένων μας.

connection.execute(%q(...))είναι να πούμε στις ράγες ότι ο κώδικάς μας είναι PostgreSQL. Αυτό βοηθά τις ράγες να τρέχουν τον κώδικα μας ως PostgreSQL.

Δεδομένου ότι τα «αναγνωριστικά» μας είναι ακέραιοι, πριν αποθηκεύσουμε στη βάση δεδομένων, ελέγχουμε εάν τα μεγαλύτερα και τα λιγότερα (2 και 1) βρίσκονται ήδη στη βάση δεδομένων χρησιμοποιώντας τον παρακάτω κώδικα:

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

Στη συνέχεια, ελέγξουμε επίσης αν οι λιγότεροι και οι μεγαλύτεροι (1 και 2) βρίσκονται στη βάση δεδομένων χρησιμοποιώντας:

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

Ο πίνακας αιτημάτων θα είναι στη συνέχεια ακριβώς όπως σκοπεύουμε όπως φαίνεται στην παρακάτω εικόνα:

Και αυτό είναι. Καλή κωδικοποίηση!

Βιβλιογραφικές αναφορές:

Edgeguides | Σκέψη