Составные первичные ключи
Это руководство представляет собой введение в составные первичные ключи для таблиц баз данных: создание таблиц, запросы, ассоциации, формы, параметры контроллеров и фикстуры для моделей с составными первичными ключами.
Это руководство представляет собой введение в составные первичные ключи для таблиц баз данных.
После прочтения этого руководства вы будете уметь:
- Создавать таблицу с составным первичным ключом.
- Делать запросы к модели с составным первичным ключом.
- Подключать в модели использование составного первичного ключа для запросов и ассоциаций.
- Создавать формы для моделей, использующих составные первичные ключи.
- Извлекать составные первичные ключи из параметров контроллера.
- Использовать фикстуры базы данных для таблиц с составными первичными ключами.
Что такое составные первичные ключи?
Иногда значения одного столбца недостаточно для однозначной идентификации каждой строки таблицы, и требуется комбинация из двух и более столбцов. Это может быть актуально при работе с legacy-схемой базы данных без единого столбца id в качестве первичного ключа, либо при изменении схемы для шардирования или мультитенантности.
Составные первичные ключи повышают сложность и могут работать медленнее, чем один столбец первичного ключа. Прежде чем использовать составной первичный ключ, убедитесь, что ваш сценарий действительно его требует.
Миграции для составных первичных ключей
Создать таблицу с составным первичным ключом можно, передав опцию :primary_key методу create_table со значением-массивом:
class CreateProducts < ActiveRecord::Migration[8.2]
def change
create_table :products, primary_key: [:store_id, :sku] do |t|
t.integer :store_id
t.string :sku
t.text :description
end
end
endЗапросы к моделям
Использование #find
Если таблица использует составной первичный ключ, для поиска записи через #find нужно передать массив:
# Найти продукт со store_id 3 и sku "XYZ12345"
irb> product = Product.find([3, "XYZ12345"])
=> #<Product store_id: 3, sku: "XYZ12345", description: "Yellow socks">SQL-эквивалент приведённого выше:
SELECT * FROM products WHERE store_id = 3 AND sku = "XYZ12345"Чтобы найти сразу несколько записей с составными ID, передайте в #find массив массивов:
# Найти продукты с первичными ключами [1, "ABC98765"] и [7, "ZZZ11111"]
irb> products = Product.find([[1, "ABC98765"], [7, "ZZZ11111"]])
=> [
#<Product store_id: 1, sku: "ABC98765", description: "Red Hat">,
#<Product store_id: 7, sku: "ZZZ11111", description: "Green Pants">
]SQL-эквивалент приведённого выше:
SELECT * FROM products WHERE (store_id = 1 AND sku = 'ABC98765' OR store_id = 7 AND sku = 'ZZZ11111')Модели с составными первичными ключами также используют полный составной первичный ключ при сортировке:
irb> product = Product.first
=> #<Product store_id: 1, sku: "ABC98765", description: "Red Hat">SQL-эквивалент приведённого выше:
SELECT * FROM products ORDER BY products.store_id ASC, products.sku ASC LIMIT 1Аналогично last использует полный составной первичный ключ, но с обратным направлением сортировки для каждого столбца:
irb> product = Product.last
=> #<Product store_id: 7, sku: "ZZZ11111", description: "Green Pants">SQL-эквивалент приведённого выше:
SELECT * FROM products ORDER BY products.store_id DESC, products.sku DESC LIMIT 1Здесь направление сортировки убывающее как для store_id, так и для sku, поскольку оба столбца являются частью составного первичного ключа.
Использование #where
Хэш условий для #where можно указать в синтаксисе, похожем на кортеж. Это бывает полезно для запросов по составному первичному ключу:
Product.where(Product.primary_key => [[1, "ABC98765"], [7, "ZZZ11111"]])Условия с :id
При указании условий в методах вроде find_by и where использование id сопоставляется с атрибутом :id модели. Это отличается от find, в который передаваемый ID должен быть значением первичного ключа.
Например, этот вызов find_by сопоставляется со столбцом :id, а не со всем составным первичным ключом:
Order.find_by(id: 5)И вызов where ведёт себя точно так же:
Order.where(id: 5)Будьте осторожны при использовании find_by(id:) на моделях, где :id не является первичным ключом, например на моделях с составным первичным ключом. Подробности смотрите в руководстве Интерфейс запросов Active Record.
Ассоциации между моделями с составными первичными ключами
Rails часто может вывести связи «первичный ключ — внешний ключ» между связанными моделями. Однако при работе с составными первичными ключами Rails обычно по умолчанию использует только часть составного ключа — как правило, столбец id, если явно не указано иное. Это поведение по умолчанию работает только в том случае, если составной первичный ключ модели содержит столбец :id, и этот столбец уникален для всех записей.
Рассмотрим следующий пример:
class Order < ApplicationRecord
self.primary_key = [:shop_id, :id]
has_many :books
end
class Book < ApplicationRecord
belongs_to :order
endВ этой схеме Order имеет составной первичный ключ, состоящий из [:shop_id, :id], а Book принадлежит Order. Rails предположит, что для ассоциации между заказом и его книгами в качестве первичного ключа следует использовать столбец :id. Он выведет, что столбцом внешнего ключа в таблице books является :order_id.
Ниже мы создаём Order и связанную с ним Book:
order = Order.create!(id: [1, 2], status: "pending")
book = order.books.create!(title: "A Cool Book")Чтобы получить доступ к заказу книги, мы перезагружаем ассоциацию:
book.reload.orderВ этом случае Rails сгенерирует следующий SQL для доступа к заказу:
SELECT * FROM orders WHERE id = 2Видно, что Rails использует в запросе только id заказа, а не shop_id и id вместе. В данном случае id достаточно, потому что составной первичный ключ модели действительно содержит столбец :id, и этот столбец уникален для всех записей.
Однако если приведённые выше требования не соблюдаются, либо вы хотите использовать в ассоциациях полный составной первичный ключ, можно задать опцию foreign_key: на ассоциации. Эта опция указывает составной внешний ключ для ассоциации; все столбцы внешнего ключа будут использоваться при запросе связанной записи (или записей). Например:
class Author < ApplicationRecord
self.primary_key = [:first_name, :last_name]
has_many :books, foreign_key: [:first_name, :last_name]
end
class Book < ApplicationRecord
belongs_to :author, foreign_key: [:author_first_name, :author_last_name]
endВ этой схеме Author имеет составной первичный ключ, состоящий из [:first_name, :last_name], а Book принадлежит Author с составным внешним ключом [:author_first_name, :author_last_name].
Создадим Author и связанную с ним Book:
author = Author.create!(first_name: "Jane", last_name: "Doe")
book = author.books.create!(title: "A Cool Book", author_first_name: "Jane", author_last_name: "Doe")Чтобы получить доступ к автору книги, перезагружаем ассоциацию:
book.reload.authorТеперь Rails будет использовать :first_name и :last_name из составного первичного ключа в SQL-запросе:
SELECT * FROM authors WHERE first_name = 'Jane' AND last_name = 'Doe'Формы для моделей с составными первичными ключами
Формы также можно строить для моделей с составными первичными ключами. Подробнее о синтаксисе построителя форм смотрите в руководстве Хелперы форм.
Предположим, у нас есть объект модели @book с составным ключом [:author_id, :id]:
@book = Book.find([2, 25])
# => #<Book id: 25, title: "Some book", author_id: 2>Следующая форма:
<%= form_with model: @book do |form| %>
<%= form.text_field :title %>
<%= form.submit %>
<% end %>Выводит:
<form action="/books/2_25" method="post" accept-charset="UTF-8" >
<input name="authenticity_token" type="hidden" value="..." />
<input type="text" name="book[title]" id="book_title" value="My book" />
<input type="submit" name="commit" value="Update Book" data-disable-with="Update Book">
</form>Обратите внимание, что в сгенерированном URL значения author_id и id разделены символом подчёркивания. После отправки формы контроллер может извлечь значения первичного ключа из параметров и обновить запись. Подробности смотрите в следующем разделе.
Параметры составного ключа
Параметры составного ключа содержат несколько значений в одном параметре. По этой причине нам нужно уметь извлекать каждое значение и передавать их в Active Record. Для такого сценария можно воспользоваться методом extract_value.
Возьмём следующий контроллер:
class BooksController < ApplicationController
def show
# Извлекаем значение составного ID из параметров URL.
id = params.extract_value(:id)
# Находим книгу по составному ID.
@book = Book.find(id)
# используем рендеринг по умолчанию для отображения вью show.
end
endИ следующий маршрут:
get "/books/:id", to: "books#show"Когда пользователь открывает URL /books/4_2, контроллер извлечёт значение составного ключа ["4", "2"] и передаст его в Book.find для отображения нужной записи во вью. Метод extract_value можно использовать для извлечения массивов из любых разделённых параметров.
Фикстуры для составных первичных ключей
Фикстуры для таблиц с составными первичными ключами достаточно похожи на фикстуры для обычных таблиц. При использовании столбца id его, как обычно, можно опустить:
class Book < ApplicationRecord
self.primary_key = [:author_id, :id]
belongs_to :author
end# books.yml
alices_adventure_in_wonderland:
author_id: <%= ActiveRecord::FixtureSet.identify(:lewis_carroll) %>
title: "Alice's Adventures in Wonderland"Однако для поддержки связей с составными первичными ключами необходимо использовать метод composite_identify:
class BookOrder < ApplicationRecord
self.primary_key = [:shop_id, :id]
belongs_to :order, foreign_key: [:shop_id, :order_id]
belongs_to :book, foreign_key: [:author_id, :book_id]
end# book_orders.yml
alices_adventure_in_wonderland_in_books:
author: lewis_carroll
book_id: <%= ActiveRecord::FixtureSet.composite_identify(
:alices_adventure_in_wonderland, Book.primary_key)[:id] %>
shop: book_store
order_id: <%= ActiveRecord::FixtureSet.composite_identify(
:books, Order.primary_key)[:id] %>Шифрование Active Record
Это руководство раскрывает шифрование информации вашей базы данных с помощью Active Record.
Engine для начинающих
В этом руководстве вы узнаете об engine, и как они могут быть использованы для предоставления дополнительной функциональности содержащим их приложениям с помощью понятного и простого для понимания инт