第一篇地址在这:一些 Ruby/Rails 小技巧
我们接着记录开发应用中遇到的一些小问题。
在 sql 中拼接字符串形式的时间需要注意时区问题
我们经常会使用这样的查询:
Product.where("created_at > %", 2.days.ago)
如果你的应用设置了时区,这里 Rails 会帮我们把时间转化为 UTC 时间插入。但如果我们传入的是字符串而不是 Rails 的 Time/Date/DateTime 格式,情况又是怎么样呢?能不能直接 2.days.ago.to_s
?
能不能直接尝试后就知道了。经过尝试,一个时间对象转换成字符串的后会在当前时区时间加上 +0800
这样的时区后缀。但是问题就在有些数据库并不能解析这个后缀。在我们用的 PostgreSQL 中,这样的后缀可以说是被直接忽略掉的。
因此通过上面的分析,该问题的答案就变成了当 sql 需要传入字符串时间时,需要转换成 UTC 时间字符串。具体可以这么处理:
# 原始时间是对象
2.days.ago.utc.to_s
# 原始时间是字符串
"2017/11/24 12:00:00".in_time_zone.utc.to_s
# 或者
"2017/11/24 12:00:00".to_time.utc.to_s # 这里不能使用 to_datetime,因为 to_time 比 to_datetime 多支持一个时区参数,并且该参数默认值为 :local , 符合我们需求
如果有其他更直观的方法,请留言告诉我。
理解 map/collect/inject/reduce/each_with_object
标题是不合理的,因为他们并不全部相关。但是这些方法经常困扰着一些新人,所以放一起对比解释一下。
map/collect
他们只是别名,C 源码中用的是 collect 方法统一定义。使用的话记住两点:
- 最终返回一个数组,每次迭代的值为该数组的元素;
- 操作单元素枚举对象(Array)时,块中应该指定一个参数;操作双元素枚举对象(Hash)时,块中应该指定 1..2 个参数,当指定 1 个参数时,其实是将 Hash 先转化成 Array 在进行操作。
inject/reduce
他们也是别名,源码中用的是 inject 方法统一定义。inject 的作用是迭代更新每次结果。需要注意的是当没有初始值参数时,实际上迭代次数为 size-1,操作枚举对象的第一个元素被指定为初始值,没有进行迭代。以下是代码说明:
当指定初始值参数时
[1, 2, 3].map(10) { |result, item| result + item } #=>16
# 等价于
result = 10
[1, 2, 3].each { |item| result = result + item } # 这里的 result + item 表达式与 map 中迭代的表达式一致
result #=> 16
当没有指定初始值参数时
[1, 2, 3].map { |result, item| result + item } #=> 6
# 等价于
data = [1, 2, 3]
result = data.shift
data.each { |item| result = result + item } # 此时只有 2, 3 元素被迭代
result #=> 6
each_with_object
这个方法其实很好理解,就是迭代前,引入一个变量并最终返回那个变量(至于这个变量你有没有用到,方法并不关心)。
[1, 2, 3].each_with_object({}) { |item, object| object[item] = 0 } #=> {1=>0, 2=>0, 3=>0}
# 等价于
object = {}
[1, 2, 3].each { |item| object[item] = 0 }
object #=> {1=>0, 2=>0, 3=>0}
[1, 2, 3].each_with_object({}) { |item, object| item += 1 } #=> {}
# 等价于
object = {}
[1, 2, 3].each { |item| item += 1 }
object #=> {}
总结
这三个方法都是为 ruby 枚举对象迭代多做了一些额外的工作,其中 map/collect 是在枚举对象迭代时,收集元素,最后生成一个数组;each_with_object 是帮助我们为枚举对象的迭代引入一个结果变量(作为参数引入);最后 inject/reduce 做的事情是引入结果变量后,还修改了迭代体,每次迭代体最后返回的结果存给了该变量。
什么时候该用 render/redirect_to .. and return
官方 Guides 中提到了 action 中重复渲染/重定向的问题,并建议了使用 render/redirect_to .. and return
解决。但是有些人刚开始可能会不明白具体哪里需要(比如我),这里先将语句展开:
render/redirect_to .. and return
=> (render/redirect_to ..) && return
=> if (render/redirect_to ..); return; end
很好理解,他是希望在一个方法里提前结束,因为 render/redirect_to
并不等于 return
。
Rails 还有 before_action
回调,这里需要怎么处理?
class HomeController < ApplicationController
before_action do
redirect_to root_url if ... # 不需要 and return
end
def index
end
end
这里不需要做其他处理的原因是因为 Rails 会判断其他域(方法、块)中是否已经渲染或重定向,这里回调换成方法体也是一样的情况。
总结:一个域(方法、块)中有多个显示的渲染或重定向,除了最后一个都需要 and return
在 model 层而不是数据库层设置默认值
原因:model 层设置默认值更灵活,且适用场景更多,例如设置动态值等。
如何设置:
model User < ApplicationRecord::Base
after_initialize :set_defaults, if: :new_record? # 仅当初始化的是新纪录的时候设置默认值
def set_defaults
self.attribute ||= 'some value' # bool 以外的字段设置
self.bool_field = true if self.bool_field.nil? # bool 字段设置
end
常量的自动加载
开发环境中,Rails 应用的自动加载会与 Ruby 的 require 冲突
原因:Ruby require
维护着 $LOADED_FEATURES
全局变量,Rails 的 load
加载不会更新 $LOADED_FEATURES
,所以 Rails 自动加载后,require
还是会再执行一遍。另外如果 require
先运行了,Rails 没有完成该常量的自动加载,也就不会把该常量标记为自动加载的常量,因此开发环境设置的常量重新加载会失效。
做法:Rails 中,应保持默认自动加载的目录文件规则。即使出现需要手动引入常量,也应该使用 Rails 的 require_dependency
解决。
自动模块
在一些项目的 lib 文件夹中,经常会出现 api.rb, api/product.rb, api/user.rb 之类的结构,其中 api.rb 只有一个空模块定义。但其实 Rails 已经为我们处理了这类需求。假设 Rails 自动加载目录中含有 api 目录,且 Rails 自动加载中为找到 Api 常量定义,则 Rails 会为 Api 常量定义好空模块。即上述出现的结果中,api.rb 文件其实是可省略的,因为 Rails 帮我们这么干了。
不要依靠赋值方法的返回值
这个主要是因为 Ruby 中有一个约定:赋值语句/方法总是返回表达式右值。我们看一个案例:
class User
attr_reader :name
def name=(name)
if name.present?
@name = name
return true
else
return false
end
end
end
user = User.new
user.name = "" #=> ""
user.name #=> nil
user = User.new
user.name = "Pine" #=> "Pine"
user.name #=> "Pine"
这里 User#name=() 的 return 语句会被忽略,不会得到想要的效果。