Как с помощью Rails сделать мой первичный ключ столбцом с целочисленным типом?

85

Я использую миграции 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
Ни один из них не будет работать, если вы используете Postgres. В «Enterprise Rails» Дэна Чака есть несколько советов по использованию естественных / составных ключей.
Azeem.Butt
Помните, что когда вы используете что-то вроде: <! - language: lang-rb ->, выполняйте «ALTER TABLE employee ADD PRIMARY KEY (emp_id);» Несмотря на то, что ограничение таблицы установлено правильно после запуска, rake db:migrateавтоматически созданное определение схемы не содержит этого ограничения!
пымкин

Ответы:

107

К сожалению, я решил, что без использования 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
Радд Зволински
источник
15
обходной путь заставит rake db: test: clone или rake db: schema: load создать emp_id как целочисленный столбец.
Донни Курния
18
фактически, теперь вы должны использовать self.primary_key=вместо set_primary_key, поскольку последний устарел.
Гэри С. Уивер
2
Вот единственное решение, которое сработало для меня. Надеюсь, это поможет другим: stackoverflow.com/a/15297616/679628
findchris
8
Проблема с этим подходом заключается в том, что при настройке вашей базы данных schema.rb emp_idснова будет integerстолбец типа. Похоже, что schema.rbникак не может хранить execute "ALTER TABLE employees ADD PRIMARY KEY (emp_id);".
Tintin81 02
4
@DonnyKurnia @ Tintin81 Вы можете переключить дамп схемы с schema.rbна параметр structure.sqlvia config.active_record.schema_format, который может быть либо :sqlили :ruby. guides.rubyonrails.org/v3.2.13/…
Свилен Иванов
21

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

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

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

Остин
источник
1
В заключение! Это единственное решение, которое у меня сработало.
findchris
Нужно ли указывать код для миграции вниз? или Rails достаточно умен, чтобы знать, что делать при миграции вниз?
amey1908
2
Для нисходящей миграции:drop_table :employees
Остин,
6
К сожалению, этот метод также оставляет нам schema.rbнесинхронизированный объект ... он сохранит :primary_key => :emp_idобъявление, но при rake db:schema:loadего вызове, как это происходит в начале тестов, он создаст целочисленный столбец. Однако может случиться так, что если вы переключитесь на использование structure.sql(параметр конфигурации), тесты сохранят настройки и будут использовать этот файл для загрузки схемы.
Том Харрисон,
18

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

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

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

[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"
>>
Аруп Ракшит
источник
3
Однако проблема заключается в том, что schema.rbгенерируемый файл не отражает stringтип первичного ключа, поэтому при генерации базы данных с помощью loadпервичного ключа создается целое число.
Питер Альфвин
9

В Rails 5 вы можете

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

См. Документацию create_table .

Павел Чучува
источник
Стоит отметить, что вам нужно будет добавить before_create :set_idв модель какой-то метод для присвоения значения первичного ключа
FloatingRock
8

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

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
Paulthenerd
источник
У меня это не работает, по крайней мере, в PostgreSQL. Первичный ключ вообще не указан.
Rudd Zwolinski
Хм, интересно, я тестировал это с MySQL on Rails 2.3.2 - может быть, это тоже связано с версией rails?
paulthenerd
Я не уверен - я не думаю, что это версия Rails. Я использую Rails 2.3.3.
Rudd Zwolinski
К сожалению, при таком подходе для таблицы не установлен фактический первичный ключ.
Шон МакКлири,
2
Этот подход работает, по крайней мере, с Rails 4.2 и Postgresql. Я тестировал, но вам нужно использовать primary_key: trueвместо того, чтобы просто primaryдать ответ
Анвар
8

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

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

  # other column definitions
end

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

Trung LE
источник
извините, но widget_id изменился на integer, когда я применил вашу технику ... у вас есть какое-нибудь решение?
kikicarbonell
1
Это больше не работает, по крайней мере, в ActiveRecord 4.1.4. you can't redefine the primary key column 'widgets'. To define a custom primary key, pass { id: false } to create_table.
Выдает
4

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

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

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

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

в config/database.yml.

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

Джайлз Боукетт
источник
2
Голосование против FUD и неточности. Я использовал Postgres в большинстве своих проектов Rails с 2007 года. Поддержка Postgres в Rails превосходна, и нет проблем с дампером схемы.
Marnen Laibow-Koser
2
С одной стороны, это правда, что Постгрес здесь не был проблемой. с другой стороны, вот ошибка дампера схемы только для pg от rails.lighthouseapp.com/projects/8994/tickets/2418 2009 года, а вот еще одна rails.lighthouseapp.com/projects/8994/tickets/2514
Джайлс Боукетт,
Была решенная проблема с Rails и Postgres, связанная с обновлением Postgres 8.3, когда они (правильно, IMO) решили перейти на стандарт SQL для строковых литералов и цитирования. Адаптер Rails не проверял версии Postgres, и какое-то время его приходилось исправлять.
Judson
@Judson Интересно. Я никогда не сталкивался с этим.
Marnen Laibow-Koser
4

Я нашел решение, которое работает с 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
шиколас
источник
3

Уловка, которая сработала для меня в Rails 3 и MySQL, заключалась в следующем:

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

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

Так:

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

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

Кларк
источник
В Rails 3.22 с MySQL 5.5.15 он создает только уникальный ключ, но не первичный ключ.
lulalala
2

вы должны использовать опцию: 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
BvuRVKyUVlViVIc7
источник
У меня это не работает, по крайней мере, в PostgreSQL. Первичный ключ вообще не указан.
Rudd Zwolinski
1
В этом подходе столбец id будет опущен, но первичный ключ фактически не будет установлен в таблице.
Шон МакКлири,
1

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

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

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

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

Виджай Сали
источник
1

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

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

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

инженер Дэйв
источник
1

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

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
Guster
источник