Рельсы знамениты своим правилом «соглашения преобладают над конфигурацией» (Convention over Сonfiguration). Однако иногда, очень редко, некоторые вещи приходится делать по-другому. Одним из таких случаев я хочу поделиться в статье. Расскажу, как сделать свой первичный ключ в таблице (использую Rails 4.2.0). Ничего сложного, по сути, но вопросы о том, как это сделать, время от времени задают, а ответы не всегда хорошие.
Представим, что вы пишете словарь. В реальной жизни у вас пара десятков таблиц, которые связаны с таблицей words всевозможными ассоциациями: one-to-many, many-to-many, many-to-many-through. Для почти любого запроса нужно вывести в ответ кроме нужной таблицы еще и колонку word, а значит, надо джоинить таблицы. Еще у вас на каком-нибудь редисе сторонним воркером обрабатывается куча текста и воркер знать не знает, что у слов в реляционной бд есть еще и айдишники. Все это причиняет почти физическую боль и задумываетесь время от времени — может, стоит сделать что-то по-другому? Волевым усилием решаете — слова в таблице words уникальны, так к черту айдишники, пусть само слово будет первичным ключем! Во всех связях belongs-to больше не надо джоинить, воркеру не нужно сверять айдишники, можно разбить информацию по разным бд без головной боли.
Для примера, пусть у нас будут 2 таблицы: words (c первичным полем word) и definitions (с ассоциацией belongs-to к words). Пишем миграции:
В моделях указываем связи и первичный ключ для words:
Этого достаточно, не нужно делать execute в миграциях и подобные костыли. Проверим, что мы получили:
Мы поменяли в модели первичный ключ, но ничего не сломалось. Кстати, последний пример может озадачить. Откуда берется id, если ни в схеме, ни в базе его нет? Вот что скрывается под капотом:
Как видим, поле id просто «проксирует» поле primary_key.
Представим, что вы пишете словарь. В реальной жизни у вас пара десятков таблиц, которые связаны с таблицей words всевозможными ассоциациями: one-to-many, many-to-many, many-to-many-through. Для почти любого запроса нужно вывести в ответ кроме нужной таблицы еще и колонку word, а значит, надо джоинить таблицы. Еще у вас на каком-нибудь редисе сторонним воркером обрабатывается куча текста и воркер знать не знает, что у слов в реляционной бд есть еще и айдишники. Все это причиняет почти физическую боль и задумываетесь время от времени — может, стоит сделать что-то по-другому? Волевым усилием решаете — слова в таблице words уникальны, так к черту айдишники, пусть само слово будет первичным ключем! Во всех связях belongs-to больше не надо джоинить, воркеру не нужно сверять айдишники, можно разбить информацию по разным бд без головной боли.
Для примера, пусть у нас будут 2 таблицы: words (c первичным полем word) и definitions (с ассоциацией belongs-to к words). Пишем миграции:
class CreateWords < ActiveRecord::Migration
def change
create_table :words, id: false do |t|
t.string :word, null: false
t.timestamps null: false
end
add_index :words, :word, unique: true
end
end
class CreateDefinitions < ActiveRecord::Migration
def change
create_table :definitions do |t|
t.string :word_id, null: false
t.timestamps null: false
end
add_index :definitions, :word_id
end
end
В моделях указываем связи и первичный ключ для words:
class Word < ActiveRecord::Base
self.primary_key = 'word'
has_many :definitions
end
class Definition < ActiveRecord::Base
belongs_to :word
end
Этого достаточно, не нужно делать execute в миграциях и подобные костыли. Проверим, что мы получили:
w = Word.create(word: 'hello')
#<Word word: "hello", created_at: "2015-03-16 21:35:59", updated_at: "2015-03-16 21:35:59">
Word.find('hello')
Word Load (0.8ms) SELECT "words".* FROM "words" WHERE "words"."word" = $1 LIMIT 1 [["word", "hello"]]
=> #<Word word: "hello", created_at: "2015-03-16 21:35:59", updated_at: "2015-03-16 21:35:59">
d = Definition.create(word: w)
=> #<Definition id: 2, word_id: "hello", created_at: "2015-03-16 21:36:22", updated_at: "2015-03-16 21:36:22">
w.definitions
=> #<ActiveRecord::Associations::CollectionProxy [#<Definition id: 2, word_id: "hello", created_at: "2015-03-16 21:36:22", updated_at: "2015-03-16 21:36:22">]>
d.word
=> #<Word word: "hello", created_at: "2015-03-16 21:35:59", updated_at: "2015-03-16 21:35:59">
d.word_id
=> "hello"
w.id
=> "hello
Мы поменяли в модели первичный ключ, но ничего не сломалось. Кстати, последний пример может озадачить. Откуда берется id, если ни в схеме, ни в базе его нет? Вот что скрывается под капотом:
# activerecord/lib/active_record/attribute_methods/primary_key.rb:17
def id
sync_with_transaction_state
read_attribute(self.class.primary_key)
end
Как видим, поле id просто «проксирует» поле primary_key.