内容


跨越边界

Rails 迁移

重新思考数据库模式变化

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 跨越边界

敬请期待该系列的后续内容。

此内容是该系列的一部分:跨越边界

敬请期待该系列的后续内容。

作为喜欢冒险的摩托车手,我关注两个严肃的社区:山地摩托车手和公路摩托车手。常规的看法是山地摩托车手更危险,但我并不同意。公路摩托手必须考虑要比石头和树危险得多的障碍:汽车。类似的异议也出现在面向对象应用程序开发使用的两个持久性策略之间。

目前,持久性框架使用两种方法中的一种: 映射包装 。映射解决方案允许创建独立的数据库模式和对象模型,然后用一个软件层来管理两者间的差异。映射解决方案试图构建一个与数据库模式的结构非常相似的对象模型。与之相反,包装解决方案用对象作为数据库表和行的包装器来操纵数据库中的数据。常规的想法认为在解决方案发布之后,映射解决方案通常更灵活,因为映射软件能够更好地处理模式或对象模型中的变化。但是这个想法忽略了其中最重要的部分:数据。要有效地管理涉及持久性域模型的应用程序变化,必须协调数据、模式和模型的变化。大多数项目团队做不到这一点。

开发团队处理模式变化时,通常会用 SQL 脚本从头开始生成一个新版模式。脚本可能会删除所有的表,再添加所有的表。这样的策略会破坏所有测试数据,因此对于生产场景来说毫无价值。偶尔,工具可能会创建脚本,由脚本生成 delta 模式,或者生成使用 alter_table 这样的 SQL 命令修改以前版本的模式。但是很少有团队会费力气创建能取消模式变化的脚本,而且成功地创建了处理数据变化的自动脚本的团队更少。简而言之,传统的映射策略忽略公路上的汽车:回退坏的模式变化并处理数据。

这篇文章深入研究了 Ruby on Rails 迁移 —— Rails 处理生产数据库变化的解决方案。迁移用包装的方式组合了协调模式变化和数据变化的威力和简单性。(如果以前没有学习过活动记录 —— Rails 底层的持久性层,我建议您先阅读 跨越边界 系列中以前的这篇 Rails 文章 。)

在 Java 编程中处理模式变化

基于映射的框架需要模式、模型和映射。这样的框架是重复性的。请想像一下指定一个属性需要重复多少次:

  • 模型中的 getter
  • 模型中的 setter
  • 模型中的实例变量
  • 映射的 “to” 端
  • 映射的 “from” 端
  • 列定义

公平地讲,Hibernate 这样的 Java 框架通过代码生成的方式消除了不少重复工作。对象关系映射器可以处理遗留模式,对于新的数据库模式,可以用 Hibernate 提供的工具直接从模型生成模式并用 IDE 生成 getter 和 setter。可以用 Java 标注把映射嵌入域模型,但是按我的观点,这在一定程度上违背了使用映射的初衷。这种代码生成技术还有另一个用途:模式迁移。有些代码生成工具可以发现新的域模型和旧的模式之间的区别,并生成沟通这些不同的 SQL 脚本。请记住这些脚本处理的是模式,而不是数据。

例如,考虑这样一个迁移:把 first_namelast_name 这两个数据库列合并成叫作 name 的单一列。来自典型 Java 持久性框架的工具对数据库管理员没有帮助,因为这些工具只处理问题的一部分:模式中的变化。在进行这个模式变化时,还需要能够处理现有数据。在需要部署这个假想应用程序的新版本时,数据库管理员通常必须手工创建 SQL 脚本来完成以下工作:

  • 创建叫作 name 的新列。
  • first_namelast_name 的数据放到新列中。
  • 删除 first_namelast_name 列。

如果造成模式变化的代码版本仍然处在不完善的状态,那么经常必须手工回退变化。只有很少的团队有这个素养可以集成并自动进行跨模型、模式和数据的变化。

Rails 迁移基础

在 Rails 中,所有模式变化 —— 包括模式最初的创建 —— 都在迁移中发生。数据库模式中的每个变化都有自己的迁移对象,迁移对象包装了前进和后退。清单 1 显示了一个空迁移:

