Rails中管理配置ENV环境变量一个小技巧

在我们Rails开发的过程中,经常会遇到一些服务提供了secret token或者密码之类的需要保密的信息,我们在程序读取他们的话,一般都是从ENV里面读取。这样是比较安全的解决方法。 但ENV里面的值如何可以方便统一管理和赋值呢?下面介绍一个方法:

新建一个YAML文件类似这样的:

1
2
3
4
5
6
7
8
9
# config/application.yml
defaults: &defaults
XXX_SECRET_KEY: XXXXXX

development:
<<: *defaults

test:
<<: *defaults

把下面代码加到config/application.rb里面去

1
2
3
4
5
6
7
if File.exists?(File.expand_path('../application.yml', __FILE__))
  config = YAML.load(File.read(File.expand_path('../application.yml', __FILE__)))
  config.merge! config.fetch(Rails.env, {})
  config.each do |key, value|
    ENV[key] ||= value.to_s unless value.kind_of? Hash
  end
end

这样config/application.rb就变成这样了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# config/application.rb
require File.expand_path('../boot', __FILE__)
require 'rails/all'
# Require the gems listed in Gemfile, including any gems# you've limited to :test, :development, or :production.
Bundler.require(:default, Rails.env)

if File.exists?(File.expand_path('../application.yml', __FILE__))
  config = YAML.load(File.read(File.expand_path('../application.yml', __FILE__)))
  config.merge! config.fetch(Rails.env, {})
  config.each do |key, value|
    ENV[key] ||= value.to_s unless value.kind_of? Hash
  end
end

module NewApp
  class Application < Rails::Application
    # ...  
  end
end

这样的话Rails启动的话会自动读取config/application.yml里面的值,然后把里面的key和value设置到ENV里面去, 无需你手动分别在各种不同的环境系统里面修改/etc/environment的值。

当然记住这个文件是不能push到代码仓库的,记得加到.gitignore里面去。

Rails中的Presenter

什么是Presenter

当你使用的models里面有很多view相关的逻辑的时候, 会觉得这块逻辑写在这个地方不太好。 很多人可能会把这些代码移到helper方法里面去,确实是可以的。但是当你view的逻辑复杂后, 可能你需要看看Presenter了。

Presenter是面向对象的方式实现view hepler。我就介绍下如何使用presenter重构helper 和model里面的一些方法。

重构views

例如我们需要显示一个博客文章的状态, 如果文章已经发表的话就显示发表的时间,否则显示为”Draft”,

1
2
3
4
5
class Post < ActiveRecord::Base
  def publication_status
    published_at || 'Draft'
  end
end

这时候看起来没有什么问题,如果需求改成希望把时间显示成xxx hours ago的话,这时候如果使用 helper里面的time_ago_in_words方法再方便不过了。但model里面不支持这个方法,所以我们移到 helper里面去。

1
2
3
4
5
6
7
8
9
module PostHelper
  def publication_status(post)
    if post.published_at
      time_ago_in_words(post.published_at)
    else
      'Draft'
    end
  end
end

这个方法确实解决了这个问题,但是helper有个缺点,把所有的helper方法都放到一个namespace下面了。 如果post有一个这样的方法,那该多方便。

创建第一个Presenter

我们来创建一个PostPresenter(app/presenter目录下面)类来实现上面的需求。

1
2
3
4
5
6
7
8
9
10
11
12
class PostPresenter < Struct.new(:post, :view)
  def publication_status
    if post.published_at?
      view.time_ago_in_words(post.published_at)
    else
      'Draft'
    end
  end
end

presenter = PostPresenter.new(@post, view_context)
presenter.publications_status

view_context是ActionView的一个实例,这样的话我们才能使用time_ago_in_words方法。 但是当我们想获取post的title的时候,这个persenter将无法获得。所以我们需要创建一个 BasePresenter来解决这个问题,所有的Presenter将会继承这个类。

1
2
3
4
5
6
7
8
9
10
class BasePresenter < SimpleDelegator
  def initialize(model, view)
    @model, @view = model, view
    super(@model)
  end

  def h
    @view
  end
end

