碍於篇幅的关系,来不及介绍关於Rails MVC的所有全貌。在Day23介绍了基本的MVC
操作,今天我们要来更深入与资料库互通讯息的model
层。
对於资料库的行为不外乎是写入资料、删除资料、查询资料。我们可以把资料库想像成是excel
表单,我们必须要对表单做操作,而model
负责的除了负责透过Rails
层级的操作资料库功能,也提供给开发者更简便对资料库操作的介面。
首先先介绍,当我们创建了一个Model,我们如何看订单的所有栏位
Order.column_names
#=> ["id", "customer_id", "created_at", ...]
#===== 找开头为 discount 的栏位
Order.column_names.select{|_| _.start_with? 'discount'}
#=> ["discount_detail"]
#===== 找含有关键字 discount 的栏位
Order.column_names.grep(/discount/)
另外介绍annotated
这个 Gem
,安装并下了rails db:migrate
指令之後,就会在model将所有栏位以注解的方式呈现
gem 'annotate'
在 app/models/blog.rb
⬇️
# == Schema Information
#
# Table name: blogs
#
# id :bigint not null, primary key
# content :string(255)
# genre :integer
# title :string(255)
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint not null
#
# Indexes
#
# index_blogs_on_user_id (user_id)
#
class Blog < ApplicationRecord
belongs_to :user
validates :title, :content, :genre, presence: true
enum genre: {
life: 0,
casual: 1,
technology: 2,
}
end
我们在Day4提过#grep
的用法,以及Array of Strings,而#grep
用在这里刚刚好,大家要记得回头看?
除了使用栏位以外,我们也会在 model
新增实体方法。以下的例子为新增方法判断折让单号有值代表该笔退货单折让成功
class ReturnOrder < ApplicationRecord
# 折让成功
define_method :allowance_success?, -> { invoice_allowance_number.present? }
end
下列为购物车的 model
来举例和购物车相关的实体方法,以下为例子上面两个方法的情境
changed_in_day?
➡️ 24小时内购物车没有动会回传true
subtotal
➡️ 购物车内商品*价钱的总和class Cart < ApplicationRecord
define_method :changed_in_day?, -> { cart_items.count.positive? && cart_items.pluck(:updated_at).max > 1.days.ago }
define_method :subtotal, -> { cart_items.includes(:variant).sum{|item| item.variant.price * item.quantity} }
end
Rails 提供一些好用的语法,可以使我们不用写SQL Statement
。我们可以透过to_sql
查看我们写的orm
是不是我们预期的Sql Statement
,通常工程师都会看Sql Statement
作为侦错的依据
Order.all.to_sql
#=> "SELECT `orders`.* FROM `orders`"
以下为针对订单做一系列Orm
语法介绍。为了简单化,今天的Orm
不会牵涉到一个以上的资料表
Order.all
#=> "SELECT `orders`.* FROM `orders`"
Order.where(status: 'processing')
#=> "SELECT `orders`.* FROM `orders` WHERE `orders`.`status` = 'processing'"
Order.where(status: ['processing', 'waiting'])
#=> "SELECT `orders`.* FROM `orders` WHERE `orders`.`status` IN ('processing', 'waiting')"
Order.where(status: ['processing', 'waiting']).pluck(:number)
#=> SELECT `orders`.`number` FROM `orders` WHERE `orders`.`status` IN ('processing', 'waiting')
#==== rails 里面若 where 一个阵列代表 in 语法
Order.select(:number).where(status: ['processing', 'waiting'])
#=> SELECT `orders`.`number` FROM `orders` WHERE `orders`.`status` IN ('processing', 'waiting') LIMIT 11
#==== rails not 用法
Order.select(:number).where.not(status: ['processing', 'waiting'])
#=> SELECT `orders`.`number` FROM `orders` WHERE `orders`.`status` NOT IN ('processing', 'waiting') LIMIT 11
#==== rails or 用法
Order.where(status: 'processing').or(Order.where(payment_status: 'unpaid'))
#=> "SELECT `orders`.* FROM `orders` WHERE (`orders`.`status` = 'processing' OR `orders`.`payment_status` = 'unpaid')"
#==== rails count 用法
Order.where(status: 'processing').or(Order.where(payment_status: 'unpaid')).count
#=> SELECT COUNT(*) FROM `orders` WHERE (`orders`.`status` = 'processing' OR `orders`.`payment_status` = 'unpaid')
#=> 64
以上为Model
的基本用法
另外我们分析一下Enumable#sum(Ruby)
和 Orm#sum(Rails)
有什麽不同
Order.all.pluck(:price)
# SELECT `orders`.`price` FROM `orders`
# => [5, 325, 325, 6080, 2850, 88, 23200, 23200, 34700, 50850, 78400, 25800, 1425, 18880, 18880, 8800, 21800, ...]
#=== ORM sum ===
Order.all.sum(:price)
# SELECT SUM(`orders`.`price`) FROM `orders`
# => 1283728
#=== 资料全捞又会失败 ===
Order.all.pluck(:price).sum
# SELECT `orders`.`price` FROM `orders`
#=> TypeError (nil can't be coerced into Integer)
ORM 的#sum
只捞取一个栏位,且相加若为nil
也不会坏掉,而Enumable#Sum(Ruby)
是对阵列做处理的方法,若里面的值含有nil
,则会错误。因此如果我们使用Enumable#sum
,我们必须使用compact
, select(:itself)
,或类似的reducer
将阵列留下只省下数字,才能进行加法。
一般的scope
用法与类别方法几乎无异
class Order < ApplicationRecord
def self.bar
"bar"
end
scope :foo, -> { "foo" }
end
Order.foo #=> "foo"
Order.bar #=> "bar"
另外scope
与类别方法一样,可以使用参数
class Order < ApplicationRecord
scope :foo, ->(x) { "foo #{x}!" }
end
Order.foo(1) #=> "foo 1!"
惯例上,我们会用scope
作为对该 model
的查询
class Order < ApplicationRecord
# 还没同步 pos
scope :yet_not_sync_pos, -> { where(sync_pos_at: nil).where(status: %w[done returned]) }
# 一个礼拜前
scope :week_ago, -> { ransack(done_at_lt: Tool.appreciation_period.ago).result }
end
Order.yet_not_sync_pos.week_ago
#=> "SELECT `orders`.* FROM `orders` WHERE `orders`.`sync_pos_at` IS NULL AND `orders`.`status` IN ('done', 'returned') AND `orders`.`done_at` < '2021-09-20 07:42:54'"
enum
的用法为内建的ActiveRecord::Enum
,当我们宣告了一个enum
方法,我们就可以使用enum
属性。使用enum
,我们可以 将型态存为Integer
,以增加资料库的速度,不过除了速度以外,重要的更是有一些 Rails
提供的方法可以使用,下列例子为enum
的一些常见的使用方法
⭐️ enum
会送一些instance method
# models/customer.rb
class Blog < ApplicationRecord
enum genre: {
life: 0,
casual: 1,
technology: 2,
}
end
# 单笔
blog = Blog.first
blog.technology? #=> true
blog.casual? #=> false
blog.life? #=> false
⭐️ 若我们不用enum
,而使用module
作为状态的话也可以,但就没有额外的helper
可以使用
class Order < ApplicationRecord
module Status
UNPAID = :unpaid # 未付款
PROCESSING = :processing # 处理中
WAITING = :waiting # 已出货
DONE = :done # 已完成
CANCELED = :canceled # 已取消
RETURNED = :returned # 退货/退款
end
end
⭐️enum
会送scope
class ReturnOrder < ApplicationRecord
# 退货状态: 申请退货/已确认商品退款中/退款失败/退货失败/已完成退货
enum status: %i(applied processing done refund_failed return_failed)
end
#========= 查询申请中的表单 =========#
ReturnOrder.applied
#=> "SELECT `return_orders`.* FROM `return_orders` WHERE `return_orders`.`status` = 0"
#=== 等同於 Conversation.where.not(status: :applied)
⭐️ enum
作为常数 ➡️ 可以很方便地做下拉式选单的资料
ReturnOrder.statuses
#=> {"applied"=>0, "processing"=>1, "done"=>2, "refund_failed"=>3, "return_failed"=>4}
另外,enum 可以和很多对象做搭配
以下这些为model提供的判断生命周期方法,可以藉由这些方法判别该笔资料位於哪一个生命周期。
以下使用 new_record?
, persisted?
判断这一笔是否并没有被存进资料库。
User.first.blogs.create.new_record? #=> true
User.first.blogs.create.persisted? #=> false
以下为 ActiveRecord 的生命周期
new record ➡️ 尚未写入阶段会判断为true
blog = Item.new
blog.new_record? #=> true
persisted ➡️ 已写入阶段会判断为true
blog.save
blog.persisted? #=> true
changed ➡️ 资料被改写但尚未存进资料库会被判断true
blog.name = "other"
blog.changed? #=> true
destroyed ➡️ 资料被删除但该笔纪录,但还没重整。因此该笔资料暂时存在在model。遇到这种情况则会判断为true
blog.destroy
blog.destroyed? #=> true
delegate 也是Rails 常见的使用方式!以下面的程序码为例,当我要取得订单订购人地址,可以使用 delegate
# models/customer.rb
class Customer < ApplicationRecord
def customer_info
# 顾客资讯
[name, phone].join('-')
end
end
# models/profile.rb
class Order < ApplicationRecord
belongs_to :user
delegate :customer_info, to: :customer
end
# 使用
order = Order.find(1)
order.customer_info # 陈汉汉-0983168969
观察 scope
的底层,我们发现scope
可以搭配call
使用,衍伸出以下用法。
module Orders
class MoreThanTenThousandQuery
class << self
delegate :call, to: :new
end
def initialize(relation = Order.all)
@relation = relation
end
def call
@relation.where(price: 10000..)
end
end
end
在 models/order.rb
,将原本应该要填入Proc
的地方填入QueryObject
class Order < ApplicationRecord
scope :m, Orders::MoreThanTenThousandQuery
end
并在 Rails Console
使用
Order.m
#=> "SELECT `orders`.* FROM `orders` WHERE `orders`.`price` >= 10000"
验证和回呼是 ActiveRecord
里面相当重要的一环。
在验证方面,开发者可以调用Rails
内建的验证,或者写自定义的验证。这些验证此验证与SQL Constraint
不一样,此为Model
层级的验证,而SQL Constraint
为SQL
层级的验证。若两者都加当然最好,这样一来就多一层的保证
⭐️ 以下为Rails
内建的检查值是否为空的验证
class Blog < ApplicationRecord
belongs_to :user
validates :title, :content, :genre, presence: true
end
⭐️ 以下为 Rails
内建检查是否为数字的验证
class Order < ApplicationRecord
validates :phone_area_code, length: { minimum: 2, maximum: 2 }, numericality: true
validates :phone, length: { minimum: 6, maximum: 8 }, numericality: true
end
⭐️ 以下为Rails
内建对 Email & 网址列的验证
class Customer < ApplicationRecord
validates :personal_website, format: { with: URI::regexp(%w(http https)) }
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
end
⭐️ 另外,我们也可以写自定义的方法
class Invoice < ApplicationRecord
# 验证统一编号
validate :valid_e_gui_tax_number
# 验证手机载具
validate :valid_e_gui_carrier
private
# 验证统一编号
def valid_e_gui_tax_number
return if buyer_ban.blank?
return errors.add(:buyer_ban, '需为8位数') unless Validator::InvoiceTaxid.at_least_8_digits?(buyer_ban)
errors.add(:buyer_ban, '不合法') unless Validator::InvoiceTaxid.valid_taxid?(buyer_ban)
end
# 验证手机载具
def valid_e_gui_carrier
return if /\A[\/][0-9A-Z+-\.]{7}\z/.match?(carrier_id) || carrier_id.blank?
errors.add(:carrier_id, '不合法')
end
end
对应的验证为 ⬇️
module Validator
class InvoiceTaxid
def self.at_least_8_digits?(taxid)
taxid.match?(/\A\d{8}\z/)
end
def self.valid_taxid?(taxid)
result = []
taxid_array = taxid.split('')
num_array = [1, 2, 1, 2, 1, 2, 4, 1]
taxid_array.zip(num_array) { |a, b| result << a.to_i * b }
sum = 0
result.each do |elm|
sum += elm.divmod(10).inject { |s, i| s + i }
end
((sum % 10 == 0) || ((sum % 9 == 9) && (taxid[5] == 7)))
end
end
end
Rails中存取资料的过程区分不同阶段,不同的阶段组成一个生命周期,Rails 提供了一些方法,使我们可以在各个生命周期中,做我们想要做的事情,而每一个阶段都称为不同的回呼,英文称作callback
。
回呼的部分,可以参考高见龙的文章。这里只简单介绍,当我们使用的一些Rails
的存取方法,有些会经过回呼、有些不会,以下举例update
, update_attribute
的用法
update #=> 经过验证、回呼
update_attribute #=> 没有经过验证、回呼
经过回呼或验证的存取,会因为经过 ActiveRecord
设定的各种生命周期,也就是Rails
的验证与回呼,因此会跑得比较慢,但资料的存取比起直接对资料库存取,可以更确保该笔Record
更完整。
Postgres 比起 Mysql 处理 Json, Array 更有优势,Drifting Ruby 的某一篇 Episode 把处理 Json 的内容写得很好。不过这里指针对 Store Attributes
来讲
如何在model
里面处理json
型态的资料是门艺术,Store Attributes
帮我们做好一部分的工作。跟大家介绍之前在某个任务中,为了做时间线的效果,我将切换状态的同时,将资料存进 Json 里面,并用Store Attribute
写好的方法,来更轻松的取值。
以下为范例程序码
# migration file (mysql)
class AddShippingStatusRecordToOrders < ActiveRecord::Migration[6.1]
def change
add_column :orders, :shipping_status_record, :json, comment: '物流时间纪录'
end
end
# models/order.rb
class Order < ActiveRecord::Base
include ApplicationHelper
before_create :shipping_updated
module ShippingStatus
READY = "ready"
PARTIAL = "partial"
SHIPPED = "shipped"
ARRIVED = "arrived"
RETURNED = "returned"
RECEIVED = "received"
end
# 物流时间纪录
store :shipping_status_record,
accessors: Order::ShippingStatus.constants.map { |c| Order::ShippingStatus.const_get(c).to_sym },
coder: JSON, suffix: :at
...
# 货运时间画押日期
def shipping_updated
self.ready_at = DateTime.now if self.ready_at.nil?
if self.shipping_status_changed?
if self.shipping_status == Order::ShippingStatus::SHIPPED
self.shipped_at = DateTime.now
end
self.shipment_updated_at = DateTime.now
# [:shipped, :ready, :partial, :arrived, :returned, :received]
return unless self.shipping_status.in? Order.stored_attributes[:shipping_status_record].map(&:to_s)
# 纪录时间动态方法
self.send(:"#{self.shipping_status}_at=", DateTime.now.strftime("%F"))
end
end
...
end
首先我们在Order
开了一个shipping_status_record
的栏位,并且在资料被创建前 before_create
的回呼存取json
值,并且这些存取的值我们可以使用利用store
这个方法将[:shipped, :ready, :partial, :arrived, :returned, :received]
转为实体可以使用的方法
order.shipped_at
order.ready_at
order.partial_at
order.arrived_at
order.returned_at
order.received_at
这章所介绍的内容是Model
基本中的基本的观念,这些基础身为 Rails 的工程师都应该要会。
Rails
占了举足轻重的地位,一定要会Store Attributes
这种好用的工具一定要知道delegate
不会没关系,但偶尔会在专案看到这种写法Rails
还有许多用法没有提到,像是
bang
的用法(加!
) ➡️ 会引发Raise
搭配 ActiveRecord::Base.transaction
区块一连串存取动作的某一动失败时资料库状态全部复原
Model
的STI
, Polymorphic
counter_cache
明天会介绍关联,以及基本的has_many
, has_one
, belongs_to
的用法
<<: Progressive Web App 唤醒锁: 维持萤幕长亮的方法 (16)
>>: Angular 转换 API 资料格式 (Adapter)
终於到最後一个区域了,我有加一些新的东西在上面这边一起跟大家说 一样先到assets资料夹下的Foo...
前言 昨天在建立环境的时候发现有很多相容性的问题,因此今天我想说这几天先来学习一下tensorflo...
前言 Hi 我是一名普通的 iOS 开发者,兴趣使然的 UI 设计师。不小心参与了几年 iOS 开发...
圆形 尝试ellipse 按照搜寻结果,我们一开始可能很直觉的会想到使用 ellipse ellip...
本篇文章同步发表在 HKT 线上教室 部落格,线上影音教学课程已上架至 Udemy 和 Youtu...