清单 1. 空迁移
class EmptyMigration < ActiveRecord::Migration
  def self.up
  end

  def self.down
  end
end

我很快将介绍如何调用迁移,但是现在请看看清单 1 中的迁移结构。在这个迁移的 up 方法中,要放置进行一个逻辑数据库变化所需的全部代码。还要捕获任何变化,从而能够取消模式变化。通过封装 updown ,Rails 开发和生产工具可以自动进行涉及持久性对象模型的变化的部署和回退过程。这些数据库变化可能包括:

  • 添加或删除新表。
  • 添加或删除新列。
  • 以其他方式修改数据库,包括添加、删除或修改索引或其他约束。
  • 修改数据库数据。

通过允许改变数据,迁移大大简化了相关数据和模式的变化的同步过程。例如,可以添加一个查询表,把每个州和州的两位数字 ZIP 代码关联起来。在迁移中,可以填充数据库表,可能通过调用 SQL 脚本或装载 fixture。如果迁移正确,那么每个迁移都会把数据库置于一个一致的状态,不需要手工干预。

每个迁移的文件名都以一个惟一的编号开头。这个约定让 Rails 可以对迁移实现很强的排序。用这个策略,可以前后转移到逻辑数据库模式的任何状态。

使用迁移

要使用迁移,只需要一个 Rails 项目和一个数据库。如果想试验这里的代码,请安装一个关系数据库管理器、Ruby 和 Rails 1.1 以上版本。就可以开始了。请按以下步骤创建数据库支持的 Rails 项目:

  1. 输入 rails blog ,创建叫作 blog 的 Rails 项目。
  2. 创建叫作 blog_development 的数据库。如果使用 MySQL,只需在 MySQL 命令行上输入 create database blog_development
  3. 通过 config/database.yml 对数据库做必要的配置,添加数据库登录 ID 和口令。

要查看编号的工作方式,请生成一个迁移:

  1. 在 blog 目录,输入 ruby script/generate migration create_blog 。(如果正在运行 Unix,可以省略 ruby 。从现在起我就省略它。)
  2. 输入 script/generate migration create_user 。(请看看 blog/db/migrate 中的文件,会看到两个顺序编号的文件。迁移生成器负责管理编号。)
  3. 删除叫作 001_create_blog.rb 的迁移,并用 script/generate migration create_blog 重新创建它。可以注意到,新创建的迁移是 003_create_blog.rb,如清单 2 所示:
清单 2. 生成迁移
> cd blog
> script/generate migration create_blog
      create  db/migrate
      create  db/migrate/001_create_blog.rb
> script/generate migration create_user
      exists  db/migrate
      create  db/migrate/002_create_user.rb
> ls db/migrate/  
001_create_blog.rb      002_create_user.rb
> rm db/migrate/001_create_blog.rb 
> script/generate migration create_blog
      exists  db/migrate
      create  db/migrate/003_create_blog.rb
> ls db/migrate/
002_create_user.rb      003_create_blog.rb

可以看到每个迁移的数字前缀。新迁移的编号是目录中的最大前缀加 1。这个策略保证了迁移按顺序生成并执行。在其他迁移的结果之上构建的迁移(例如一个迁移要向其他迁移创建的表中添加一列)会保持一致。编号机制简单、直观而一致。

要查看迁移在数据库中的工作方式,请删除 db/migrations 目录中的全部迁移。输入 script/generate model Article ,生成 Article 的模型对象以及与 清单 1 中的迁移类似的空迁移。请注意,Rails 为每篇文章生成了模型对象和迁移。请把 db/migrate/001_create_articles.rb 编辑成清单 3 这样:

清单 3. CreateArticles 的迁移
class CreateArticles < ActiveRecord::Migration
  def self.up
    create_table :articles do |t|
      t.column :name, :string, :limit => 80
      t.column :author, :string, :limit => 40
      t.column :body, :text
      t.column :created_on, :datetime
    end
  end

  def self.down
    drop_table :articles
  end
