Rails笔记——ActiveRecord

Rails笔记——ActiveRecord

以下文章转自 http://hpxing.blogbus.com/,在转载之前并未得到作者同意,如果作者请任何意见或者建议,请PM我。

php?name=rails" onclick="tagshow(event)" class="t_tag">rails笔记 activerecord

修改model的属性(如果是性质变化)以后最好重启动,避免奇怪的错误
自动类型转换:

  * int, integer => Fixnum
  * decimal, numeric => Float
  * clob, blob, text => String
  * interval, date => Date
  * float, double => FLoat
  * char, varchar, string => String
  * datetime, time => Time
  * boolean => 字面值

注意

1 decimal到ruby的float中运算以后精度可能_无法保证_(使用放大以后的integer代替) 2 数据表示boolean的特殊之处: Boolean处理 modelobj.name1? 来访问rails的boolean, (否则是ruby内置的低能boolean) 对比

  * rails: 0 ,"0","f","false","" ,nil, false 都是false,其他为true
  * ruby: nil,false是false, 其他为true_ 若要定制boolean可以自己写一个name1?的方法

record的mapping

ModelClass.columns.map{|col|col.name} 查看column ModelClass.columns_hash 返回列信息hash
访问数据

使用object.property访问, 也可以使用object[:property]的方式访问(当和ruby保留字冲突时) 后者还可以用来覆盖默认行为,比如

class Account < ActiveRecord::Base
  def balance=(value)
   raise BalanceTooLow if value < MINIMUM_LEVEL
   self[:balance] = value
  end
end

添加modelobj1.name1_before_type_cast可以得到原始的数据对象(未转换的)

serialize :last_five 可以使用YAML序列化任何对象到TEXT字段,但是这样只有识别YAML的程序可以读取

set_primary_key 'another_column' 可以让rails按照another_column来当id, 但是在对象级别访问还是使用id属性(此时映射到another_column)

使用establish_connection方法可以连接到不同于配置的数据库,方法中忽略的参数还是从配置读取默认值
对象操作
创建

new是内存中, save以后才到数据苦 Order.new do |o| ... o.save end

create一步搞定,可以接受一个hash和hash的数组来创建多个, 返回创建的数组,比如order=Order.create(params)(直接从http上拿参数创建)
find 查找

find支持的参数

  * :all or :first
  *

  :conditinos conditions支持的参赛
    o 直接使用sql conditions=>"..."
    o 使用?参数conditons=>["..?..",a]
    o 使用conditons=>["..:name", hash]
  * :order
  * :limit
  * :offset
  * :include

join指令, 可能用得不多 ???

LineItem.find(:all, :conditions => "pr.title = 'Programming Ruby'", :joins => "as li inner join products as pr on li.product_id = pr.id")

Order.count(支持的参数和:condition类似) 可以获取记录数

默认按照id来find会找不到报异常, 但是自己的复杂find只会返回空记录(nil or []) ObjectA.find_by_sql 可以生成自由填充的ObjectA, 但是如果要修改记得要load id

find_(all)_by_A_and_B(a,b) (只支 持and) 可以自动转换为find(:first(:all) ,:conditions=>'A=? and B=?',a,b)

reload重新读取

  * save 返回true false
  *

  save!返回nil 或者报exception ,如果表中有lock_version int default 0 字段,会自动使用乐观锁定
  *

  delete 和delete_all 会直接删除,
  * destory和destory_all会把对象都读入内存然后调用所有的callback然后再删除(没有前面快)

事务

rails里面实现trasaction, 通过trasaction(obj1,objec2) 调用会自动rollback内存里面的对象

rails内置的 save,delete方法(可能导致多个sql)默认是事务(原子)的,不用额外加事务

rails不支持跨数据库事务(至少目前),下面有个简单模拟方案

User.transaction(user) do
  Account.transaction(account) do
   account.calculate_fees
   user.date_fees_last_calculated = Time.now
   user.save
   account.save
  end
end

