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

Рельсы знамениты своим правилом «соглашения преобладают над конфигурацией» (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.

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 8

    0
    а зачем тогда табличка words?
      0
      это больше похоже на value-object
        0
        Не совсем. Таблица содержит допустимые значения + некоторые характеристики, вроде частоты употребления. Идея в том, чтобы при парсинге текстов отфильтровывать всё лишнее, кроме нужных мне слов.
      +2
      Вообще, при работе с Рельсами лучшее, что можно сделать — это не плевать против ветра.

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

      Не говоря уже о сторонних гемах, которые вполне могут принимать за должное то, что ключ интовый, а не строковой.
        0
        Вообще, в rails 4.2 на PostgreSQL уже хорошо поддерживаются первичные ключи типа UUID. Надо только в миграциях не забывать прописывать add_reference :table, :attr, type: :uuid для внешних ключей. Проблем пока не возникает.
          0
          Да особо ничего героического не нужно. Я много раз менял и primary_key, и виды подкладывал вместо таблиц, что-то в редких случаях, конечно, ругалось, но решалось все довольно быстро.

          А неповортливые гемы-расширения ActiveRecord надо сразу отправлять в утиль, иначе рано или поздно из всплывут еще какие-нибудь какашки.
          +1
          То есть теперь вместо ключа-цифры вы в каждой связной таблице храните само слово? Как это сказывается на кол-ве хранимой информации, скорости доступа к данным?
            +2
            О возможности переопределения primary key написано прям в официальной документации по ActiveRecord, если я не ошибаюсь. Ну и зачем эта статья?

            Only users with full accounts can post comments. Log in, please.