为什么FleaPHP使用Table Data Gateway代替Active Record提供数据库

许多开发者很疑惑为什么 FleaPHP 以高效开发为目标,却没有提供 Active
Record 模式。本文尝试详细阐述这个问题。Active Record 是什么? Active
Record 模式中文名为活动记录,在《企业应用架构模式》一书中定义如下:
活动记录:一个对象,它包装数据库表或视图中的某一行,封装数据库访问,并在这些数据上增加了领域逻辑。
举个例子来说,一个图书数据表,每一条记录就是一本图书的信息。那么采用
Active Record 时,每一本图书就是一个 Active Record 对象实例。Active
Record 因 Ruby On Rails 而流行 Active Record
之所以现在这么炙手可热,甚至许多人将 Active Record 和 ORM 划等号,完全是
Ruby On Rails 的原因。在 Ruby On Rails 中,Active Record
除了最基本的将数据记录和一个对象互相映射外,还提供了数据间关联关系的处理。例如:
一本图书有一个或者多个作者,所以每一个图书对象都和多个作者对象关联。反过来一个作者可以写多本书,所以一个作者对象也和多个图书对象关联。
在 RoR
中,我们获取一个图书对象时,自动就获得了该图书对象所对应的作者对象。更进一步,通过图书对象关联的作者对象,我们
可以获取该作者所写的所有图书的对象实例。而这些工作,在 RoR
中只需要几行代码而已,以前我们需要写上一大段代码才能实现同样的效果。 RoR
中,对 Active Record 模式的实现完全利用了 Ruby
语言的灵活性,简短几行代码就可以定义一个关联。并且通过复杂的
ActiveRecord:Base 对象,提供了 CRUD操作的默认处理。所以使用 RoR
时,绝大部分常见的数据库操作只需要很少量的代码就可以完成,大大提高了开发效率。
但 Active Record 模式也不是完美的,Active Record 存在不少缺点。 *
Active Record 模式需要数据表结构和对象属性一一对应,否则将难以使用
Active Record 模式; * Active Record
模式并不能够真正适合完全面向对象的应用程序。因为 Active Record
模式本质上就要求一个对象必须和一个数据表对应。但在完全面向对象的应用程序中,数据和操作数据的方法很可能分布在各个不同的对象中,这些对象却并没有和
某一个数据表完全对应,而且 Active Record
无法很好的处理对象的继承、聚合等面向对象常见的对象间关系; * 随着逐渐向
Active Record 添加业务逻辑,Active Record 对象中会混入越来越多的 SQL
语句,这在更复杂的项目中显然是一个不利因素。 如果在 Active Record
模式中添加了对数据关系的处理,那么还要注意性能问题: 假如一个 Active
Record
对象有多个关联。那么我取出一个对象时,很可能就连带取出了其他不少对象。但这些对象可能根本就是本次操作用不上的。其次,将对象更新到数据库时,也需要对关联的对象进行处理,否则对关联对象的修改就会丢失。
虽然可以用各种技巧来避免这些情况,但毫无疑问需要开发者对 RoR 的 Active
Record
很熟悉才行。否则看上去很简单的代码,背后则会是噩梦般的数据库操作。
其次,假设我们要将数据库中每本书的单价减半,那么采用 Active Record
模式时,就必须首先读取所有的记录并实例化为对象,然后更新对象属性,再写回数据库。可想而知这样会有多差的效率。
当然了,实际开发中没有人会这样做。开发者会编写一个单独的方法,用一条 SQL
语句完成对批量数据的更新。但也说明 Active Record
模式不适合批量处理数据,而现实世界中,批量处理数据的需求随处可见。
不过由于 RoR 对开发效率戏剧性的提高,所以对于追求开发效率的项目,RoR
是一个很不错的选择。而且性能上的不足可以通过更新硬件或者配合其他技术手段来改善。因此在现实世界中,37signals.com
公司的所有基于 RoR 开发的应用,都获得了良好的性能表现。Active Record 与
ORM 许多人将 Active Record 与 ORM 划等号,这是错误的。

模型

