使用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,并使其成为整数列。还有其他方法可以让rails为我生成PRIMARY_KEY约束(我正在使用PostgreSQL),而不必在execute调用中编写SQL吗?

注意:我知道最好不要将字符串列用作主键,所以请不要回答只是说要添加一个整数主键。我仍然可以添加一个,但是这个问题仍然有效。


我有同样的问题,我很乐意为此找到答案。到目前为止,所有建议都没有奏效。
肖恩·麦克莱里

1
如果您使用的是Postgres,它们都不起作用。Dan Chak的“ Enterprise Rails”提供了一些使用自然/复合键的技巧。
Azeem.Butt 2010年

请注意,当您使用以下命令时:<!-语言:lang-rb->执行“ ALTER TABLE employee ADD PRIMARY KEY(emp_id);” 尽管在运行后正确设置了表约束rake db:migrate自动生成的架构定义不包含此约束!
pymkin

Answers:


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

这只会创建具有指定名称type的列:primary_key。在标准数据库适配器中将其设置为以下内容:

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

解决方法

由于我们将这些作为主键类型,因此必须使用它execute来创建不是整数的主键(PostgreSQLserial是使用序列的整数):

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);"

正如Sean McCleary所述,您的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作为整数列。
Donny Kurnia

18
实际上,由于不推荐使用,所以现在应该使用self.primary_key=而不是set_primary_key
加里·韦弗

2
这是唯一对我有用的解决方案。我希望它对其他人有帮助: stackoverflow.com/a/15297616/679628
findchris 2014年

8
这种方法的问题在于,当您从中设置数据库时,schema.rb emp_id它将integer再次成为类型列。似乎schema.rb无法execute "ALTER TABLE employees ADD PRIMARY KEY (emp_id);"以任何方式存储。
Tintin81'1

4
@DonnyKurnia @ Tintin81您可以将模式转储从切换schema.rbstructure.sql通过config.active_record.schema_format设置,可以是:sql:rubyguides.rubyonrails.org/v3.2.13/...
Svilen伊万诺夫

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 2014年

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,:string,:text,:integer,:float,:decimal,:datetime,:time,:date,:binary,:boolean 。

正如我所显示的,我得到了上述想法。这是运行此迁移后的表元数据:

[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,主键将创建为整数。
彼得·阿尔夫文


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

这对我不起作用,至少在PostgreSQL中不起作用。根本没有指定主键。
Rudd Zwolinski 09年

嗯,很有趣,我已经在Rails 2.3.2上的MySQL上对其进行了测试-也许也可能与Rails的版本有关?
paulthenerd

我不确定-我不认为这是Rails的版本。我正在使用Rails 2.3.3。
拉德·兹沃林斯基

不幸的是,使用这种方法没有为表设置实际的主键。
肖恩·麦克莱里

2
这种方法至少适用于Rails 4.2和Postgresql。我已经测试过,但是您需要使用答案,primary_key: true而不是仅仅primary给出答案
Anwar 2015年

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更改为整数...您有解决方案吗?
kikicarbonell 2013年

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 2014年

4

在几乎所有的解决方案都说“这对我在X数据库上有效”之后,我看到原始海报的评论是“对Postgres不起作用”。实际上,这里真正的问题可能是对Rails的Postgres支持,它并不是完美无缺的,并且在2009年最初发布此问题时可能更糟。例如,如果我没记错的话,如果您使用的是Postgres,则基本上无法获得有用的输出rake db:schema:dump

我自己不是Postgres忍者,我从Xavier Shay在Postgres上出色的PeepCode视频中获得了此信息。该视频实际上忽略了Aaron Patterson的图书馆,我想是Texticle,但我可能会记错了。但是除此之外,它还很棒。

无论如何,如果您在Postgres上遇到此问题,请查看该解决方案是否可在其他数据库中使用。可能用于rails new生成一个新应用作为沙箱,或者只是创建类似

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

config/database.yml

并且,如果您可以确认这是Postgres支持问题,并且找到了解决方案,请为Rails贡献补丁或将修复程序打包到gem中,因为Rails社区中的Postgres用户群很大,这主要归功于Heroku 。


2
对FUD和不准确性的投票。自2007年以来,我在大多数Rails项目中都使用了Postgres。Rails对Postgres的支持非常出色,并且模式转储器没有问题。
Marnen Laibow-Koser'5

2
一方面,确实Postgres并不是这里的问题。另一方面,这是2009年rails.lighthouseapp.com/projects/8994/tickets/2418中的一个仅含
Giles Bowkett

Rails和Postgres围绕Postgres 8.3更新存在一个确定的问题,他们(正确地是IMO)决定将其转换为字符串文字和引号的SQL标准。Rails适配器不检查Postgres版本,必须暂时修补一下。
Judson 2012年

@贾德森有趣。我从来没有遇到过。
Marnen Laibow-Koser 2012年

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将非null列上的唯一索引转换为主键!


在带有MySQL 5.5.15的Rails 3.22中,它仅创建唯一键,而不创建主键。
lulalala 2012年

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 09年

1
这种方法将省略id列,但实际上不会在表上设置主键。
肖恩·麦克莱里

1

这个解决方案怎么样

在Employee模型内部,为什么不能添加代码来检查列中的唯一性,例如:假设Employee是Model,因为您有EmpId,它是字符串,那么我们可以在EmpId中添加 “:uniqueness => true”

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

我不确定这是否是解决方案,但这对我有用。


1

我知道这是我偶然发现的一个旧线程...但是我很震惊,没有人提到DataMapper。

我发现如果您需要偏离ActiveRecord约定,我发现它是一个不错的选择。同样,这是一种更好的遗留方法,您可以按原样支持数据库。

Ruby Object Mapper(DataMapper 2)也具有很多前景,并且也基于AREL原理构建!


1

添加索引对我有用,我正在使用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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.