Rails: Як встановити унікальне взаємозамінне обмеження індексу

Налаштування перевірки унікальності в рейках - це те, що ви в кінцевому підсумку будете робити досить часто. Можливо, ви навіть уже додали їх до більшості своїх додатків. Однак ця перевірка дає лише хороший користувальницький інтерфейс та досвід. Він інформує користувача про помилки, що перешкоджають збереженню даних у базі даних.

Чому перевірки унікальності недостатньо

Навіть за умови перевірки унікальності небажані дані іноді зберігаються у базі даних. Для наочності давайте розглянемо модель користувача, показану нижче:

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

Щоб перевірити стовпець імені користувача, rails запитує базу даних за допомогою SELECT, щоб перевірити, чи вже існує ім’я користувача. Якщо так, друкується “Ім'я користувача вже існує”. Якщо цього не відбувається, він запускає запит INSERT для збереження нового імені користувача в базі даних.

Коли два користувачі виконують один і той же процес одночасно, база даних іноді може зберігати дані незалежно від обмежень перевірки, і саме тут надходять обмеження бази даних (унікальний індекс).

Якщо користувач A і користувач B намагаються одночасно зберегти одне і те ж ім'я користувача в базі даних, rails запускає запит SELECT, якщо ім'я користувача вже існує, він інформує обох користувачів. Однак, якщо ім'я користувача не існує в базі даних, він одночасно запускає запит INSERT для обох користувачів, як показано на зображенні нижче.

Тепер, коли ви знаєте, чому унікальний індекс бази даних (обмеження бази даних) важливий, давайте розберемося, як його встановити. Досить просто встановити унікальний індекс (и) бази даних для будь-якого стовпця або набору стовпців у напрямках. Однак деякі обмеження бази даних у рейках можуть бути складними.

Короткий огляд встановлення унікального індексу для одного або декількох стовпців

Це просто так само, як запуск міграції. Припустимо, у нас є таблиця користувачів із іменем користувача стовпця, і ми хочемо переконатися, що кожен користувач має унікальне ім’я користувача. Ви просто створюєте міграцію та вводите такий код:

add_index :users, :username, unique: true 

Потім ви запускаєте міграцію і все. Тепер база даних гарантує, що подібні імена користувачів не зберігаються в таблиці.

Для кількох пов’язаних стовпців припустимо, що ми маємо таблицю запитів зі стовпцями sender_id та receiver_id. Подібним чином ви просто створюєте міграцію та вводите такий код:

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

І це все? О, не так швидко.

Проблема з кількома міграціями стовпців вище

Проблема полягає в тому, що ідентифікатори в даному випадку взаємозамінні. Це означає, що якщо у вас ідентифікатор sender_id 1, а ідентифікатор приймача 2, таблиця запитів все одно може зберегти ідентифікатор sender_id 2 і receiver_id 1, навіть якщо вони вже мають очікуваний запит.

Ця проблема часто трапляється в асоційованій асоціації. Це означає, що і відправник, і одержувач є користувачами, а посилання на ідентифікатор відправника або одержувача посилається з імені користувача. Користувач з 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 і навпаки.

Наприклад, якщо користувач з ідентифікатором sender_id 1 надсилає запит користувачеві з ідентифікатором приймача 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(...))полягає в тому, щоб сказати rails, що наш код - PostgreSQL. Це допомагає rails запускати наш код як 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)) 

Тоді таблиця запитів буде точно такою, як ми плануємо, як показано на малюнку нижче:

І це все. Щасливого кодування!

Список літератури:

Крайові напрямні | Thoughtbot