Используя Rails, как я могу установить свой первичный ключ, чтобы он не был столбцом с целым типом?

Я использую Rails-миграции для управления схемой базы данных, и я создаю простую таблицу, где я бы хотел использовать нецелое значение в качестве первичного ключа (в частности, строки). Чтобы отвлечься от моей проблемы, скажем, есть таблица employees, где сотрудники идентифицируются буквенно-цифровой строкой, например. "134SNW".

Я попытался создать таблицу в миграции следующим образом:

create_table :employees, {:primary_key => :emp_id} do |t|
    t.string :emp_id
    t.string :first_name
    t.string :last_name
end

То, что это дает мне, похоже на то, что он полностью проигнорировал строку t.string :emp_id и пошел вперед и сделал ее целым столбцом. Есть ли другой способ, чтобы рельсы генерировали ограничение PRIMARY_KEY (я использую PostgreSQL) для меня, без необходимости писать SQL в вызове execute?

ПРИМЕЧАНИЕ. Я знаю, что не лучше использовать строковые столбцы в качестве первичных ключей, поэтому, пожалуйста, не отвечайте, просто добавляя целочисленный первичный ключ. Я могу добавить его в любом случае, но этот вопрос все еще действителен.

Ответ 1

К сожалению, я решил, что это невозможно сделать без использования execute.

Почему это не работает

Изучив источник ActiveRecord, мы можем найти код для create_table:

В schema_statements.rb:

def create_table(table_name, options={})
  ...
  table_definition.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
  ...
end

Итак, мы видим, что при попытке указать первичный ключ в параметрах create_table он создает первичный ключ с указанным именем (или, если ни один не указан, id). Он делает это, вызывая тот же метод, который вы можете использовать внутри блока определения таблицы: primary_key.

В schema_statements.rb:

def primary_key(name)
  column(name, :primary_key)
end

Это просто создает столбец с указанным именем типа :primary_key. В стандартных адаптерах базы данных установлено следующее:

PostgreSQL: "serial primary key"
MySQL: "int(11) DEFAULT NULL auto_increment PRIMARY KEY"
SQLite: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL"

Обходной путь

Поскольку мы зацикливаемся на них в качестве типов первичных ключей, мы должны использовать execute для создания первичного ключа, который не является целым числом (PostgreSQL serial является целым числом с использованием последовательности):

create_table :employees, {:id => false} do |t|
  t.string :emp_id
  t.string :first_name
  t.string :last_name
end
execute "ALTER TABLE employees ADD PRIMARY KEY (emp_id);"

И как Шон Макклири отметил, ваша модель ActiveRecord должна установить первичный ключ, используя set_primary_key:

class Employee < ActiveRecord::Base
  set_primary_key :emp_id
  ...
end

Ответ 2

У меня есть один способ справиться с этим. Выполненный SQL - это ANSI SQL, поэтому он, скорее всего, будет работать с большинством реляционных баз данных, совместимых с ANSI SQL. Я тестировал, что это работает для MySQL.

Миграция:

create_table :users, :id => false do |t|
    t.string :oid, :limit => 10, :null => false
    ...
end
execute "ALTER TABLE users ADD PRIMARY KEY (oid);"

В вашей модели сделайте следующее:

class User < ActiveRecord::Base
    set_primary_key :oid
    ...
end

Ответ 3

Это работает:

create_table :employees, :primary_key => :emp_id do |t|
  t.string :first_name
  t.string :last_name
end
change_column :employees, :emp_id, :string

Это может быть не очень красиво, но конечный результат - именно то, что вы хотите.

Ответ 4

Похоже, что можно использовать этот подход:

create_table :widgets, :id => false do |t|
  t.string :widget_id, :limit => 20, :primary => true

  # other column definitions
end

class Widget < ActiveRecord::Base
  set_primary_key "widget_id"
end

Это приведет к тому, что column widget_id станет основным ключом для класса Widget, тогда вам нужно заполнить поле при создании объектов. Вы должны быть в состоянии сделать это, используя запрос перед созданием.

Итак, что-то вроде строк

class Widget < ActiveRecord::Base
  set_primary_key "widget_id"

  before_create :init_widget_id

  private
  def init_widget_id
    self.widget_id = generate_widget_id
    # generate_widget_id represents whatever logic you are using to generate a unique id
  end
end

Ответ 5

Я на Rails 2.3.5, и мой следующий способ работает с SQLite3

create_table :widgets, { :primary_key => :widget_id } do |t|
  t.string :widget_id

  # other column definitions
end

Нет необходимости: id = > false.

Ответ 6

Я попробовал это в Rails 4.2. Чтобы добавить свой настраиваемый первичный ключ, вы можете написать свою миграцию как:

# tracks_ migration
class CreateTracks < ActiveRecord::Migration
  def change
    create_table :tracks, :id => false do |t|
      t.primary_key :apple_id, :string, limit: 8
      t.string :artist
      t.string :label
      t.string :isrc
      t.string :vendor_id
      t.string :vendor_offer_code

      t.timestamps null: false
    end
    add_index :tracks, :label
  end