通过继承SimpleDelegator类并且调用了super(@model)初始化方法,这个保证了如果我们调用的 方法在presenter里面不存在的话,就会调用model对应的方法。

并且定义了一个h方法来代替view。上面的ProjectPresenter将会重构成这样:

1
2
3
4
5
6
7
8
9
class PostPresenter < BasePresenter
  def publication_status
    if @model.published_at?
      h.time_ago_in_words(@model.published_at)
    else
      'Draft'
    end
  end
end

我们可以在controller里面初始化:

1
2
3
4
5
6
class PostsController < ApplicationController
  def show
    post = Post.find(params[:id])
    @post = PostPresenter.new(post, view_context)
  end
end

将presenter移出controller

上面的presenter在controller里面每次初始化感觉比较麻烦,现在我们通过定义一个helper方法, 来简化这个初始化。

1
2
3
4
5
6
7
module ApplicationHelper
  def present(model)
    klass = "#{model.class}Presenter".constantize
    presenter = klass.new(model, self)
    yield(presenter) if block_given?
  end
end

上面通过传的model来实例化其对应的Presenter,然后通过block的方式实现你需要的调用。

1
2
3
- present(@post) do |post|
  %h2= post.title
  %p= post.author

定制的Presenter

上面的presenter都是默认认为是modle的名字加上Presenter,如果post还有一个定制的 AdminPostPresenter该如何实现呢?其实很简单,present再加一个参数就搞定了,代码如下。

1
2
3
4
5
6
7
module ApplicationHelper
  def present(model, presenter_class=nil)
    klass = presenter_class || "#{model.class}Presenter".constantize
    presenter = klass.new(model, self)
    yield(presenter) if block_given?
  end
end

调用的地方写成present(@post, AdminPostPresenter)这样就可以了。

总结

使用Presenter极大的分离了model里面view相关的逻辑,也给测试带来了方便。也有类似的gem 例如draper实现了差不多的功能。如果你的view和model有相关的代码,可以考虑使用Presenter 来重构一下吧。

参考原文

http://nithinbekal.com/posts/rails-presenters/

为什么要写Git教程

其实市面上已经有很多Git教程了,我也看过好几本,但是说实话我都没有很完整看完一本, 并且有时看过之后由于没有实践,导致就忘记了。虽然在GitCafe工作一直接触Git知识, 但感觉自己还是没有达到精通的程度。

在网上看了Atlassian公司的Git英文教程还不错, 就萌生了通过自己翻译整个教程的过程,重新巩固下Git知识,这样也给自己一点压力去学的更好。

把完成此翻译作为我2015年的第一个目标,我希望在3个月内可以搞定。

数据缓存提升性能gem - Dalli介绍

Rails可能很多人诟病的就是性能问题,所以很多人想出来了改善方法,其中dalli这个gem就是通过memcache来改善的。

地址

https://github.com/mperham/dalli 使用这个gem必须保证安装了memcached

什么数据适合存入cache呢?

一般情况下,数据库里面不怎么改变的数据,并且使用的地方比较多的话是非常适合存入cache的。例如:一个国家的所有省份。

举个例子说明下:

1
2
3
4
5
6
7
class Grade < ActiveRecord::Base
  has_many :skills
end

class Skills < ActiveRecord::Base
  belongs_to :grade
end

如果想取得一个grade所有skills非常简单如下:

1
2
grade = Grade.first
skills = grade.skills

现在每次取一个grade的skills都会访问数据库,可以考虑把skill放到cache里面去了。

1
2
3
4
5
def cached_skills
  Rails.cache.fetch([self.class.name, id, :skills], expires_in: 240.hours) {
    skills.to_a
  }
end

所以我们取skills的话可以通过下面方式取出来

1
grade.cached_skills

这个过程是什么样的呢?当第一次访问的时候,会去访问数据库然后把数据存入到memcached里面去,下次访问的话就直接从cache里面取了,不再访问数据库了。

  • [self.class.name, id, :skills] 这是存储的key
  • cache将会在240小时后过期
  • 如果不转化成数组(to_a)直接将active-record的relations存储到cache的话,每次访问cache这个relations还是会查询数据库的,所以大家务必要注意。

