新手问题 Rails association 中的 foreign keys

redemption · January 05, 2016 · Last by redemption replied at January 10, 2016 · 4454 hits

问题背景

以 belongs_to association 举例:

在添加 belongs_to 关系的时候,需要在 table 中创建 foreign Keys。比如下面的例子:

class Order < ActiveRecord::Base
  belongs_to :customer
end

建立上面的关系的时候,order table 是如下方式建立的

class CreateOrders < ActiveRecord::Migration
  def change
    create_table :orders do |t|
      t.integer  :customer_id
      ······
    end

    add_index :orders, :customer_id
  end
end

或者是下面的代码

class CreateOrders < ActiveRecord::Migration
  def change
    create_table :orders do |t|
      t.belongs_to :customer, index: true
      ······
    end
  end
end

通过观察 rails 中 belongs_to 方法的源码发现,这两个代码是等价的。

通过观察发现,belongs_to 的 association 中在 table 上创建 foreign keys,也等同于在 table 中建立一个 存放 id 的列,并在该 column 上建立了一个 index。所以这里的 foreign keys 并不是 database 中的 foreign keys。

其他的 association 的 foreign keys 也是同样的建立方法。

问题

  1. 理论上来说是否任何一个 integer 的 column 上建立一个 index 都能成为 rails association 中的 foreign keys 呢?
  2. 这种 foreign keys 是否在功能上代替了 database 中 foreign keys 的作用了呢?在我个人看来好像是的,
  3. 如果这里 foreign keys 在功能上代替了 database 中的 foreign keys 了话,那么在 database 中去建立 foreign keys 还有什么作用呢?如果无法在功能上代替 foreign keys 的话,那为什么上面的例子中不去在 database 中添加 foreign keys 呢?

功能上替代的意思:对于 database 中的 foreign key 的作用,(我认为)只是用于 join table。

我不理解你所说的『database 中 foreign keys 』是指什么?跟 column 上建立一个 index 有什么不同,还是说只是字段命名不一样?

@hanluner database 中的 foreign keys 就是通过 add_foreign_key 这些在 database 中添加的 foreign key。我这里的提的问题我换一个叙述可能容易理解一点。 我们只考虑数据库建模的话。如果我们要表示 order 属于 customer 的关系话。一般的实现方式就是在 order 的 table 中去建立一个 customer_id 的 column,并且通过 sql 语句明确指出这是一个 foreign key。 但是这里 rails model 去实现这个关系的时候,在数据库中的操作只是建立了一个 customer_id 的 column,并为这个 column 建立了一个 index。并没有在数据库层面上明确出这是一个 foreign key(也就是没有通过 sql 语句去说明这个 column 是 foreign key)。

所以我就有了上面 3 个问题

#2 楼 @redemption 我理解你的意思了。Rails 其实并没有使用 MySQL 那种标准的外键,它只是在用它自己定义的这种外键。而 MySQL 中一般是不建议使用外键的。我不知道 Rails 是不是出于这种考虑才会仅仅是定义一个字段加上索引就行了。

@hanluner 那我是不是可以这么理解了。

  • rails 自己实现了一套外键的功能
  • rails 的这种外键功能上是囊括数据库中外键的功能的
  • 数据库的外键与数据库相关性较强,而使用 rails model 实现的外键,可以避免这种依赖

#4 楼 @redemption 我只能说应该是这样,但是实际上的,可能还需要查一查资料了。你提出这个问题是很好的。

#3 楼 @hanluner MySQL 不建议使用外键,没听说过啊,为什么?

可以这样试试:

config.active_record.schema_format = :sql

然后运行 rake db:structure:dump

应该可以看到 native 的DDL

belongs_to 的主要作用是给 Model 加一些方法。

class Order < ActiveRecord::Base
  belongs_to :customer
end

比如这个,会给 Order 加上

Order#customer
Order#build_customer
Order#create_customer

等方法。

index 跟这个应该没有关系。

你可以做一下这个实验,然后把结果发上来:

  1. 添加 belongs_to :nowhere # 看看 schema 里没有 nowhere_id 会发生啥
  2. 添加 nowhere_id 到 schema,不加 index,不建立 nowhere Model,看看 belongs_to 会不会添加方法
  3. 添加 index
  4. 去掉 index,添加 nowhere Model
  5. 加上 index

@qinfanpeng 通过你这种方法,我发现很奇怪的地方。无论是否设置 foreign_key,产生的 sql 是一样的。并没有设置 foreign_key 的 sql 语句。而且我用的数据库是 sqlite,也支持 foreign_key

