Pull to refresh

Переопределяем первичный ключ в Ruby on Rails

Reading time 3 min
Views 7.1K
Рельсы знамениты своим правилом «соглашения преобладают над конфигурацией» (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). Пишем миграции:

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.
Tags:
Hubs:
+11
Comments 8
Comments Comments 8

Articles