一、Active Record 基础
介绍Models,数据库持久性以及Active Record模式

  1. Active Record是什么?
    Active Record 是 MVC 中的 M(模型),负责处理数据和业务逻辑。
    注意:这里业务逻辑在Model处理。
    Active Record负责创建和使用需要持久存入数据库的数据。Active
    Record实现了Active Record模式,是一种对象关系映射系统。

1-1)Active Record模式
在Active
Record模式中,对象中既有持久存储的数据,也有针对数据库的操作。Active
Record模式吧数据存取逻辑作为对象的一部分,处理对象的用户知道如何把数据写入数据库,还知道如何从数据库取出数据。

1-2)对象关系映射
对象关系映射(ORM)Object-Relational-Mapping
是一种技术手段,吧应用中的对象和关系型数据库中的数据表连接起来。使用ORM,应用中对象的属性和对象之间的关系可以通过一种简单的方法从数据库中获取,无需直接编写SQL语句,也不过度依赖特定的数据库种类。

1-3)用作ORM框架的Active Record
Active Record作用:

  • 表示模型和其中的数据
  • 表示模型之间的关系
  • 通过相关联的模型表示继承层次结构
  • 持久存入数据库之前,验证模型
  • 以面向对象的方式处理数据库操作

2)Active Record中的约定大于配置原则

使用其他编程语言或框架开发应用时,可能必须要编写很多配置代码。大多数 ORM
框架都是这样。但是,如果遵循 Rails 的约定,创建 Active Record
模型时不用做多少配置(有时甚至完全不用配置)。Rails
的理念是,如果大多数情况下都要使用相同的方式配置应用,那么就应该把这定为默认的方式。所以,只有约定无法满足要求时,才要额外配置。

2-1)命名约定
默认情况下,Active
Record使用一些命名约定,查找模型和数据库表之间的映射关系。Rails吧模型的类名转换为复数,然后查找对应的数据表。
eg:

模型类名为Book, 数据库表 books

Rails提供的单复数转换功能很强大,常见和不常见的转换方式都能处理。

模型类:单数,每个单词的首写字母应该大写 BookClub
数据库表: 复数,下划线分隔单词 book_clubs

图片.png

2-2)模式约定
根据字段的作用不同,Active
Record对数据库表中的字段命名也做了相应的约定:

  • 外键:使用 singularized_table_name_id 形式命名,例如
    item_id,order_id。创建模型关联后,Active Record 会查找这个字段;

  • 主键:默认情况下,Active
    Record使用整数字段id作为表的主键。使用Active
    Record迁移创建数据库表时,会自动创建这个字段。

还有一些可选的字段,能为 Active Record 实例添加更多的功能:

  • created_at:创建记录时,自动设为当前的日期和时间;

  • updated_at:更新记录时,自动设为当前的日期和时间;

  • lock_version:在模型中添加乐观锁;

  • type:让模型使用单表继承;

  • (association_name)_type:存储多态关联的类型;

  • (table_name)_count:缓存所关联对象的数量。比如说,一个 Article
    有多个 Comment,那么 comments_count 列存储各篇文章现有的评论数量;

虽然这些字段是可选的,但在 Active Record
中是被保留的。如果想使用相应的功能,就不要把这些保留字段用作其他用途。例如,type
这个保留字段是用来指定数据库表使用单表继承(Single Table
Inheritance,STI)的。如果不用单表继承,请使用其他的名称,例如“context”,这也能表明数据的作用。

3)创建Active Record模型

创建 Active Record 模型的过程很简单,只要继承 ApplicationRecord
类就行了:

class Product < ApplicationRecord

end

这代码会创建Product模型,对应于数据库中的products表。
上面的代码会创建 Product 模型,对应于数据库中的 products
表。同时,products 表中的字段也映射到 Product 模型实例的属性上。假如
products 表由下面的 SQL 语句创建:

CREATE TABLE products (
   id int(11) NOT NULL auto_increment,
   name varchar(255),
   PRIMARY KEY  (id)
);

按照这样的数据表结构,可以编写下面的代码:

p = Product.new
p.name = "Some Book"
puts p.name # "Some Book"

4)覆盖命名约定

如果想使用其他的命名约定,或者在Rails应用张使用即有的数据库可以吗?没问题,默认的约定能够轻易覆盖。

ApplicationRecord 继承自
ActiveRecord::Base,后者定义了一系列有用的方法。使用
ActiveRecord::Base.table_name= 方法可以指定要使用的表名:

class Product < ApplicationRecord
  self.table_name = "my_products"
end

如果这么做,还要调用 set_fixture_class
方法,手动指定固件(my_products.yml)的类名:

class ProductTest < ActiveSupport::TestCase
  set_fixture_class my_products: Product
  fixtures :my_products
  ...
end

还可以使用 ActiveRecord::Base.primary_key= 方法指定表的主键:

class Product < ApplicationRecord
  self.primary_key = "product_id"
end

5)CRUD:读写数据

CURD 是四种数据操作的简称:C 表示创建,R 表示读取,U 表示更新,D
表示删除。Active Record 自动创建了处理数据表中数据的方法。增删改查。

5-1)create增
Active Record
对象可以使用散列创建,在块中创建,或者创建后手动设置属性。new
方法创建一个新对象,create 方法创建新对象,并将其存入数据库。
例如,User 模型中有两个属性,name 和 occupation。调用 create
方法会创建一个新记录,并将其存入数据库:

user = User.create(name: "David", occupation: "Code Artist")

new 方法实例化一个新对象,但不保存:

user = User.new
user.name = "David"
user.occupation = "Code Artist"

调用 user.save 可以把记录存入数据库。

如果在 create 和 new
方法中使用块,会把新创建的对象拉入块中,初始化对象:

user = User.new do |u|
  u.name = "David"
  u.occupation = "Code Artist"
end

5-2)query查
Active Record 为读取数据库中的数据提供了丰富的 API。
返回所有集合:

users=User.all

返回第一个:

user=User.first

返回第一个名为David用户:

davidUser = User.find_by(name:'David')

查找所有名字为David,职业为Code Artists的用户,而且按照created_at
反向排序:

users=User.find_by(name:'David', occupation:'Code Artists').order(created_at::desc)

5-3)Update更新

检索到Active Record对象后,可以修改其属性,然后再将其存入数据库。

user=User.find_by(name:'David')
user.name=‘Dave’
user.save

还有种使用散列的简写方式,指定属性名和属性值,例如:

user=User.find_by(name:'David')
user.update(name:'Dave')

一次更新多个属性时使用这种方法最方便。如果想批量更新多个记录,可以使用类方法update_all:

User.update_all "max_login_attempts=3, must_change_password='true'"

5-4)Delete删除

检索到Active Record对象后还可以将其销毁,从数据库中删除。

user=User.find_by(name:'David')
user.destroy

6)数据验证

在存入数据库之前,Active
Record还可以验证模型。模型验证有很多方法,可以检查属性值是否不为空,是否是唯一,没有在数据库中出现过,等等。

吧数据存入数据库之前进行验证是是否重要的步骤,所以调用save和update方法时都会做数据验证。验证失败时返回false,此时不会对数据库做任何操作。这2个方法都有对应的爆炸方法(save!和update!)。爆炸方法要严格一些,如果验证失败,就会抛出ActiveRecord::Recordnvalid异常。

class User < ApplicationRecord
  validates :name, presence: true
end

user = User.new
user.save  # => false
user.save! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank

注意:验证是在写class的时候,预定了validates

7)回调
Active
Record回调用于在模型生命周期的特定时间上绑定代码,相应的事件发生时,执行绑定的代码。例如创建新记录,更新记录时,删除记录时,等等。

8)迁移
Rails提供了一个DSL(Domain-Special
Language)用来处理数据库模式,叫作”迁移“。
注意:什么是数据库模式?
模式(schema)是数据库体系结构中的一个节点
对于SQL Server数据库来说:
访问一个具体的表,可以由4个部分组成:

服务器名+数据库名+模式名+表名