end

前后迁移

要准确地看到迁移做的工作,只需运行迁移并查看数据库。在 blog 目录中,输入 rake migrate 。( rake 是 Ruby 上与 C 平台的 make 或 Java 平台的 ant 等价的东西。) migrate 是一个 rake 任务。

接下来,显示数据库中的表。如果使用 MySQL,只需进入 mysql> 命令提示符,输入 use blog_development; ,然后输入 show table; ,就可以看到清单 4 的结果:

清单 4. Rails 迁移创建的 schema_info 表
mysql> show tables;
+----------------------------+
| Tables_in_blog_development |
+----------------------------+
| articles                   |
| schema_info                |
+----------------------------+
2 rows in set (0.00 sec)

mysql> select * from schema_info;
+---------+
| version |
+---------+
|       1 |
+---------+
1 row in set (0.00 sec)

请注意第二个表: schema_info 。我的迁移指定了 articles 表,但是 rake migrate 命令自动创建了 schema_info 。请执行 select * from schema_info

不带参数运行 rake migrate 时,就是要求 Rails 运行所有还没有应用的迁移。Rails 做以下工作:

  • 如果 schema_info 表还不存在,就创建这个表。
  • 如果 schema_info 中没有行,就用值 0 插入一行。
  • 运行编号大于当前迁移的所有迁移的 up 方法。 rake 通过读取 schema_info 表中 version 列的值,判断当前迁移的编号。 rake 按照编号从小到大运行 up 迁移,从大到小运行 down 迁移。

要向下迁移,只要带着版本号运行 rake migrate 即可。向下迁移会破坏数据,所以要小心。有些操作(例如删除表或列)也会破坏数据。清单 5 显示了向下迁移然后回退的结果。可以看到 schema_info 忠实地跟踪当前版本号。这种方式完成了一项精彩的工作:允许在代表不同开发阶段的模式之间平滑地移动。

清单 5. 向下迁移
> rake migrate VERSION=0
(in /Users/batate/rails/blog)
== CreateArticles: reverting ==================================================
-drop_table(:articles)
   -> 0.1320s
== CreateArticles: reverted (0.1322s) =========================================

> mysql -u root blog_development;
mysql> show tables;
+----------------------------+
| Tables_in_blog_development |
+----------------------------+
| schema_info                |
+----------------------------+
1 row in set (0.00 sec)

mysql> select * from schema_info;
+---------+
| version |
+---------+
|       0 |
+---------+
1 row in set (0.00 sec)

mysql> exit
Bye
> rake migrate
(in /Users/batate/rails/blog)
== CreateArticles: migrating ==================================================
-create_table(:articles)
   -> 0.0879s
== CreateArticles: migrated (0.0881s) =========================================

> mysql -u root blog_development;
mysql> select * from schema_info;
+---------+
| version |
+---------+
|       1 |
+---------+
1 row in set (0.00 sec)

现在要打开表本身了。请看 清单 3 和表定义。如果使用 MySQL,可以执行 show create table articles; 命令,生成清单 6 的结果:

清单 6. articles 的表定义
mysql> show create table articles;
+----------+...-----------------+
| Table    | Create Table |
+----------+...-----------------+
| articles | CREATE TABLE 'articles' (
  'id' int(11) NOT NULL auto_increment,
  'name' varchar(80) default NULL,
  'author' varchar(40) default NULL,
  'body' text,
  'created_on' datetime default NULL,
  PRIMARY KEY  ('id')
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+----------+...-----------------+
1 row in set (0.00 sec)

可以看到,这个表定义的大部分都直接来自迁移。Rails 迁移的一个核心优势就是不需要使用直接的 SQL 语法来创建表。由于在 Ruby 中处理每个模式修改,所以生成的 SQL 是独立于数据库的。但是请注意 id 列。虽然没有指定这个列,但是 Rails 迁移会自动创建它,并具有 auto_incrementNOT NULL 属性。具有这个特殊列定义的 id 列符合 Rails 标识符列的规范。如果想创建这个表,但不要 id ,迁移只需添加 :id 选项,如清单 7 的迁移所示:

清单 7. 创建没有 id 列的表
  def up
    create_table :articles, :id => false do |t| 
      ...
    end
  end

现在已经深入研究了单一迁移,但是还没有在模式中进行变化。现在要创建另一个表,这次是用于评论的表。请输入 script/generate model Comment 生成叫作 Comment 的模型。把 db/migrate/002_create_comments.rb 生成的迁移编辑成像清单 8 一样。还需要一个有几个列的新表,还要利用 Rails 的功能添加非空列和默认值。

清单 8. 用于评论的第二个迁移
class CreateComments < ActiveRecord::Migration
  def self.up
    create_table :comments do |t|
      t.column :name, :string, :limit => 40, :null => false
      t.column :body, :text
      t.column :author, :string, :limit => 40, :default => 'Anonymous coward'
      t.column :article_id, :integer
    end
  end

  def self.down
    drop_table :comments
  end
end

运行这个迁移。如果在迁移过程中出错,只要记住迁移的工作方式即可。需要检查 schema_info 表中行的值,并查看数据库的状态。在纠正代码之后,可能需要手工删除某些表,或者修改 schema_info 中行的值。请记住,并没有发生什么魔术。Rails 运行所有还没运行的迁移上的 up 方法。如果要添加的表或列已经存在,那么操作就会失败,所以需要确保迁移在一致的状态下运行。至于现在,请运行 rake migrate 。清单 9 显示了结果:

清单 9. 运行第二个迁移
> rake migrate(in /Users/batate/rails/blog)
== CreateComments: migrating ==================================================
-create_table(:comments)
   -> 0.0700s
== CreateComments: migrated (0.0702s) =========================================

> mysql -u root blog_development;
mysql> select * from schema_info;
+---------+
| version |
+---------+
|       2 |
+---------+
1 row in set (0.00 sec)

迁移可以处理许多不同类型的模式变化。可以添加和删除索引;通过删除、改名或添加列来修改表;甚至在必要的时候借助于 SQL。用 SQL 能做什么,用迁移就能做什么。Rails 对大多数常见操作都有包装器,包括:

  • 创建表( create_table
  • 删除表( drop_table
  • 向表中添加列( add_column
  • 从表中删除列( remove_column
  • 给列改名( rename_column
  • 修改列( change_column
  • 创建索引( create_index
  • 删除索引( drop_index

有些迁移修改不只一个列,形成数据库中单一的逻辑变化。请考虑这个迁移:添加顶级 blog,带有属于这个 blog 的文章。需要创建一个新表,还要添加指向 blog 的每个文章的外键。清单 10 显示了完整的迁移。可以输入 rake migrate 来运行迁移。

清单 10. 创建表并添加列的迁移
class CreateBlogs < ActiveRecord::Migration
  def self.up
    create_table :blogs do |t|
      t.column :name, :string, :limit => 40;
    end
    add_column "articles", "blog_id", :integer
  end

  def self.down
    drop_table :blogs
    remove_column "articles", "blog_id"
  end
end

还有数据

迄今为止,我只把重点放在模式的变化上,但是数据的变化也是重要的。有些数据库变化要求数据和模式一起变化,有些数据变化要求逻辑变化。例如,假设想为每篇 blog 文章创建一个新评论,表明文章是对评论开放的。如果在 blog 已经开放一段时间之后才实施这个变化,那么希望只对还没有评论的文章添加新评论。用迁移可以容易地进行这个变化,因为迁移可以访问对象模型并根据模型的状态进行决策。请输入 script/generate migration add_open_for_comments 。需要对评论进行修改,捕捉 belongs_to 关系,并编写新的迁移。清单 11 显示了模型对象和新的迁移:

清单 11. 模型对象和新迁移
class AddOpenForComments < ActiveRecord::Migration
  def self.up
    Article.find_all.each do |article|
      if article.comments.size == 0
        Comment.new do |comment|
          comment.name = 'Welcome.'
          comment.body = "Article '#{article.name}' is open for comments."
          article.comments << comment
          comment.save
          article.save
        end
      end
    end
  end

  def self.down
  end
end

对于清单 11 中的迁移,做了个战术性决策。它认定用户在加入之后不想看到欢迎信息消失,所以选择向下迁移时不删除任何记录。在迁移中解决数据变化的能力是个强大的工具。可以同步数据和模式中的变化,也可以解决涉及对模型对象进行逻辑操作的数据变化。

我已经演示了迁移中能做的大部分工作。还可以利用其他一些工具。如果想对现有数据库使用迁移,可以用 rake schema_dump 对现在的模式做个快照。这个 rake 任务在 db/schema.rb 中用正确的迁移语法创建 Ruby 模式。然后可以生成迁移,并把导出的模式拷贝进迁移。(请参阅 参考资料 获得更多细节。)我没有谈到测试 fixture,在设置测试数据或填充数据库时它们会很有帮助。请参阅 跨越边界 系列中以前关于单元测试的 文章 获得更多细节。

最终比较

Java 编程的迁移方案并不强壮。有些产品对于有些模式迁移问题有针对性强的解决方案,但是没有协调模式变化的系统化过程 —— 包括前进和后退 —— 处理数据和对象模型中的变化会是项艰难的任务。Rails 解决方案有一些核心优势:

  • Rails 迁移是 DRY 的(不重复你自己)。使用 Rails 时,只要准确地指定每个列的定义一次。其他一些映射器要求指定一个列六次:在模式、 getter、setter、模型的实例变量、“from” 映射和 “to” 映射中。
  • Rails 迁移既支持数据迁移,也支持模式迁移。
  • Rails 迁移支持把模型逻辑用于数据迁移,而 SQL 脚本做不到这一点。
  • Rails 迁移独立于数据库,但 SQL 脚本不独立。
  • Rails 迁移允许对不支持的扩展(例如存储过程或约束)直接使用 SQL,而有些 ORM 映射器不支持。

迁移有这么多好处,您可能以为会有复杂的代码段,但实际上它们出奇的简单。迁移具备有意义的名称和版本编号。每个迁移都有 updown 方法。最后, rake 协调它们以正确的顺序运行。这个简单的策略是革命性的。不在模型中表达每个模式变化而是在独立的迁移中表达,这个概念既优雅又有效。协调数据和模式变化是另一个理念的变化,而且是个有效的变化。更好的是,这些想法完全不依赖于语言。如果要构建新的 Java 包装框架,最好考虑迁移。

在这个系列的下一篇文章中,您将看到新的 Ajax 和 Web 服务支持的框架,它充分利用了 Rails 中的元编程。届时请敞开心灵继续跨越边界。


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • Beyond Java (Bruce Tate,O'Reilly,2005):作者关于 Java 语言的兴起、平台期以及其他能够在某些领域挑战 Java 语言的技术的一本书。
  • ActiveRecord::Migration :关于 ActiveRecord::Migration 的 Rails API 文档,这是了解迁移的最新功能的好地方。
  • Book review: Agile Web Development with Rails ”(Darren Torpey,developerWorks,2005 年 5 月):深入研究这本书可以加深读者对 Rails 和敏捷开发方式背后的逻辑的理解。
  • Understanding Migrations :Rails Wiki 上有一份关于迁移的良好概述,是代码之外最新的信息源。
  • From Java To Ruby: Things Every Manager Should Know (Pragmatic Bookshelf,2006):作者的书,介绍了什么时候和在哪里从 Java 编程转向 Ruby on Rails 才有意义,以及如何进行转换。
  • Programming Ruby (Dave Thomas 等,Pragmatic Bookshelf,2005):关于 Ruby 编程的流行书。
  • Ruby on Rails :下载开放源码的 Ruby on Rails Web 框架。
  • Ruby :从项目的 Web 站点得到 Ruby。
  • Java 技术专区 :数百份 Java 编程各方面的文章。

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=162918
ArticleTitle=跨越边界: Rails 迁移
publish-date=09252006