За допомогою 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. "Підприємницькі рейки" Дана Чака містить кілька порад щодо використання природних / композитних клавіш.
Azeem. Батт

Пам'ятайте, що коли ви використовуєте щось на зразок: <! - language: lang-rb -> виконайте "ALTER TABLE співробітники ДОДАТИ ПЕРВИННИЙ КЛЮЧ (emp_id);" Хоча обмеження таблиці встановлено правильно після запуску, rake db:migrateвизначення автогенерованої схеми не містить цього обмеження!
pymkin

Відповіді:


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, оскільки остання застаріла.
Gary S. Weaver

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

4
@DonnyKurnia @ Tintin81 Ви можете переключити дамп схеми з schema.rbна structure.sqlза допомогою 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 первинним ключем класу віджетів, тоді вам залишається заповнити поле, коли створюються об’єкти. Ви повинні мати можливість зробити це за допомогою функції зворотного дзвінка перед створенням.

Отже, щось на зразок

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

Це не працює для мене, принаймні не в PostgreSQL. Первинний ключ взагалі не вказаний.
Rudd Zwolinski

Хм, цікаво, я тестував це за допомогою MySQL на Rails 2.3.2 - можливо, це теж може бути пов’язано з версією рейок?
paulthenerd

Я не впевнений - я не думаю, що це версія Rails. Я використовую Rails 2.3.3.
Rudd Zwolinski

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

2
Цей підхід працює принаймні з Rails 4.2 та Postgresql. Я тестував, але вам потрібно використовувати primary_key: trueзамість того, щоб просто primaryдати відповідь
Anwar,

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.


вибачте, але widget_id змінився на ціле число, коли я застосовую вашу техніку ... у вас є рішення eny?
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.
Tamer Shlash

4

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

Я сам не ніндзя Postgres, я отримав цю інформацію з чудового відео PeepCode від Xavier Shay на Postgres. Це відео насправді виходить на бібліотеку Аарона Паттерсона, я думаю, що Текстил, але я міг помилятися. Але крім цього, це досить здорово.

У будь-якому випадку, якщо ви стикаєтесь із цією проблемою на 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 відмінна, і немає проблем із дампером схеми.
Марнен Лейбоу-Козер

2
з одного боку, це правда, що Postgres тут не була проблемою. з іншого боку, ось помилка самоскиду схеми лише у форматі pg від 2009 rails.lighthouseapp.com/projects/8994/tickets/2418, а ось ще одна rails.lighthouseapp.com/projects/8994/tickets/2514
Джайлз Боукетт,

Виникла вирішена проблема з Rails та Postgres навколо оновлення Postgres 8.3, де вони (правильно, IMO) вирішили перейти на стандарт SQL для рядкових літералів та цитування. Адаптер Rails не перевіряв версії Postgres, і деякий час його потрібно було виправляти.
Джадсон,

@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

А в моделі worker.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

Це не працює для мене, принаймні не в PostgreSQL. Первинний ключ взагалі не вказаний.
Rudd Zwolinski

1
Цей підхід опускає стовпець id, але первинний ключ насправді не буде встановлений у таблиці.
Шон Макклірі,

1

Як щодо цього рішення,

Чому всередині моделі Employee ми не можемо додати код, який перевірятиме унікальність восени, наприклад: Припустимо, що Employee is Model у тому, що у вас є EmpId, який є рядком, то для цього ми можемо додати ": uniqueness => true" до EmpId

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

Я не впевнений, що це рішення, але це спрацювало для мене.


1

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

Я розумію, якщо вам потрібно відхилятися від конвенції ActiveRecord, я виявив, що це відмінна альтернатива. Крім того, це кращий підхід до застарілих даних, і ви можете підтримувати базу даних "як є".

Картограф об'єктів Ruby (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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.