Раилс: Како поставити јединствено заменљиво ограничење индекса

Постављање валидације јединствености у шине је нешто што ћете на крају радити прилично често. Можда сте их већ додали у већину својих апликација. Међутим, ова валидација даје само добар кориснички интерфејс и искуство. Обавештава корисника о грешкама које спречавају задржавање података у бази података.

Зашто валидација јединствености није довољна

Чак и уз валидацију јединствености, нежељени подаци се понекад чувају у бази података. Да бисмо били јаснији, погледајмо доњи модел корисника:

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

Да би потврдио колону корисничког имена, раилс поставља упит бази података користећи СЕЛЕЦТ да би утврдио да ли корисничко име већ постоји. Ако се догоди, исписује се „Корисничко име већ постоји“. Ако се то не догоди, покреће ИНСЕРТ упит за задржавање новог корисничког имена у бази података.

Када два корисника истодобно изводе исти процес, база података понекад може да сачува податке, без обзира на ограничење валидације, и ту долазе ограничења базе података (јединствени индекс).

Ако корисник А и корисник Б истовремено покушавају задржати исто корисничко име у бази података, раилс покреће СЕЛЕЦТ упит, ако корисничко име већ постоји, обавјештава оба корисника. Међутим, ако корисничко име не постоји у бази података, истовремено покреће ИНСЕРТ упит за оба корисника, као што је приказано на доњој слици.

Сада када знате зашто је јединствени индекс базе података (ограничење базе података) важан, хајде да се позабавимо начином постављања. Прилично је једноставно поставити јединствени индекс (индексе) базе података за било који ступац или скуп колона у шинама. Међутим, нека ограничења базе података у шинама могу бити незгодна.

Кратки преглед постављања јединственог индекса за једну или више колона

Ово је једноставно као покретање миграције. Претпоставимо да имамо табелу корисника са корисничким именом колоне и желимо да осигурамо да сваки корисник има јединствено корисничко име. Једноставно креирате миграцију и унесете следећи код:

add_index :users, :username, unique: true 

Затим покренете миграцију и то је то. База података сада осигурава да се у табели не сачувају слична корисничка имена.

За више придружених колона, претпоставимо да имамо табелу захтева са колонама сендер_ид и рецеивер_ид. Слично томе, једноставно креирате миграцију и унесете следећи код:

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

И то је то? Ух, не тако брзо.

Проблем са вишеструком миграцијом колона изнад

Проблем је што су ИД-ови у овом случају заменљиви. То значи да ако имате сендер_ид 1, а рецеивер_ид 2, табела захтева и даље може сачувати сендер_ид 2 и рецеивер_ид 1, иако они већ имају захтев на чекању.

Овај проблем се често дешава у аутореференцијалној асоцијацији. То значи да су и пошиљалац и прималац корисници, а на сендер_ид или рецеивер_ид референцује се усер_ид. Корисник са усер_ид (сендер_ид) од 1 шаље захтев кориснику са усер_ид (рецеивер_ид) од 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 

Проблем овог решења је у томе што се пријемни_ид и сендер_ид замењују сваки пут пре него што се сачувају у бази података. Дакле, колона прималац_ид мораће да сачува сендер_ид и обрнуто.

На пример, ако корисник са ИД-ом пошиљаоца 1 пошаље захтев кориснику са ИД-ом пријемника 2, табела захтева биће приказана доле:

Ово можда не звучи као проблем, али боље је ако ваше колоне чувају тачне податке које желите да сачувају. Ово има бројне предности. На пример, ако требате послати обавештење примаоцу путем рецеивер_ид, тада ћете у бази података упитати тачан ИД из колоне рецеивер_ид. Ово је већ постало збуњујуће оног тренутка када почнете да мењате податке сачуване у табели захтева.

Исправно решење

Овај проблем се може у потпуности решити директним разговором са базом података. У овом случају ћу објаснити коришћење ПостгреСКЛ-а. Приликом извођења миграције, морате осигурати да јединствено ограничење провјерава и (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(...))је рећи шинама да је наш код ПостгреСКЛ. Ово помаже шинама да покрећу наш код као ПостгреСКЛ.

Будући да су наши „ид-ови“ цели бројеви, пре него што их сачувамо у бази података, проверавамо да ли су највећи и најмањи (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)) 

Табела захтева тада ће бити тачно онаква какву намеравамо, као што је приказано на слици испод:

И то је то. Срећно кодирање!

Референце:

Едгегуидес | Тхоугхтбот