Active Record 迁移与关联


Active Record迁移

按时间顺序管理数据库模式,不必用纯SQL来修改数据库模式。

更新db/schema.rb文件,以匹配最新的数据库结构。

class CreateProducts  def change
create_table :products do |t|
t.string :name
t.text :description
t.timestamps
end
end
end

创建数据表的时候默认创建主键为id


迁移文件储存在db/migrate文件夹中,一个迁移文件包含一个迁移类。

文件名采用yyyymmddhhmmss_create_products.rb形式,即UTC时间戳加上下划线在加上迁移的名称。

Add_foreign_key: 外键 以fk_rails_开头并且后面紧跟着10个字符的外键名。

提供一堆的辅助方法使用,如果辅助方法不够用,可以使用excute方法执行任意的SQL语句。

Product.connection.execute("UPDATE products SET price='free' where 1=1")

 

validate:return_amount_cannot_be_greater_than_loan_amount

def return_amount_cannot_be_greater_than_loan_amount

if return_amount > loan_amount

errors.add("return_amount can't be greater than loan_amount")

end

end

 


使用up和down方法

up方法用于描述对数据库模式所做的改变,down方法用于撤销up方法所做的改变。换句话说,如果调用up方法之后紧跟着调用down方法,数据库模式不会改变。

class ExampleMigration < ActiveRecord::Migration[5.0]
def up
  create_table :distributors do |t|
    t.string :zipcode
  end

  # 添加 CHECK 约束
  execute <<-SQL
    ALTER TABLE distributors
      ADD CONSTRAINT zipchk
      CHECK (char_length(zipcode) = 5);
  SQL

  add_column :users, :home_page_url, :string
  rename_column :users, :email, :email_address
end

def down
  rename_column :users, :email_address, :email
  remove_column :users, :home_page_url

  execute <<-SQL
    ALTER TABLE distributors
      DROP CONSTRAINT zipchk
  SQL

  drop_table :distributors
end
end

运行迁移:

db:migrate version=

回滚:

db:rollback
?
db:rollback STEP=3

安装数据库:

db:setup

重置数据库:

db:reset

运行指迁移:

db:migrate:up VERSION=

在不同环境中运行迁移:

db:migrate RAILS_ENV=test

Active Record关联

为什么要使用关联?因为关联能让常规操作变得更简单。

关联的类型:

rails支持六种关联:

  • belongs_to 两个模型间一对一关系

  • has_one 两个模型间一对一关系

  • has_many 两个模型之间一对多关系

  • has_many:through 借助第三个模型,创建两个模型之间的多对多关系

  • has_one:through 建立两个模型之间的一对一关系,一个模型通过第三个模型拥有另一模型的实例。

  • has_and_belongs_to_many 不借助第三个模型,直接建立两个模型之间的多对多关系,但是有一个join_table产生。


多态关联:

在同一个关联中,一个模型可以属于多个模型。

polymorphic:true

自联结:

在一个数据表中保存所有雇员的信息,但要建立经理和下属之间的关系。在迁移模式中,要添加一个引用字段,指向模型自身。

class Employee < ApplicationRecord
has_many :subordinates, class_name: "Employee",
                        foreign_key: "manager_id"

belongs_to :manager, class_name: "Employee"
end

小技巧和注意事项

  • 控制缓存

  • 避免命名冲突

  • 更新模式

  • 控制关联的作用域

  • 双向关联

控制缓存:

关联添加的方法都会使用缓存,记录最近一次查询的结果,以备后用。缓存还会在方法之间共享。

如果想要重载缓存,在关联上调用reload即可。

避免命名冲突:

关联的名称并不能随意使用。因为创建关联时,会向模型添加同名方法,所以关联的名字不能和ActiveRecord::Base中的实例方法同名。如果同名,关联方法会覆盖ActiveRecord::Base中的实例方法,导致错误。

更新模式:

关联非常有用,但是没什么魔法。关联对应的数据库模式需要你自己编写。不同的关联类型,要做的事也不同。对belongs_to关联来说,要创建外键;对has_and_belongs_to_many关联来说,要创建相应的关联表。

控制关联的作用域:

默认情况下,关联只会查找当前模块作用域中的对象。要想让处在不同命名空间中的模型正常建立关联,声明关联时要指定完整的类名。

双向关联:

一般情况下,都要求能在关联的两端进行操作,即在两个模型中都要声明关联。

通过关联的名称,Active Record能探知这两个模型之间建立的是双向关联。这样,Active Record只会加载一个对象副本,从而确保应用运行效率更高效,并避免数据不一致。

Active Record 提供了 :inverse_of 选项,可以通过它明确声明双向关联:

class Author < ApplicationRecord
has_many :books, inverse_of: 'writer'
end