让cache过期

如果某个grade的skills改变了,而grade又没有收到通知,这时候从cache中取出来的将是旧的数据,这个时候我们需要让cache里面这个grade过期或者删除掉

一种方法使用after_commit

在Skill的model里面加上

1
2
3
4
5
6
# in skill model
after_commit :flush_cache

def flush_cache
  Rails.cache.delete([self.grade.class.name, self.grade_id, :skills])
end

这个方法可以保证cache里面的数据都是最新的。

第二种方法是通过改变key和加入touch关键字

model定义如下:

1
2
3
4
5
6
7
class Grade < ActiveRecord::Base
  has_many :skills
end

class Skills < ActiveRecord::Base
  belongs_to :grade, touch: true
end

touch什么意思呢?如果skill更新的话,也会更新grade的updated_at字段

这时候将cached_skills方法修改成下面的:

1
2
3
4
5
def cached_skills
  Rails.cache.fetch([self.class.name, updated_at.to_i, :skills], expires_in: 240.hours) {
    skills.to_a
  }
end

这样的话如果skill发生改变的话,因为把updated_at作为key了,所以从cache里面取的永远是最新的skills。

但这种方法有如下缺点,使用的时候需要小心:

在这种情况下,如果topic改变也会更新grade的updated_at,这样的话,也会影响skills,cache的效果就不明显了。 上面的2种cache过期方法,大家根据实际情况自行选择。

总结

这个gem可以帮助我们减少很多数据库访问,极大的提高性能。大家使用的时候,务必注意cache里面数据保持更新的问题,不然引起很多因为旧数据引起的bug就不太好了。

Ruby数组中容易忽视的Suboptimal代码

首先解释下什么是Suboptimal,根据翻译来看就是未达标准的,非最优的。 本文主要介绍下一些数组中比较细节的但很容易被开发人员忽视的非最优使用方法。

1.寻找Array中满足条件的第一个值

很多人可能会使用以下方法:

1
  [1,2,3].select {|x| x > 1 }.first

但其实有更好的有效率的替代方法:

1
  [1,2,3].detect {|x| x > 1 }

为什么说detect方法更有效率呢,因为select内部会遍历所有的元素,而且会生成新的对象, 而detect方法会在找到第一个满足的值就直接返回退出了。

2.使用reverse循环的时候应该使用reverse_each方法

很多人会使用如下方法:

1
[1,2,3].reverse.each {|x| x+1 }

更好的替代方法如下:

1
[1,2,3].reverse_each {|x| x+1 }

因为先reverse再做each的话会做2遍循环,而reverse_each内部只需做一次循环。

3. 寻找Array中满足条件的是否存在

很多人会这么写:

1
[1,2,3].select {|x| x > 1 }.empty?

更好的替代方法如下:

1
[1,2,3].none? {|x| x > 1 }

原因和第一个一样,select会遍历所有,而none?会遇到满足第一个条件的而停下来返回false。

4. 不要使用连续的select查询用一个select代替

有人会这么写:

1
[1,2,3,4].select {|x| x > 1 }.select {|x| x < 4 }

但最好写成一个select就可以了

1
[1,2,3,4].select {|x| x > 1 && x < 4 }

这个原因很明显就不解释了,感觉犯这种错误的人应该也不多。

5. 计算数组满足条件的值的长度

很多人会这么写:

1
[1,2,3].select {|x| x > 1 }.size

下面这种写法效率会更高点:

1
[1,2,3].count {|x| x > 1 }

select这种写法会生成新的对象,而下面count不会。

6. map后面做flatten操作

很多人会这么写:

1
[1,2,3].map {|x| [x,x+1] }.flatten

更有效率的写法如下:

1
[1,2,3].flat_map {|x| [x, x+1]}

原因是上面做的循环次数比下面方法多。

详解 Ruby 2.0 新特性 Keyword Argument

Ruby 2.0.0推出了一个新的特性Keyword Argument,中文名可能叫关键字参数或者命名参数,我在这里详细介绍下这个特性的具体细节。

什么是Keyword Argument呢?

