如何在Rails中将主键设置为非整数类型列?

88
我正在使用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为我生成主键约束(我使用的是PostgreSQL),而不必在execute调用中编写SQL语句?注意:我知道使用字符串列作为主键不是最好的选择,所以请不要回答只是让我添加一个整数主键。也许我还会添加一个,但这个问题仍然很重要。

我有同样的问题,很想看到这个问题的答案。到目前为止,所有的建议都没有起作用。 - Sean McCleary
1
如果你正在使用Postgres,它们中没有一个会起作用。Dan Chak的《企业Rails》提供了一些有关使用自然/复合键的技巧。 - Azeem.Butt
请注意,当您使用以下内容时:<!-- language: lang-rb --> execute "ALTER TABLE employees ADD PRIMARY KEY (emp_id);" 虽然在运行 rake db:migrate 后表约束设置正确,但自动生成的模式定义不包含此约束! - pymkin
14个回答

108

不幸的是,我已经确定没有使用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);"

正如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
20
现在应该使用 self.primary_key= 代替 set_primary_key,因为后者已被弃用。 - Gary S. Weaver
2
这是我找到的唯一有效的解决方案。希望能对其他人有所帮助:https://dev59.com/OnM_5IYBdhLWcg3w2XFg#15297616 - findchris
8
这种方法的问题在于,当你从 schema.rb 创建数据库时,emp_id 列将再次成为 integer 类型。似乎 schema.rb 无法以任何方式存储 execute "ALTER TABLE employees ADD PRIMARY KEY (emp_id);" - Tintin81
4
您可以通过设置config.active_record.schema_format来将架构转储从 schema.rb 切换到 structure.sql,可选项为 :sql:ruby。详情请参考:http://guides.rubyonrails.org/v3.2.13/migrations.html#what-are-schema-files-for - Svilen Ivanov
显示剩余2条评论

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 - Austin
6
不幸的是,这种方法也会使我们得到一个与之不同步的 schema.rb ... 它将保留 :primary_key => :emp_id 声明,但当调用 rake db:schema:load 时(在测试开始时),它将生成一个整数列。然而,如果您切换使用 structure.sql(配置选项),则测试将保留设置并使用该文件加载模式。 - Tom Harrison

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


11
在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

10

我已经在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 生成数据库时,主键将被创建为整数。 - Peter Alfvin

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
嗯,有趣。我已经在 Rails 2.3.2 上使用 MySQL 进行了测试——这可能与 Rails 版本有关,不是吗? - paulthenerd
我不确定——我认为这不是Rails的版本问题。我正在使用Rails 2.3.3。 - Rudd Zwolinski
很不幸,使用这种方法并没有为该表设置实际的主键。 - Sean McCleary
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已更改为整数...您有任何解决方案吗? - enrique-carbonell
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上对我没有用”。实际问题可能是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
对于恐吓和不准确的信息进行负面评价。自2007年以来,我在大多数Rails项目中都使用Postgres。Rails中对Postgres的支持非常出色,并且模式转储器没有任何问题。 - Marnen Laibow-Koser
2
一方面,Postgres 在这里并不是问题。另一方面,这里有一个来自 2009 年的仅适用于 pg 的模式转储程序错误 https://rails.lighthouseapp.com/projects/8994/tickets/2418,还有另一个 https://rails.lighthouseapp.com/projects/8994/tickets/2514。 - Giles Bowkett
Rails和Postgres之间存在一个明显的问题,涉及到Postgres 8.3更新,他们(在我看来是正确的)决定改用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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接