对于访问本地的数据库:

因为服务器已经连接上了,因此不用指定
数据库名,通过use 数据库名 指定了
模式名,如果不指定的话,数据库默认使用dbo模式

模式(schema) 是用于 在一个 大项目中的 各个 小项目
每个 小项目的表, 放在 各自的 模式(schema) 下面.
这样, 遇到 小项目里面. 有 相同名字的 表的话, 不会发生冲突.

例如一个 公司的 系统.
里面分2个 子系统, 分别为 财务系统人力资源系统.
这2个 子系统, 共用一个数据库
那么 财务系统的表, 可以放在 财务的 模式(schema).
人力资源系统的表,放在 人力资源系统的模式里面。
这2个 子系统, 能够 互相访问 对方的表
但是又不因为 表重名 的问题,影响对方。

迁移的代码存储在特定的文件中,通过 rails 命令执行。可以用在 Active
Record 支持的所有数据库上。下面这个迁移新建一个表:

class CreatePublications < ActiveRecord::Migration[5.0]
  def change
    create_table :publications do |t|
      t.string :title
      t.text :description
      t.references :publication_type
      t.integer :publisher_id
      t.string :publisher_type
      t.boolean :single_issue

      t.timestamps
    end
    add_index :publications, :publication_type_id
  end
end

Rails
会跟踪哪些迁移已经应用到数据库上,还提供了回滚功能。为了创建表,要执行
rails db:migrate 命令。如果想回滚,则执行 rails db:rollback 命令。

注意,上面的代码与具体的数据库种类无关,可用于 MySQL、PostgreSQL、Oracle
等数据库。

二、Active Record 数据库迁移

迁移是 Active Record
的一个特性,允许我们按时间顺序管理数据库模式。有了迁移,就不必再用纯
SQL 来修改数据库模式,而是可以使用简单的 Ruby DSL 来描述对数据表的修改。

1)迁移概述
迁移是以一致和轻松的方式按时间顺序修改数据库模式的实用方法。它使用 Ruby
DSL,因此不必手动编写
SQL,从而实现了数据库无关的数据库模式的创建和修改。

我们可以把迁移看做数据库的新“版本”。数据库模式一开始并不包含任何内容,之后通过一个个迁移来添加或删除数据表、字段和记录。Active Record
知道如何沿着时间线更新数据库模式,使其从任何历史版本更新为最新版本。Active Record
还会更新 db/schema.rb文件,以匹配最新的数据库结构。

示例:

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name
      t.text :description

      t.timestamps
    end
  end
end

这个迁移用于添加 products 数据表,数据表中包含 name 字符串字段和
description 文本字段。同时隐式添加了 id 主键字段,这是所有 Active Record
模型的默认主键。timestamps 宏添加了 created_at 和 updated_at
两个字段。后面这几个特殊字段只要存在就都由 Active Record 自动管理。

对于支持事务并提供了用于修改数据库模式的语句的数据库,迁移被包装在事务中。如果数据库不支持事务,那么当迁移失败时,已成功的那部分操作将无法回滚。这种情况下只能手动完成相应的回滚操作。

某些查询不能在事务内部运行。如果数据库适配器支持 DDL 事务,就可以使用
disable_ddl_transaction! 方法在某个迁移中临时禁用事务。
如果想在迁移中完成一些 Active Record 不知如何撤销的操作,可以使用
reversible 方法:

class ChangeProductsPrice < ActiveRecord::Migration[5.0]
  def change
    reversible do |dir|
      change_table :products do |t|
        dir.up   { t.change :price, :string }
        dir.down { t.change :price, :integer }
      end
    end
  end
end

或者用 up 和 down 方法来代替 change 方法:

class ChangeProductsPrice < ActiveRecord::Migration[5.0]
  def up
    change_table :products do |t|
      t.change :price, :string
    end
  end

  def down
    change_table :products do |t|
      t.change :price, :integer
    end
  end
end

2)创建迁移