在这里通过一个例子说明:

1
2
3
4
def log(msg, level: "ERROR", time: Time.now)
  puts "#{ time.ctime } [#{ level }] #{ msg }"
end
log("Hello!", level: "INFO")  #=> Mon Feb 18 01:46:22 2013 [INFO] Hello!

看起来很普通好像没有什么特别新的,是不是?

详细解释

在Ruby 1.9里面也有下面的调用方法方式:

1
log("Hello!", level: "INFO")

上面传的第二个参数是hash,定义像下面这样:

1
2
3
4
5
def log(msg, opt = {})
  level = opt[:level] || "ERROR"
  time  = opt[:time]  || Time.now
  puts "#{ time.ctime } [#{ level }] #{ msg }"
end

但是也许需求不是那么简单:

  • 希望hash如果传给我们不期望的key,需要抛出异常
  • 希望hash的其中一个key可以传nil

那我们需要把方法改成如下:

1
2
3
4
5
6
7
def log(*msgs)
  opt = msgs.last.is_a?(Hash) ? msgs.pop : {}
  level = opt.key?(:level) ? opt.delete(:level) : "ERROR"
  time  = opt.key?(:time ) ? opt.delete(:time ) : Time.now
  raise "unknown keyword: #{ opt.keys.first }" if !opt.empty?
  msgs.each {|msg| puts "#{ time.ctime } [#{ level }] #{ msg }" }
end

如果你处理类似需求的方法,会不会觉得很麻烦。如果使用Ruby 2.0 的新功能Keyword arguments,代码将会变得下面这么简单干净:

1
2
3
def log(msg, level: "ERROR", time: Time.now)
  puts "#{ time.ctime } [#{ level }] #{ msg }"
end

更多细节

我们可以调用的时候,参数部分省略

1
2
log("Hello!")                                  #=> Mon Feb 18 01:46:22 2013 [ERROR] Hello!
log("Hello!", level: "ERROR", time: Time.now)  #=> Mon Feb 18 01:46:22 2013 [ERROR] Hello!

Keyword argument的顺序不是很重要,但其他参数的顺序是不可以随便写的。

1
2
log("Hello!", time: Time.now, level: "ERROR")  #=> Mon Feb 18 01:46:22 2013 [ERROR] Hello!
log(level: "ERROR", time: Time.now, "Hello!")  # 运行错误

但如果你传入一个非期待的key时,将会报错

1
log("Hello!", date: Time.new)  #=> unknown keyword: date

如果你不想这个非期待的key报错,需要加入一个**开头的参数来保存这个key,

1
2
3
4
5
def log(msg, level: "ERROR", time: Time.now, **kwrest)
  puts "#{ time.ctime } [#{ level }] #{ msg }"
end

log("Hello!", date: Time.now)  #=> Mon Feb 18 01:46:22 2013 [ERROR] Hello!

当然你可以将Keyword argument和可变参数混合放一起使用,不太建议这么使用,会减少代码的可读性,增加了复杂度,容易出现bug。

1
2
3
def f(a, b, c, m = 1, n = 1, *rest, x, y, z, k: 1, **kwrest, &blk)
...
end

限制

可变参数和Keyword arguments混用的时候,容易出问题。例子:

1
2
3
4
5
6
7
def foo(*args, k: 1)
  p args
end

args = [{}, {}, {}]

foo(*args) #=> [{}, {}]

可变参数的最后一个值被赋给keyword arguments了

其次你也不能把**作为参数

1
2
3
def foo(**)
end
foo(k: 1) #=> unknown keyword: k

还有参数的key不能ruby里面的保留关键字,例:

1
2
3
def foo(if: false)
end
foo(if: true)

如果想使用if作为key,只能使用**参数如下:

1
2
3
4
def foo(**kwrest)
  p kwrest[:if]
end
foo(if: true) #=> true

解决 Can't Verify CSRF Token Authenticity 错误

问题现象