class Book < ApplicationRecord
belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end

在has_many声明中指定:inverse_of选项后,Active Record便能识别双向关联。


关联详解

belongs_to关联详解:

belongs_to关联创建一个模型与另一个模型之间的一对一关系。用数据库术语来说,就是这个类中包含外键。如果外键在另一个类中,应该使用has_one关联。

声明belongs_to关联后,所在的类自动获得了五个和关联相关的方法:

association
association=(associate)
build_association(attributes={})
create_association(attributes={})
create_association!(attributes={})

例如:

class Book < ApplicationRecord
belongs_to :author
end
?
?
book模型的每个实例都获得了这些方法:
author
author=
build_author
create_authorcreate_association 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,对象的外键会自动设置,只要能通过所有数据验证,就会把关联对象存入数据库。
create_author! 与 create_association 方法作用相同,但是如果记录无效,会抛出 ActiveRecord::RecordInvalid 异常。

定制belongs_to关联行为。定制的方法很简单,声明关联时传入选项或者使用代码块即可。

如:

class Book < ApplicationRecord
belongs_to :author, dependent: :destroy,
  counter_cache: true
end

belongs_to关联支持下列选项:

- :autosave
- :class_name
- :counter_cache
- :dependent
- :foreign_key
- :primary_key
- :inverse_of
- :polymorphic
- :touch
- :validate
- :optional

解析:

:autosave

把他的选项值设置为true,保存父对象,会自动保存所有的子对象,并把标记为析构的子对象销毁。

:class_name

如果另一个模型无法从关联的名称获取,可以使用:class_name选项指定模型名。

:counter_name

提高统计所属对象数量操作的效率。声明belongs_to关联的模型中加入计数缓存功能,这样声明关联后,rails会及时更新缓存,调用size方法时会返回缓存中的值。

:dependent

选项控制属主销户后怎么处理关联的对象。

  • destory;也销毁关联的对象

  • delete_all:直接从数据库中删除关联的对象(不执行回调)

  • nullify:把外键设为NULL(不执行回调)

  • restrict_with_exception:如果有关联记录,抛出异常

  • restrict_with_error:如果有关联的对象,为属主添加一个错误

:foreign_key

按照约定,用来存储外键的字段名是关联名后加_id。:foreign_key选项可以设置要使用的外键名。

:primary_key

按照约定,rails假定使用表中的id列保存主键。使用:primary_key选项可以指定使用其他列。

:inverse_of

:inverse_of选项指定belongs_to关联另一端的has_many和has_one关联名。不能和:polymorphic选项一起使用。

:polymorphic

选项为true时,表明这是个多态关联。

:touch

如果把:touch选项设为true,保存或销毁对象时,关联对象的updated_at或updated_on字段会自动设为当前时间。

:validate

选项设置为true,保存对象时,会同时验证关联的对象。该选项的默认值是false,保存对象时不验证关联的对象。

:optional

选项设置为true,不会验证关联的对象是否存在,默认值是false。

belongs_to的作用域

有时可能需要定制belongs_to关联使用的查询,定制的查询可在作用域代码块中指定。

如:

class Book < ApplicationRecord
belongs_to :author, -> { where active: true },
                    dependent: :destroy
end

在作用域代码块中可以使用任何一个标准的查询方法,如:

where

指定关联对象必须满足的条件。

includes

指定使用关联时要及早加载的间接关联。

readonly

通过关联的对象只能是只读的。

select

用于覆盖检索关联对象使用的SQL SELECT字句。默认情况下,rails检索所有字段。

如果在belongs_to关联中使用select方法,应该同时设置:foreign_key选项,确保返回的结果正确。

 

把对象赋值给belongs_to关联不会自动保存对象,也不会保存关联的对象。


has_one关联详解

:as

:as选项表明这是多态关联。

class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end

class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end

class Product < ApplicationRecord
has_many :pictures, as: :imageable
end

把对象赋值给 has_one 关联时,那个对象会自动保存(因为要更新外键)。而且所有被替换的对象也会自动保存,因为外键也变了。

如果由于无法通过验证而导致上述保存失败,赋值语句返回 false,赋值操作会取消。

如果父对象(has_one 关联声明所在的模型)没保存(new_record? 方法返回 true),那么子对象也不会保存。只有保存了父对象,才会保存子对象。

如果赋值给 has_one 关联时不想保存对象,使用 association.build 方法。


has_many关联详解