Model

建立两个 model,Article 与 Comment

class CreateArticles < ActiveRecord::Migration
  def change
    create_table :articles do |t|
      t.string :title
      t.text :text

      t.timestamps null: false
    end
  end
end

class CreateComments < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.string :commenter
      t.text :body
      t.references :article, index: true # ,foreign_key: true

      t.timestamps null: false
    end
  end
end

结果

不设置 foreign_key

CREATE TABLE "schema_migrations" ("version" varchar NOT NULL);
CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
CREATE TABLE "articles" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar, "text" text, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL);
CREATE TABLE "comments" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "commenter" varchar, "body" text, "article_id" integer, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL);
CREATE INDEX "index_comments_on_article_id" ON "comments" ("article_id");
INSERT INTO schema_migrations (version) VALUES ('20160105114731');

INSERT INTO schema_migrations (version) VALUES ('20160105120123');

设置 foreign_key 为 true

CREATE TABLE "schema_migrations" ("version" varchar NOT NULL);
CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
CREATE TABLE "articles" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar, "text" text, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL);
CREATE TABLE "comments" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "commenter" varchar, "body" text, "article_id" integer, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL);
CREATE INDEX "index_comments_on_article_id" ON "comments" ("article_id");
INSERT INTO schema_migrations (version) VALUES ('20160105114731');

INSERT INTO schema_migrations (version) VALUES ('20160105121912');

#8 楼 @emayej

你这个我做了实验,只要添加了 belongs_to,就会存在相应的方法。 而且从输出的 sql 来看,可以猜测 belongs_to 添加的方法就是去寻找相应的 column 中的值,并将该值作为去相关 table 中查找的目标。例如我的 Model 中存在正常的 Article 和 Comment 的关系,当我用 comment 去输出 article 时。

comment.article

其输出的 sql 是:

SELECT  "articles".* FROM "articles" WHERE "articles"."id" = ? LIMIT 1  [["id", 1]]

而当不存在 Nowhere table 的时候,直接返回的是 nil,没有输出 sql 语句。所以我猜想 belongs_to 产生的方法,在具体实现的时候会去检查数据库里面是否有相应的 table。

#10 楼 @redemption 看起来 Active Record 并不会在 SQL 里面添加 foreign key constraint。 belongs_to 在实现时应该首先去查找当前 table 是否有对应的 column。 foreign_key: 这个 option 应该是用来指定 column name 的。

@emayej @hanluner @nowherekai @qinfanpeng 我去研究一下相关源码,看看到底是怎么回事。谢谢大家了

还有就是可以试试用 MySQL Workbench 这些的 GUI 工具,里面可以自动画出各个表之间的关联关系,看看能否识别出 Rails 建立的这种“外键”关系。

#6 楼 @nowherekai 你去看下 MySQL 的外键生成以及其他一些资料就可以理解了。Rails 的外键跟 MySQL 的是不一样。

之前 rails 的 references 关联的确没有做数据库层面的外键约束,rails 偏向使用 validation (rails level) 处理类似的约束。

但 rails 4.2 增加这个功能,可以去看看 add_foreign_key 这个 api.

数据库里面的外键是为了保证数据完整性。 Rails 设置外键是为了在 join 表的指定 column

#9 楼 @redemption 简单点说就是没必要。 如果 Rails 自己能保证外键约束,为什么还要多此一举让数据库去管外键呢? 再加上数据库的外键可能各个系统设计上会不同,甚至还会遇到不支持外键的数据库,总不能为这些数据库分别写几套适配器吧。

性能上两种方式差别大吗?

#18 楼 @redemption 补充一点:

As we work toward the end of this book’s coverage of Active Record, you might have noticed that we haven’t really touched on a subject of particular importance to many programmers: foreign-key constraints in the database. That’s mainly because use of foreign-key constraints simply isn’t the Rails way to tackle the problem of relational integrity. To put it mildly, that opinion is controversial and some developers have written off Rails (and its authors) for expressing it. There really isn’t anything stopping you from adding foreign-key constraints to your database tables, although you’d do well to wait until after the bulk of development is done. The exception, of course, is those polymorphic associations, which are probably the most extreme manifestation of the Rails opinion against foreign-key constraints. Unless you’re armed for battle, you might not want to broach that particular subject with your DBA.

---- 摘自《The Rails 4 Way》

You need to Sign in before reply, if you don't have an account, please Sign up first.