今天偶尔看看一个项目的代码,然后想运行看看,通过localhost:3000访问,登陆的时候一直报下面的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Started POST "/users/sign_in" for 127.0.0.1 at 2014-11-13 23:11:54 +0800
Processing by Devise::SessionsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"4HS675PeZDP+xBZkb45AJxQouoywke3lAJOvA3IFLt8=", "user"=>{"login"=>"xxx@test.com", "password"=>"[FILTERED]", "remember_me"=>"0"}, "
commit"=>"login"}

Can't verify CSRF token authenticity
Completed 422 Unprocessable Entity in 100ms

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
  actionpack (4.1.1) lib/action_controller/metal/request_forgery_protection.rb:176:in `handle_unverified_request'
  actionpack (4.1.1) lib/action_controller/metal/request_forgery_protection.rb:202:in `handle_unverified_request'
  devise (3.2.4) lib/devise/controllers/helpers.rb:182:in `handle_unverified_request'
  actionpack (4.1.1) lib/action_controller/metal/request_forgery_protection.rb:197:in `verify_authenticity_token'
  activesupport (4.1.1) lib/active_support/callbacks.rb:424:in `block in make_lambda'
  activesupport (4.1.1) lib/active_support/callbacks.rb:160:in `call'
  activesupport (4.1.1) lib/active_support/callbacks.rb:160:in `block in halting'
  activesupport (4.1.1) lib/active_support/callbacks.rb:166:in `call'
  activesupport (4.1.1) lib/active_support/callbacks.rb:166:in `block in halting'
  activesupport (4.1.1) lib/active_support/callbacks.rb:149:in `call'
  activesupport (4.1.1) lib/active_support/callbacks.rb:149:in `block in halting_and_conditional'
  activesupport (4.1.1) lib/active_support/callbacks.rb:149:in `call'
  activesupport (4.1.1) lib/active_support/callbacks.rb:149:in `block in halting_and_conditional'
  activesupport (4.1.1) lib/active_support/callbacks.rb:149:in `call'
  activesupport (4.1.1) lib/active_support/callbacks.rb:149:in `block in halting_and_conditional'
  activesupport (4.1.1) lib/active_support/callbacks.rb:86:in `call'
  activesupport (4.1.1) lib/active_support/callbacks.rb:86:in `run_callbacks'
  actionpack (4.1.1) lib/abstract_controller/callbacks.rb:19:in `process_action'
  actionpack (4.1.1) lib/action_controller/metal/rescue.rb:29:in `process_action'

原因推测

google 搜了下,很多人是ajax提交登陆的时候,没有设置authenticity_token才会发生, 但我这个是有authenticity_token的。

解决方法

最后发现原来此项目的session_store.rb配置了特定的域名,应该是为了子域名session共享而配置的。

1
2
3
4
Lvh::Application.config.session_store :cookie_store, key: '_lvh_session', domain: {
  production: '.lvh.com',
  development: '.lvh.local'
}.fetch(Rails.env.to_sym, :all)

所以本地开发环境访问的时候必须使用 lvh.local:3000访问, 这样登陆就没有问题了。

我的第一个全程马拉松

11月2号早上5点伴随着闹钟,早早的爬起床来,已经好久没有这么早起床了,还真有点不习惯。 快速洗漱过后,去赶公交,在公交上也看到了好几位参加2014上海国际马拉松的选手,有的人竟然只穿着短裤,还是女的,真是太厉害了。 从南京东路地铁口,发现好多参加比赛的选手,不是一般般多,看来花钱找罪受的人还不少,呵呵我也是其中一个。 本来需要等同事一起去比赛现场,可惜他打不到车,只能独自先去找换衣车。一路上发现人超多,上厕所的都排了好长的几条队伍。 到了比赛准备区,很多人在热身,好吧。我作为第一次参加的菜鸟也混了进去,进行了貌似很专业的热身动作,随着比赛时间越来越近, 什么样的人都来了,西游记的师徒四人,蓝精灵,济公。。。这些人就是来赚眼球的。

比赛开始,随着发令枪响起,我等了2分钟才出发,前面的人实在太多,刚起跑不久,下起了大雨,还很冷。。。真是郁闷。第一次跑马拉松就 遇到这种鬼天气,一开始大家跑的都很快,我也不急,重在参与,计划跑个20多公里退出,也不丢脸。所以省点体力,不要跑不到一半就退出,就 有点丢脸了。伴随着路边大妈和美女的加油声,不知不觉就跑完了10公里,大概花了1个多小时,这时雨也差不多停了,路上补充了一次水分。感觉 不是很累,我想20公里看来可以坚持下来。跑了大概18公里的时候,明显感觉有点累了,速度也慢了下来,这时也感觉有点饿了,路边只有饮料可以 补充,自己又忘记带能量补充,没办法只能多喝点饮料补充了。就这样坚持到了23公里,发现自己好像还能继续跑,只是跑一会后,稍微走一会也能 撑下去,想想继续跑吧,能坚持跑到30公里也算突破自己很多了,我平时练习最多才10公里。跑到快30公里的时候,发现隔壁的马路上写着39公里, 很多人在叫还有3公里了,加油啊!!!我想,靠,这些什么人啊咋这么快,我才30公里呢。那时候我已经开始每公里80%靠跑,20%靠走的频率再坚持, 跑到35公里开始感觉脚和关节已经很痛了,跑步和走路的分配基本就2:8了。这时不远的收容车在向你招手,自己想想都35公里了放弃太可惜了,继续坚持吧。就这样竟然坚持到了37公里,这时已经过去了4个小时,这时2个腿已经完全太不起来了,实在很痛,感觉脚底皮都磨破了,也有抽筋的迹象。 很多人都已经在步行了,我想算了就步行吧,反正6个小时,估计也能走到终点。就这样走走停停,路上再喝点饮料补充补充。拖着双腿,终于抵达终点,终点本想跑过去,最后发现还是只能走过去了,还好在6个小时内完赛了。

真的不可思议,第一次比赛,平时最多练过10公里,竟然跑完了全程。看来很多事情,不尝试一下,你永远都不知道自己的潜力有多大,希望下次有机会再次参加全程马拉松,争取跑出更好的成绩。

关于上海马拉松的组织:

总体组织的不错,包括后勤,补给站的设置,马路的监管,存衣库的管理。

我的个人建议:厕所貌似还是有点少,如果路边还有些食物补给就更好了。

避免mongoid循环处理太久时间

问题现象

使用mongoid的过程中发现一个问题,一个很简单的数据库查询,然后进行循环处理,循环始终跑不到最后,会抛出Moped::Errors::CursorNotFound异常。

1
2
3
  User.where(...).each do |user|
    # do something long time
  end

问题原因

官方说是查询时间太久了,导致cursor被服务器kill掉了

解决方法

第一种方法

将查询出来的结果放到临时变量再循环

1
2
3
4
  users = User.where(...)
  users.each do |user|
    # do something long time
  end

第二种方法

no_timeout方法再循环

1
2
3
  User.where(...).no_timeout.each do |user|
    # do something long time
  end

Rails 4.2性能提升新特性Adequate Record介绍

最近看了Rails 4.2的更新bolg, 发现在Rails 4.2中加了个Adequate Record的新特性,这个特性是用来改善数据库查询性能的,看了介绍, 把我所理解的写了下来。

性能提升变化图

性能变化图

测试的是Model.find(id)Model.find_by_name(name)方法, 提升了将近2倍,真厉害啊!

性能提升方法

一般的ActiveRecord查询转换成SQL数据库查询包含下面3个步骤

查询步骤

而最新的rails一般find查询都会被转化为如下SQL

1
2
3
4
SELECT * FROM posts WHERE id = ? [id, 10]
SELECT * FROM posts WHERE id = ? [id, 12]
SELECT * FROM posts WHERE id = ? [id, 22]
SELECT * FROM posts WHERE id = ? [id, 33]

你可以注意到SQL查询语句都是一样的,只是参数不一样。所以可以将3个步骤的第一步缓存起来, 如果下次做类似find查询,就可以跳过第一步,这样性能就提升了。

但性能提升只限下面3个方法:

1
2
3
Post.find(id)
Post.find_by_name(name)
Post.find_by(name: name)

参考链接

http://tenderlovemaking.com/2014/02/19/adequaterecord-pro-like-activerecord.html