end

При просмотре документации column(name, type, options = {}) и прочитайте строку:

Параметр type обычно является одним из типов родств миграции, который является одним из следующих:: primary_key,: string,: text,: integer,: float,: decimal,: datetime,: time,: date,: binary,: boolean.

Я получил приведенные выше, как я показал. Ниже приведены метаданные таблицы после выполнения этой миграции:

[[email protected]_track (master)]$ rails db
psql (9.2.7)
Type "help" for help.

music_track_development=# \d tracks
                    Table "public.tracks"
      Column       |            Type             | Modifiers
-------------------+-----------------------------+-----------
 apple_id          | character varying(8)        | not null
 artist            | character varying           |
 label             | character varying           |
 isrc              | character varying           |
 vendor_id         | character varying           |
 vendor_offer_code | character varying           |
 created_at        | timestamp without time zone | not null
 updated_at        | timestamp without time zone | not null
 title             | character varying           |
Indexes:
    "tracks_pkey" PRIMARY KEY, btree (apple_id)
    "index_tracks_on_label" btree (label)

music_track_development=#

И из консоли Rails:

Loading development environment (Rails 4.2.1)
=> Unable to load pry
>> Track.primary_key
=> "apple_id"
>>

Ответ 7

После почти каждого решения, в котором говорится, что "это сработало для меня в базе X", я вижу комментарий по оригинальному плакату о том, что "не работал у меня в Postgres". Настоящей проблемой здесь может быть поддержка Postgres в Rails, что не является безупречным и, вероятно, было хуже в 2009 году, когда этот вопрос был первоначально опубликован. Например, если я правильно помню, если вы на Postgres, вы в принципе не можете получить полезный вывод из rake db:schema:dump.

Я не сам ниндзя Postgres, я получил эту информацию от Xavier Shay отличного видео PeepCode на Postgres. Это видео на самом деле пропускает библиотеку Аарона Паттерсона, я думаю, что Texticle, но я мог вспомнить неправильно. Но кроме этого это довольно хорошо.

В любом случае, если вы столкнулись с этой проблемой в Postgres, посмотрите, работают ли решения в других базах данных. Возможно, используйте rails new для создания нового приложения в виде песочницы или просто создайте что-то вроде

sandbox:
  adapter: sqlite3
  database: db/sandbox.sqlite3
  pool: 5
  timeout: 5000

в config/database.yml.

И если вы можете проверить, что это проблема с поддержкой Postgres, и вы выяснили исправление, внесите исправления в Rails или упакуйте исправления в самоцвет, потому что пользовательская база Postgres в сообществе Rails довольно большая, в основном благодаря Героку.

Ответ 8

Я нашел решение для этого, которое работает с Rails 3:

Файл миграции:

create_table :employees, {:primary_key => :emp_id} do |t|
  t.string :emp_id
  t.string :first_name
  t.string :last_name
end

И в модели employee.rb:

self.primary_key = :emp_id

Ответ 9

В Rails 5 вы можете сделать

create_table :employees, id: :string do |t|
  t.string :first_name
  t.string :last_name
end

См. документация create_table.

Ответ 10

вам нужно использовать опцию: id = > false

create_table :employees, :id => false, :primary_key => :emp_id do |t|
    t.string :emp_id
    t.string :first_name
    t.string :last_name
end

Ответ 11

Трюк, который работал у меня на Rails 3 и MySQL, был следующим:

create_table :events, {:id => false} do |t|
  t.string :id, :null => false
end

add_index :events, :id, :unique => true

Итак:

  • use: id = > false, чтобы не генерировать целочисленный первичный ключ
  • используйте нужный тип данных и добавьте: null = > false
  • добавить уникальный индекс в этот столбец

Кажется, что MySQL преобразует уникальный индекс в столбце non null в первичный ключ!

Ответ 12

Как насчет этого решения,

Внутри модели Employee почему мы не можем добавить код, который будет проверять уникальность в coloumn, например: Предположим, что Employee является Model, поскольку у вас есть EmpId, который является строкой, для чего мы можем добавить ": uniqueness = > true" для EmpId

    class Employee < ActiveRecord::Base
      validates :EmpId , :uniqueness => true
    end

Я не уверен, что это решение, но это сработало для меня.

Ответ 13

Я знаю, что это старый поток, на который я наткнулся... но я в шоке, никто не упоминал DataMapper.

Я считаю, что если вам нужно отклониться от соглашения ActiveRecord, я обнаружил, что это отличная альтернатива. Кроме того, это лучший подход к наследию, и вы можете поддерживать базу данных "как есть".

Ruby Object Mapper (DataMapper 2) содержит много обещаний и основывается на принципах AREL тоже!

Ответ 14

Добавление индекса работает для меня, я использую MySql btw.

create_table :cards, {:id => false} do |t|
    t.string :id, :limit => 36
    t.string :name
    t.string :details
    t.datetime :created_date
    t.datetime :modified_date
end
add_index :cards, :id, :unique => true