但是这样只是模拟,如果是执行期出错可以回退, 但是user commit的时候如果出错, 此时内部的account已经commit了,不能回退了
扩展的activerecord
act_as_list

act_as_list : one-to-many对象关系是通过list(默认是set)来完成, 既然有list, 就有了排序,首先对应表中必须有position(自动默认)字段,用来标示排序,如果不默认就得显示通过:order指定

class Parent < ActiveRecord::Base
  has_many :children, :order => :position
end
class Child < ActiveRecord::Base
  belongs_to :parent
  acts_as_list :scope => :parent_id
end

这里的:scope=> :parent_id 说明了list是相对于单个parent_id的, 否则就是所有的product共用一个排序

对应关系建立以后, child对象会增加如下move_lower,move_higher, move_to_top,move_to_bottom,还会有first?和last?方法

注意 对对象作了排序操作以后, 只是修改数据库中的记录, parent对象还不会在list中调整, 必须调用parent.reload
act_as_tree

和act_as_list类似,例子如下

class Category < ActiveRecord::Base
  acts_as_tree :order=> "name"
end

实际上这个代码等同于===>

class Category < ActiveRecord::Base
  belongs_to :parent,
   :class_name => "Category"
  has_many :children,
   :class_name => "Category",
   :foreign_key => "parent_id",
   :order => "name",
   :dependent => true
end

通过children操作, 如有必要,可以通过:counter_cache=> true 并添加children_count来优化子对象数量获取
aggregation hibernate的 component

简单的说就是把几个字段映射为一个字段,方法为新建立一个对象 class Name attr_reader :first,:last end 然后在主对象中使用composed_of

class XX < ActiveRecord::Base
  composed_of :name, :class_name=>Name,:mapping=>
  [
  [:first_name,:first]
  [:last_name,:last]
  ]
end

其中:class_name和mapping可省略 ???

aggregation还可以用来聚合单一字段,下面的类就聚合了一个字段(以,号分隔)

class XxYy
  attr_reader :list
  def initialize(db_str)
  @list=db.str.split(/,/)
  end
  def xx_yy
  @list.join(',')
  end
end

然后主类需要用composed_of 标明映射的字段xx_yy

class Main
  composed_of :xx_yy
end

注意 aggregation对象都是value object, 你不能修改他里面的值,即使修改了,rails也不会回写它们到数据库中,唯一修改的办法是新建另外一个component然后赋给主对象
单表继承

只适用于大部分值都在父类的对象结构, 对于abstact方法的对象结构(子类直接差别非常大),可能不适合

  * 表中必须有所有子对象的属性, 不能有重合,冲突,所有 父类可能有很多允许NULL的字段
  * 表中有个type字段标示类型 ,根对象的type是"",
  * 子对象的类型实际上父对象也有,所有这里的对象不严格了, rail说是权宜之计
  * 由于type对象和ruby内置的type属性冲突,所有要用model[:type]来访问

验证 validation模块
底层有三个方法

  * validate 任何保存操作都调用
  * validate_on_create 创建对象的时候调用 通过new_record?区分
  * validate_on_update 更新对象的时候调用

实现这三个方法就可以自动使用validate, 手动调用的方法是valid?()

validate就是简单的判断, 然后再errors.add(:xxxx,"readon")就可以,例子如下

class User < ActiveRecord::Base
  def validate
   unless name && name =~ /^\w+$/
    errors.add(:name, "is missing or invalid")
   end
  end
  def validate_on_create
   if self.find_by_name(name)
    errors.add(:name, "is already being used")
   end
  end
end

提示 rails增加了一个blank?方法 可以判断string为nil或""
高层的帮助指令(很多)

大部分指令都支持:on和:message参数,

  * :on标示何时进行验证
  * :message标示验证失败的帮助信息(可格式化)

失败以后,controller会自动重新显示form,然后page可以通过调用error_messages_for()来显示错误消息

