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

Я использую 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 и пошел вперед и сделал его целочисленным столбцом. Есть ли другой способ заставить rails генерировать для меня ограничение PRIMARY_KEY (я использую PostgreSQL), без необходимости писать SQL в вызове execute?

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

Ответы [ 14 ]

107 голосов
/ 16 сентября 2009

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

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

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

In 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
21 голосов
/ 08 марта 2013

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

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

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

18 голосов
/ 15 сентября 2009

У меня есть один способ справиться с этим. Выполненный 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

9 голосов
/ 16 мая 2015

Я пробовал это в 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 ,: двоичный,: логический.

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

[arup@music_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"
>>
8 голосов
/ 26 ноября 2016

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

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

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

8 голосов
/ 13 мая 2010

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

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

  # other column definitions
end

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

8 голосов
/ 29 июля 2009

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

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

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

Так что-то вроде

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
4 голосов
/ 19 октября 2012

Я нашел решение, которое работает с 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
4 голосов
/ 19 февраля 2012

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

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

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

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

in config/database.yml.

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

3 голосов
/ 23 марта 2011

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

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

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

Итак:

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

Похоже, что MySQL преобразует уникальный индекс по ненулевому столбцу в первичный ключ!

...