声明之后,所在的类自动获得了16个关联相关的方法:

  • collection

  • collection<<(object, …)

  • collection.delete(object, …)

  • collection.destroy(object, …)

  • collection=(objects)

  • collection_singular_ids

  • collection_singular_ids=(ids)

  • collection.clear

  • collection.empty?

  • collection.size

  • collection.find(…)

  • collection.where(…)

  • collection.exists?(…)

  • collection.build(attributes = {}, …)

  • collection.create(attributes = {})

  • collection.create!(attributes = {})

    例如:

    class Author < ApplicationRecord
    has_many :books
    end
    ?
    ?
    books
    books<<(object, ...)
    books.delete(object, ...)
    books.destroy(object, ...)
    books=(objects)
    book_ids
    book_ids=(ids)
    books.clear
    books.empty?
    books.size
    books.find(...)
    books.where(...)
    books.exists?(...)
    books.build(attributes = {}, ...)
    books.create(attributes = {})
    books.create!(attributes = {})

    has_many方法的选项

    :source

    指定has_many:through关联的源关联名称。只有无法从关联名中解出源关联的名称时才需要设置这个选项。

    :source_type

    :source_type选项指定通过多态关联处理 has_many :through 关联的源关联类型。

    :through

    :through选项指定一个联结模型,查询通过他执行。

    class Physician < ApplicationRecord
    has_many :appointments
    has_many :patients, through: :appointments
    end

    class Appointment < ApplicationRecord
    belongs_to :physician
    belongs_to :patient
    end

    class Patient < ApplicationRecord
    has_many :appointments
    has_many :physicians, through: :appointments
    end

    has_many的作用域:

    :extending

    指定一个模块名,用于扩展关联代理。

    :group

    指定一个属性名,用在SQL GROUP BY子句中,分组查询结果。

    :limit

    限制通过关联获取的对象数量。

    :offset

    指定通过关联获取对象时的偏移量。如-> { offset(11) } 会跳过前 11 个记录。

    :order

    指定获取关联对象时使用的排序方式,用在SQL ORDER BY子句中。

    :distinct

    确保集合中没有重复的对象。与:through选项一起使用最有用。

     

    什么时候保存对象

    把对象赋值给 has_many 关联时,会自动保存对象(因为要更新外键)。如果一次赋值多个对象,所有对象都会自动保存

    如果由于无法通过验证而导致保存失败,赋值语句返回 false,赋值操作会取消。

    如果父对象(has_many 关联声明所在的模型)没保存(new_record? 方法返回 true),那么子对象也不会保存。只有保存了父对象,才会保存子对象。

    如果赋值给 has_many 关联时不想保存对象,使用 collection.build 方法


    has_and_belongs_to_many关联详解

    如果has_and_belongs_to_many关联使用的联结表中,除了两个外键之外还有其他列,通过关联获取的记录中会包含这些列,但是只读的,因为rails不知道如何保存对这些列的改动。

    has_and_belongs_to_many 关联的联结表中使用其他字段的功能已经废弃。如果在多对多关联中需要使用这么复杂的数据表,应该用 has_many :through 关联代替 has_and_belongs_to_many 关联。

    has_and_belongs_to_many方法的选项

    :join_table

    如果默认按照字典顺序生成的联结表名不能满足要求,可以使用 :join_table 选项指定。


    关联回调

    关联回调和普通回调差不多,只不过由集合生命周期中的事件触发。关联回调有四种:

    before_add

    after_add

    before_remove

    after_remove

    rails会把要添加或删除的对象传入回调。

    同一个事件可以触发多个回调,多个回调使用数组指定。


    关联扩展

    rails基于关联代理对象自动创建的功能是死的,可以通过匿名模块、新的查找方法、创建对象的方法等进行扩展。

    如:

    class Author < ApplicationRecord
    has_many :books do
      def find_by_book_prefix(book_number)
        find_by(category_id: book_number[0..2])
      end
    end
    end

    如果扩展需要在多个关联中使用,可以将其写入具名扩展模块。

    如:

    module FindRecentExtension
    def find_recent
      where("created_at > ?", 5.days.ago)
    end
    end

    class Author < ApplicationRecord
    has_many :books, -> { extending FindRecentExtension }
    end

    class Supplier < ApplicationRecord
    has_many :deliveries, -> { extending FindRecentExtension }
    end

    在扩展中可以使用proxy_association方法的三个属性获取关联代理的内部信息:

    proxy_association.owner:返回关联所属的对象。

    proxy_association.reflection:返回描述关联的反射对象。

    proxy_association.target:返回belongs_to或has_one关联的关联对象,或者 has_many 或 has_and_belongs_to_many` 关联的关联对象集合;


    单表继承

    表间的继承,只需要加一个type字段。

    如:

    $ rails generate model vehicle type:string color:string price:decimal{10.2}
    ?
    ?
    $ rails generate model car --parent=Vehicle
    ?
    ?
    class Car < Vehicle
    end
    ?
    ?
    Car.create(color: 'Red', price: 10000)
    ?
    INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000)
    ?
    Car.all
    ?
    ?
    SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car')