2-1)创建独立的迁移
迁移文件储存在 db/migrate
文件夹中,一个迁移文件包含一个迁移类。文件名采用
YYYYMMDDHHMMSS_create_products.rb 形式,即 UTC
时间戳加上下划线再加上迁移的名称。迁移类的名称(驼峰式)应该匹配文件名中迁移的名称。例如,在
20080906120000_create_products.rb 文件中应该定义 CreateProducts 类,在
20080906120001_add_details_to_products.rb 文件中应该定义
AddDetailsToProducts 类。Rails
根据文件名的时间戳部分确定要运行的迁移和迁移运行的顺序,因此当需要把迁移文件复制到其他
Rails 应用,或者自己生成迁移文件时,一定要注意迁移运行的顺序。

当然,计算时间戳不是什么有趣的事,因此 Active Record 提供了生成器:

$ bin/rails generate migration AddPartNumberToProducts

上面的命令会创建空的迁移,并进行适当命名:

class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
  def change
  end
end

2-2)模型生成器
模型和脚手架生成器会生成适用于添加新模型的迁移。这些迁移中已经包含用于创建有关数据表的指令。如果我们告诉
Rails
想要哪些字段,那么添加这些字段所需的语句也会被创建。例如,运行下面的命令:

$ bin/rails generate model Product name:string description:text

上面的命令会创建下面的迁移:

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name
      t.text :description

      t.timestamps
    end
  end
end

我们可以根据需要添加“字段名称/类型”对,没有数量限制。

2-3)传递修饰符

可以直接在命令行中传递常用的类型修饰符。这些类型修饰符用大括号括起来,放在字段类型之后。例如,运行下面的命令:

$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}

上面的命令会创建下面的迁移:

class AddDetailsToProducts < ActiveRecord::Migration[5.0]
  def change
    add_column :products, :price, :decimal, precision: 5, scale: 2
    add_reference :products, :supplier, polymorphic: true, index: true
  end
end

3)编写迁移

使用生成器创建迁移后,就可以开始写代码了。

3-1)创建数据表

create_table方法是最基础、最常用的方法,其代码通常是由模型或脚手架生成器生成的。典型的用法像下面这样:

create_table :products do |t|
  t.string :name
end

上面的命令会创建包含 name 字段的 products
数据表(后面会介绍,数据表还包含自动创建的 id 字段)。
默认情况下,create_table 方法会创建 id 主键。可以用 :primary_key
选项来修改主键名称,还可以传入 id: false
选项以禁用主键。如果需要传递数据库特有的选项,可以在 :options 选项中使用
SQL 代码片段。例如:

create_table :products, options: "ENGINE=BLACKHOLE" do |t|
  t.string :name, null: false
end

上面的代码会在用于创建数据表的 SQL 语句末尾加上
ENGINE=BLACKHOLE(如果使用 MySQL 或 MarialDB,默认选项是
ENGINE=InnoDB)。
还可以传递带有数据表描述信息的 :comment
选项,这些注释会被储存在数据库中,可以使用 MySQL Workbench、PgAdmin III
等数据库管理工具查看。对于大型数据库,强列推荐在应用的迁移中添加注释。目前只有
MySQL 和 PostgreSQL 适配器支持注释功能。
3-2)创建联结数据表
create_join_table 方法用于创建 HABTM(has and belongs to
many)联结数据表。典型的用法像下面这样:

create_join_table :products, :categories

上面的代码会创建包含 category_id 和 product_id 字段的
categories_products 数据表。这两个字段的 :null 选项默认设置为
false,可以通过 :column_options 选项覆盖这一设置:

create_join_table :products, :categories, column_options: { null: true }

联结数据表的名称默认由 create_join_table
方法的前两个参数按字母顺序组合而来。可以传入 :table_name
选项来自定义联结数据表的名称:

create_join_table :products, :categories, table_name: :categorization

上面的代码会创建 categorization 数据表。
create_join_table
方法也接受块作为参数,用于添加索引(默认未创建的索引)或附加字段:

create_join_table :products, :categories do |t|
  t.index :product_id
  t.index :category_id
end

发表评论

电子邮件地址不会被公开。 必填项已用*标注