指令列表如下

  * validates_acceptance_of 用于检查checkbox, 检查对应参数的值是否为"1"
  * validates_asscociated 调用指派model自己的validate, 此方法指派的属性必须也是一个ActiveRecord的Model, 这个方法需要注意不要形成循环验证
  * validates_confirmation_of 用来检查如同password的带confirm的输入,同时检查xxx字段和xxx_confirmation是否一样(约定)
  * validates_each {|model,attr,value| ...} 一次检查多个字段,需要传入一个block, :allow_nil参数如果为true(默认false), nil就不会被传入进来
  * validates_exclusion_of attr....,:in=> enum[] 验证对应的参数不在给的列表内(支持include?的都行)
  * validates_inclusion_of 和上门相反
  * validates_format_of :with=>/..../ 正则匹配
  * validates_length_of 验证长度 有:maximum :minimum :in可用, :message还有三个扩展:too_long :too_short :wrong_length对应细化的错误信息(格式化支持%d标识要求的数值)
  * validates_numericality_of 验证数字, 支持:only_integer(支持负数)
  * validates_presence_of 验证非空
  * validates_uniqueness_of 验证唯一 :scope=>"xx" 指定的字段可以作为范围(在同一个xx中唯一)

Callbacks

一共16个callback, 其中14个如下图调用 (可以看出没有严格嵌套)

callback调用图

另外两个特殊的callback是after_find after_initialize

callback的注册可以通过

  * 直接在类中写方法,如def before_create end
  * 通过类指令: before_cate :some_method
  * 通过提供一个block {|model|...}
  * 提供一个callback对象的实例,单独的Callback对象相当于专门把callback方法独立出来放入一个类中,使得callback可以在类之间复用(默认在/app/model下)

要统一注册callback ,可以修改ActiveRecord::Base根类

注意 字段created_at created_on updated_at updated_on会自动被rails更新
Observers 类似aop

默认在app/model下一个典型的例子, 如果不使用observe类指令, 默认是根据类名推导出来为Audit类

class AuditObserver < ActiveRecord::Observer observe Order, Payment, Refund def after_save(model) model.logger.info("#{model.class.name} #{model.id} created") end end

最后要调用一下 XXXObserver.instance方法才能生效, 在rails中可以在controller中使用observe方法,不用单独instance了
属性扩展 Advanced Attributes

model.attributes可以取得属性hash,可以访问特殊名的字段

特殊的组合sql可能无法得到字段类型(mysql5.0好像没有这个问题了),导致rails以文本方式保存数据,这时我们可以通过调用 read_attribute("xxx")和write_attribute("xxx")可以直接在底层操作数据,作一些转换来构造一个facade column来满足需求,例子如下

class ProductData < ActiveRecord::Base
  CUBITS_TO_INCHES = 18
  def length
   read_attribute("length") * CUBITS_TO_INCHES
  end
  def length=(inches)
   write_attribute("length", Float(inches) / CUBITS_TO_INCHES)
  end
end

注意事项

  * 为万无一失, 使用find_by_sql最好都把id给select出来, 不然不能保存
  * rails复写了ruby的id和hash功能,id是数据库ID(原来是object_id),如果id相等则认为对象相等,所有没有保存的对象(没有id)会都相等,用来hash不安全
  * 使用原始的connection: Order.connection.select_all(..) ,详情查阅文档

magic column name

  * created_at, created_on, updated_at, updated_on Automatically updated with the timestamp (_at form) or date (_on form) of a row’s creation or last update (page 267).
  * lock_version Rails will track row version numbers and perform optimistic locking if a table contains lock_version (page 213).
  * type Used by single table inheritance to track the type of a row (page 253).
  * id Default name of a table’s primary key column (page 197).
  * xxx_id Default name of a foreign key reference to table named with the singlua form of xxx (page 216).
  * xxx_count Maintains a counter cache for the child table xxx (page 235).
  * position The position of this row in a list if acts_as_list is used (page 243).
  * parent_id A reference to the id of this row’s parent if acts_as_tree is used (page