如何部署 Rails 到 Ubuntu 14.04 服务器

由于经常开发 Rails 应用,也常常部署到 Ubuntu 服务器上,所以我就整理了 一篇比较完整的部署配置步骤,供大家以及自己以后参考。

环境系统说明

  • Ruby 2.3.1
  • Rails 4.2.6
  • Ubuntu 14.04 64 位

    配置步骤

1. 系统增加 deploy 用户

以 root 用户身份登入 ubuntu 系统,然后执行下面的命令增加具有 sudo 权限的 deploy 用户。

1
2
3
4
5
$ sudo adduser deploy
$ sudo adduser deploy sudo
$ visudo
# 将 sudo 用户改成运行 sudo 无需密码
% sudo   ALL=(ALL) NOPASSWD:ALL

2. 把你的 SSH 公钥加入到 authorized_keys

这一步是为了以后你自己本地使用 cap 部署代码准备的,这样的话,每次部署无需输入密码。

1
2
3
4
5
## 切换到 deploy 用户
$ su deploy
$ mkdir .ssh
$ vim .ssh/authorized_keys
# 粘贴你的 ssh 公钥,然后保存

3. 安装 Ruby 需要的依赖库

Ruby 安装的时候会需要用到一些系统的依赖库,所以提前装好。

1
2
3
4
$ sudo apt-get update
$ sudo apt-get install git-core curl zlib1g-dev build-essential libssl-dev libreadline-dev
libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev
python-software-properties libffi-dev nodejs

4. 安装 Ruby

我这里使用的是 Rbenv 安装 Ruby, 我这里使用的是 2.3.1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cd
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc
$ exec $SHELL

$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
$ echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc
$ exec $SHELL

$ rbenv install 2.3.1
$ rbenv global 2.3.1
$ ruby -v
$ gem install bundler

5. 安装 Nginx + Passenger

我使用 Nginx + passenger 来运行 Rails 应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7
$ sudo apt-get install -y apt-transport-https ca-certificates

# Add Passenger APT repository
$ sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger trusty main > /etc/apt/sources.list.d/passenger.list'
$ sudo apt-get update
$ sudo apt-get install -y nginx

$ sudo vim /etc/nginx/nginx.conf
  # 这行取消注释
  include /etc/nginx/passenger.conf;

$ sudo vim /etc/nginx/passenger.conf
  passenger_root /usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini;
  passenger_ruby /home/deploy/.rbenv/shims/ruby; # 设置 Rbenv

6. 安装 postgres

安装 postgres 的时候,需要注意将 pg_hba.conf 里面的配置改成 md5。

1
2
3
4
5
6
7
8
9
10
$ sudo apt-get install postgresql postgresql-contrib libpq-dev
$ sudo su - postgres
$ vim /etc/postgresql/9.3/main/pg_hba.conf
local   all             all                           md5
$ service postgresql restart
# 新建 app_db_user 的数据库用户名,请牢记数据库密码,待会需要配置到 database.yml 里面去
$ createuser --interactive -P blog_db_user
# 新建 blog_production 数据库
$ createdb blog_production -Ublog_db_user
$ exit

7. 代码部署

我是把代码放到 /home/deploy/apps/ 下面的,这里可以随意改动,不需要按照我这个配置。

1
2
3
4
5
6
7
8
$ cd ~
$ mkdir -p apps/blog/shared/config
$ cd apps/blog/shared/config
# 创建一些 application.yml database.yml secrets.yml

# 在服务器上生成公钥配置到项目的 deploy key 里面去
$ ssh-keygen
$ cat ~/.ssh/id_rsa.pub

服务器配置完成后,本地需要使用 capistrano 来自动部署代码,下面自己本机电脑上的操作。

Gemfile

1
2
3
4
5
6
7
8
group :development do
  gem 'capistrano', '~> 3.5'
  gem 'capistrano-rbenv', '~> 2.0'
  gem 'capistrano-bundler', '~> 1.1'
  gem 'capistrano-rails', '~> 1.1'
  gem 'capistrano-passenger', '~> 0.2'
  gem 'capistrano-sidekiq'
end

bundle 完成之后, 运行 bundle exec cap install 会生成默认的 Capfile, deploy.rb 和 config/deploy 目录下的各个环境的配置文件。

Capfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Load DSL and set up stages
require "capistrano/setup"

# Include default deployment tasks
require "capistrano/deploy"

# Include tasks from other gems included in your Gemfile
# require 'capistrano/rvm'
# 因为使用的是 rbenv
require 'capistrano/rbenv'
# require 'capistrano/chruby'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
require 'capistrano/passenger'
# 因为使用了 sidekiq,部署的时候会自动重启 sidekiq
require 'capistrano/sidekiq'

# 因为使用了 whenever,部署的时候会自动重新生成定时任务
require "whenever/capistrano"

# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }

config/deploy.rb

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
29
30
31
32
33
34
35
36
37
38
39
40
# config valid only for current version of Capistrano
lock '3.5.0'

set :application, 'blog'
set :repo_url, 'git@github.com:grant/blog.git'

set :rbenv_type, :user # or :system, depends on your rbenv setup
set :rbenv_ruby, '2.3.1'

# Default branch is :master
ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp

set :rails_env, 'production'

# 设置成代码的目录
set :deploy_to, '/home/deploy/apps/blog'

# Default value for :linked_files is []
set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml', 'config/application.yml')

# Default value for linked_dirs is []
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'private/sdf_files', 'vendor/bundle', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'public/system', 'public/uploads')

# Default value for keep_releases is 5
# set :keep_releases, 5
set :whenever_identifier, -> { "#{fetch(:application)}_#{fetch(:stage)}" }

# 下面是自动做 db:seed 在部署完成后
namespace :deploy do
  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
      # Here we can do anything such as:
      within release_path do
        with rails_env: fetch(:rails_env) do
          execute :rake, 'db:seed'
        end
      end
    end
  end
end

config/deploy/production.rb

1
server 'grantcss.com', user: 'deploy', roles: %w(web app db)

这个时候,在本地运行下面的命令,代码会自动部署到服务器上的 /home/deploy/apps/blog 目录

1
$ cap production deploy

8. 配置 nginx 启动 Rails 应用

将这个应用配置到 nginx 里面,然后删除默认的配置,重启 nginx 即可。设置的域名确保 A 地址指向你的服务器了。 至此,一个 Rails 应用部署配置完成了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ssh deploy用户 登录到服务器
$ cd /etc/nginx/sites-available/
$ vim blog
server {
    listen 80;
    server_name blog.grantcss.com;
    passenger_enabled on;
    rails_env    production;
    root         /home/deploy/apps/blog/current/public;
}

$ sudo ln -s /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/blog
$ sudo rm /etc/nginx/sites-enabled/default
$ sudo service nginx restart

如何在 Rails 中使用 PostgresSQL 表分区

当你在开发中遇到这样一种场景,需要收集单个设备的数据信息,这个这个设备每隔 10 秒就会发一次数据 到 Rails 服务器,服务器需要把每次来的数据收集起来,以便以后数据分析使用,但这个设备会很多,几千, 几万,甚至几十万都是有可能的。

那一个设备的一天的数据量就是 10 * 60 * 60 * 24 = 864000 条,一年的就是 3 个亿,如果 1 万个设备的话, 就是 1 万亿条数据,看起来数据量有点恐怖,如果放在单表里面,势必随着数据量的增大,查询,插入等数据操作 的性能都会受到很大影响。

这个时候,有什么好的方法可以解决这个问题呢? 我当时正好使用的是 PostgresSQL,于是查了些资料,很多人 推荐表分区来解决此问题,那让我们来看看如何使用这一特性吧。

什么是表分区

在 PG 里面就是一个主表,会有很多的子表继承主表,字段和主表一样。一般都是建立一个主表,里面是空,然后每个分区都去继承它。无论何时,都应保证主表里面是空的。

PostgresSQL 分区是把逻辑上的一个大表分割成物理上的几块。分区不仅能带来性能的提升,还能带来管理和维护上的方便。

分区的好处是:

  • 查询性能可以得到极大提升。
  • 更新的性能也可以得到提升,因为表的每块的索引要比在整个数据集上的索引要小。如果索引不能全部放在内存里,那么在索引上的读和写都会产生更多的磁盘访问。
  • 批量删除可以用简单的删除某个分区来实现。
  • 可以将很少用的数据移动到便宜的、转速慢的存储介质上。

表在多大情况下才考虑分区呢? PostgresSQL官方给出的建议是:当表本身大小超过了机器物理内存的实际大小时(the size of the table should exceed the physical memory of the database server),可以考虑分区。

Rails 中如何实现分区

Gemfile

因为本人使用的是 Rails 4.2 所以 Gemfile 配置如下, partitioned 是分区的主 gem, activerecord-redshift-adapter 是 partitioned 依赖的 gem ,为了兼容 rails 4 设置的版本。

1
2
gem "partitioned", github: "dkhofer/partitioned", branch: "rails-4-2"
gem 'activerecord-redshift-adapter',  github: "arp/activerecord-redshift-adapter", branch: "rails4-compatibility"

创建分区规则

因为我需要以 created_at 字段按月分区,所以需要继承 Partitioned::ByMonthlyTimeField 类。

1
2
3
4
5
6
7
8
9
10
11
class PartitionedByCreatedAtMonthly < Partitioned::ByMonthlyTimeField
  self.abstract_class = true

  def self.partition_time_field
    :created_at
  end

  partitioned do |partition|
    partition.index :id, unique: true
  end
end

指定表使用此分区规则

如果你想指定哪张表使用此分区规则,只需要这个表的 model 继承 PartitionedByCreatedAtMonthly, 而不是继承 ActiveRecord::Base。

1
2
class DeviceLog < PartitionedByCreatedAtMonthly
end

创建好 5 年的分区表

使用 migration 来提前创建分区表,这时候数据库会新建这样的分区表:”device_logs_partitions”.“p201607”, “device_logs_partitions”.“p201608”, “device_logs_partitions”.“p201609”…

1
2
3
4
5
6
7
8
9
10
11
12
class CreatePartitionedTables < ActiveRecord::Migration
  def up
    # 创建分区表专用的名字为 device_logs_partitions 的模式
    DeviceLog.create_infrastructure
    dates = DeviceLog.partition_generate_range(Date.today, Date.today + 5.year)
    DeviceLog.create_new_partition_tables(dates)
  end

  def down
    DeviceLog.delete_infrastructure
  end
end

测试

上面的配置完成后,运行 rake db:migrate,使用 DeviceLog 模型创建一条记录,你会发现创建的数据会根据 created_at 的值自动创建到对应的分区表里面去。

1
2
### 假设是今天 20170731 号运行的
DeviceLog.create(:log => 'xxx')

会看到如下的插入 SQL

1
INSERT INTO device_logs_partitions.p201607 (log,...) values ('xxx',...;

如何查询分区表的数据呢

因为所有数据都到分区表了,所以 gem 的作者不建议直接使用 DeviceLog.find 方法,取而代之的是下面的查询方法, 这样的话只会在分区表里面查询。

1
2
3
DeviceLog.from_partition(today).find(1)
DeviceLog.find(:first, conditions: {created_at:
  @select_date.beginning_of_day..@select_date.end_of_day})

其他分区规则

至此基本分区表用法就是这样,这个 gem 其实还提供其他的分区规则,具体用法类似:

1
2
3
4
5
6
Partitioned::ById
Partitioned::ByForeignKey
Partitioned::ByDailyTimeField
Partitioned::ByMonthlyTimeField
Partitioned::ByWeeklyTimeField
Partitioned::ByYearlyTimeField

总结

这个 gem 确实比自己在 PG 里面设定分区规则要方便很多,有类似需求的可以尝试使用下。

如何 Docker 化你的 Rails 应用

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中, 然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。

最近有项目需要打包快速部署到不同的服务器的需要,如果和以前一样,从安装 Ruby 开始,然后数据库,依赖的一些库(node), nginx 配置等,一步一步搞的话,又慢又麻烦, 于是就想尝试使用 Docker 部署看看。

这里就不解释 Docker 的一些基本教程和使用方法了,需要学习的可以去 https://www.gitbook.com/book/yeasy/docker_practice/ 看看。 未完待续…

Rails 5 中的迅捷开发模式

对于大型项目代码来说,Rails 5 增加了一个迅捷开发模式。

正如我们所知道的,在开发(development)模式时,如果代码发生了变化, 当一个请求到来时, Rails 会重新加载整个程序。这样做的方法已经发展了很多年。

很长一段时间的 Rails 简单地在每一个请求无条件地重新加载。

Rails 的 3.2 改善了这个处理,增加了文件系统监视器,通过遍历代码树的请求检查每个文件的修改时间(mtimes)。

这棵树的遍历,是每个请求才会发生,而不是显示每个页面视图都会做。尤其是遍历应用的每个 assets 文件,虽然遍历整个应用程序树一次可能不会花很多时间,但取决于你 assets 文件的数量和代码规模。

Rails 5 是增加了一个事件驱动的文件监视系统。当有新的文件变化的操作系统调用 Rails 的异步处理来设置标志。当收到请求时,该标志会被设定。

这个监视器默认是关闭的,应用程序可以在 Gemfile 增加如下配置:

1
2
3
group :development do
  gem 'listen', '~> 3.0.4'
end

在 Linux 和 Mac OS X 安装没有什么特别依赖,但是在 BSD 和 Windows 安装的话,需要安装些依赖。

即使事件驱动监视器被启用,Rails Console 里想加载新的代码,仍然需要手动 reload, 因为如果后台自动改变已经实例化的对象会让人感到惊讶。

相比遍历整个应用程序树,这个监视系统会加快开发模式里的请求 Response 速度,特别是对于大型项目来说。

配置 Sidekiq 使用 Upstart 启动和停止,并且使用 Monit 监视

upstart 配置

/etc/init/sidekiq.conf

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# /etc/init/sidekiq.conf - Sidekiq config

# This example config should work with Ubuntu 12.04+.  It
# allows you to manage multiple Sidekiq instances with
# Upstart, Ubuntu's native service management tool.
#
# See workers.conf for how to manage all Sidekiq instances at once.
#
# Save this config as /etc/init/sidekiq.conf then mange sidekiq with:
#   sudo start sidekiq
#   sudo stop sidekiq
#   sudo status sidekiq
#
# or use the service command:
#   sudo service sidekiq {start,stop,restart,status}
#

description "Sidekiq Background Worker"

# This starts upon bootup and stops on shutdown
start on runlevel [2345]
stop on runlevel [06]

# change to match your deployment user
setuid deploy
setgid deploy
env HOME=/home/deploy

respawn
respawn limit 3 30

# TERM is sent by sidekiqctl when stopping sidekiq. Without declaring these as
# normal exit codes, it just respawns.
normal exit 0 TERM

#instance $index

script
# this script runs in /bin/sh by default
# respawn as bash so we can source in rbenv
exec /bin/bash <<'EOT'
  # Pick your poison :) Or none if you're using a system wide installed Ruby.
  # rbenv
  # source /home/apps/.bash_profile
  # OR
  # source /home/apps/.profile
  # OR system:
  # source /etc/profile.d/rbenv.sh
  #
  # rvm
  # source /home/apps/.rvm/scripts/rvm
  # Logs out to /var/log/upstart/sidekiq.log by default

  # 服务器上的数据库用户名和密码在这个文件里面
  source /etc/environment

  # 服务器用的是 rbenv
  export HOME=/home/deploy
  export PATH="$HOME/.rbenv/bin:$PATH"
  eval "$(rbenv init -)"
  export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"

  cd /var/www/xxx_app/current
  exec bundle exec sidekiq -e production -P /var/www/xxx_app/current/tmp/pids/sidekiq.pid -L /var/www/xxx_app/current/log/sidekiq.log
EOT
end script

monit 配置

monit 使用 upstart 的配置将会非常简单

/etc/monit/conf.d/sidekiq.monitrc

1
2
3
4
5
check process sidekiq_application_production with pidfile /var/www/xxx_app/shared/tmp/pids/sidekiq.pid
  start program = "/bin/bash -c 'sudo start sidekiq'"
  stop program = "/bin/bash -c 'sudo stop sidekiq'" with timeout 90 seconds
  if totalmem is greater than 500 MB for 2 cycles then restart # eating up memory?
  group sidekiq

什么是 Service 对象? 什么时候使用?

经常在网上看到有人在讨论 Rails Service 对象模型。但是在 Rails 的 guide 里面并未提及这个对象的定义。所以看起来貌似很模糊,到底什 么是 service 对象呢? 什么时候要用呢?

什么是 Service

一般是来说 service 里面包含系统互动的相关处理。简单来说,service 里面一般都是 包含不止一个 model。

例如: 我们包含一个 User 的 Model,这个 User 包含密码。如果用户忘记密码,我们需要 发送一封 email 让他重置密码。这个功能就是 service。

关于重置密码也有可能有其他需求,例如系统管理员可以帮助重置密码。

在上面的例子里面,把 service 放到 MVC 世界解释如下:

  • Model (User) - 一个用户有一个密码。必须不能为空并且长度至少 8 位。这个模型里面并没有 关于各种情况重置密码的处理。
  • View - 忘记密码页面的 form 和成功失败状态的显示
  • Controller - form 提交之后,实例化忘记密码 service 发送邮件并且显示处理的结果。
  • Service - 用户的一个行为,发送邮件并且告诉 controller 处理结果。

目前 Service 存在在哪里?

有下面几种方法是容易把 service 的逻辑封装在 Rails 应用里面了。

  • 臃肿的 Model,紧身的 Controller 因为这个原则迫使很多人把 service 逻辑放在 model 里面。这种会把 model 自己的方法和一些多个 model 之间交互的方法混淆在一起。这些交互的方法可能会成为一个问题,重构和测试将会非常麻烦。

  • Concerns 当很多 model 有一样的方法的时候,我们一般会把这些方法抽取放到一个 module 里面去。concern 在 Rails 4 里面也是被推荐使用的。有时候确实是可以的,但这种方法增加了抽象性,有时候会发现去哪里找一个方法会变得困难。 而且会产生潜在的互相依赖的问题。

  • Observer 和 Callbacks 这是 model 生命周期的 hook 处理。对于上面那个例子,我们可以创建和保存一个 ForgottenPassword 对象, 然后在它的 callback 里面做发送邮件处理。这种缺点是和 model 的生命周期绑定太紧,这种行为可以非常迅速地变得难以跟踪,尤其是与正在运行的请求周期之外的观察者, 而回调可以使事情很难测试或预测行为。

  • 臃肿的 Controller 很多人使用 controller 用来连接对象之间的交互。这种方式看起来可以,但缺点是导致 controller 变得 太大变得笨重,测试也是非常麻烦。如果你在 worker 里面也需要这样的功能,可能会复制导致重复代码。

Service 对象

Service 对象实现了独立的功能。上面的忘记密码的例子可以写成这样:

1
2
3
4
5
6
7
class ForgottenPassword
  def initialize(user_id)
    user = User.find user_id
    mail = UserMailer.password_reset @user
    mail.deliver
  end
end

这个例子很简单,你可以做错误处理,以及设置返回值。这样以便 controller 或者 rake task 调用的地方 使用更加方便。

使用了 service, 可以使 User model 里面不需要和 UserMailer 产生耦合。 User model 的测试代码也将变得轻松,ForgottenPassword 的测试也会变得非常直接。

ForgottenPasswordController 代码将会是下面这个样子:

1
2
3
4
5
6
7
8
9
10
class ForgottenPasswordController
  def create
    begin
      ForgottenPassword.new(params[:user_id])
      # render the view
    rescue
      # Handle exceptions and render the view
    end
  end
end

这样 controller 的测试也将会变得简单,没有必要做交互业务的测试,因为 service 测试以及做了。

什么时候使用?

在我看来,model 间的相互交互处理可以作为独立的 service 来处理。这样做将使你隔离你的测试到 非常离散的和独立的领域。

而且,如果你修改现有的应用, 通过采用 service 会看到这些好处的。开发和维护的功能会更容易。

总结

Service 虽然在 Rails 里面并未提及,但我觉得值得这样去做。这样可以使你的程序更加简单,也有助于 测试和维护,我希望你可以考虑使用 Service。

Enumerable Module 里面比较实用的方法

Enumerable module 里面有很多实用的方法,可能有些方法,我们不是很熟悉。 如果知道的话,可能对于改善我们的代码会有所帮助。我这边提取了几个比较实用的 方法供大家参考。(注意本文是 Ruby 2.2.0 版本测试的结果)

chunk {|elt|…}

chunk 方法会将根据 block 里面表达式的返回值分组处理,注意只会分组连续的元素。 例如:

1
2
> [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].chunk { |n| n.even? }.to_a
=> [[false, [3, 1]], [true, [4]], [false, [1, 5, 9]], [true, [2, 6]], [false, [5, 3, 5]]]

注意 block 返回值里面是 nil 或 :_separator 的话,表示这个元素将会被删除掉, 对于过滤元素非常实用。 如果读文件的内容的时候,想忽略空行的时候可以使用这个方法过滤。

1
2
3
4
5
6
7
8
[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].chunk { |n| n > 5 || nil }.to_a
=> [[true, [9]], [true, [6]]]

[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].chunk { |n| n > 5 && nil }.to_a
=> [[false, [3, 1, 4, 1, 5]], [false, [2]], [false, [5, 3, 5]]]

[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].chunk { |n| n > 5 || :_separator }.to_a
=> [[true, [9]], [true, [6]]]

还有一个比较特别的 :_alone 表达式表示自己独立分组不和别的元素在一起。 例子:

1
2
3
4
5
[3, 9, 10, 2, 6, 7].chunk { |n| n > 5 }.to_a
=> [[false, [3]], [true, [9, 10]], [false, [2]], [true, [6, 7]]]

[3, 9, 10, 2, 6, 7].chunk { |n| n > 5 ? :_alone : true }.to_a
=> [[true, [3]], [:_alone, [9]], [:_alone, [10]], [true, [2]], [:_alone, [6]], [:_alone, [7]]]

在 rails 开发中也会经常遇到使用 chunk 的情况,例如:想把用户按照月份分组显示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- @users = User.order('created_at desc')

%h1 User Monthly Breakdown

- @users.chunk { |u| u.created_at.strftime("%B %Y") }.each do |month, users|
  %h3= month
  %table.table
    %thead
      %tr
        %th Name
        %th Email

    - users.each do |user|
      %tr
        %td= user.name
        %td= user.email

partition { |obj| block }

将一个集合根据 block 的返回值,分割成2个。

1
(1..6).partition { |v| v.even? }  #=> [[2, 4, 6], [1, 3, 5]]

max_by, min_by, minmax_by

从名字上就可以猜出来是用来取最大最小值的。

1
2
3
4
5
a = %w(albatross dog horse)
a.max                        #=> "horse"
a.max_by { |x| x.length }   #=> "albatross"
a.min_by { |x| x.length }   #=> "dog"
a.minmax_by { |x| x.length }   #=> ["dog", "albatross"]

zip

将 enum 每个元素和参数的每个对应元素合并。 例子:

1
2
3
4
5
6
a = [ 4, 5, 6 ]
b = [ 7, 8, 9 ]

[1, 2, 3].zip(a, b)      #=> [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
[1, 2].zip(a, b)         #=> [[1, 4, 7], [2, 5, 8]]
a.zip([1, 2], [8])       #=> [[4, 1, 8], [5, 2, nil], [6, nil, nil]]

[译] Rails Sessions 是如何工作的?

看到 Justin Weiss 写的一篇 Rails 的 Session 是如何工作的,觉得写的不错。 就翻译出来,和大家共勉。以下是正文:

如果 Rails 不能告诉你谁在访问你的网站?如果你不知道同一个人在访问2个不同的页面? 如果所有的数据在你返回 response 之前都消失的话?

对于一个静态网站来说也许没有问题。但是大多数应用程序需要存储一个用户的数据的。可能是一个 用户 id 或者 选择的语言偏好,或者他们通过 iPad 访问你的网站,想看看桌面的版本。

session 是一个非常好的地方来放置这些数据。一般是存储一些不止一个请求需要用的数据。

Session 使用起来很简单。

1
session[:current_user_id] = @user.id

但是它们会有点神奇。 session 是什么? Rails 是怎么处理这些数据,对于不同的人显示不同的数据。 还有你怎么决定在哪里存储你的 session 数据呢?

什么是 Session?

session 就是存储多个请求需要共享的数据。你可以设置一些数据在 controller 里面。

1
2
3
4
5
6
app/controllers/sessions_controller.rb
def create
  # ...
  session[:current_user_id] = @user.id
  # ...
end

然后在另外一个 controller 里面读取。

1
2
3
4
5
6
app/controllers/users_controller.rb

def index
  current_user = User.find_by_id(session[:current_user_id])
  # ...
end

这个看起来可能不是很有趣。但是这个协调了浏览器和 Rails 应用之间的联系。这个是通过 cookies 实现的。

当你对一个页面发送请求的话,rails 服务器的response将会返回一个 cookie。

1
2
3
$ curl -I http://www.google.com | grep Set-Cookie

Set-Cookie: NID=67=J2xeyegolV0SSneukSOANOCoeuDQs7G1FDAK2j-nVyaoejz-4K6aouUQtyp5B_rK3Z7G-EwTIzDm7XQ3_ZUVNnFmlGfIHMAnZQNd4kM89VLzCsM0fZnr_N8-idASAfBEdS; expires=Wed, 16-Sep-2015 05:44:42 GMT; path=/; domain=.google.com; HttpOnly

你的浏览器将会保存这些 cookie。直到你的 cookie 过期,每次你发送一个请求,浏览器将会把这个 cookie 发送 到服务器。

1
2
3
4
5
> GET / HTTP/1.1
> User-Agent: curl/7.37.1
> Host: www.google.com
> Accept: */*
> Cookie: NID=67=J2xeyegolV0SSneukSOANOCoeuDQs7G1FDAK2j-nVyaoejz-4K6aouUQtyp5B_rK3Z7G-EwTIzDm7XQ3_ZUVNnFmlGfIHMAnZQNd4kM89VLzCsM0fZnr_N8-idASAfBEdS; expires=Wed, 16-Sep-2015 05:44:42 GMT; path=/; domain=.google.com; HttpOnly

很多 cookie 看起来是乱码一样的文字组合,其实应该这样。这个数据并不是给浏览器的使用用户看的, 是给你的 Rails 应用读的,也只有它可以读出来里面的信息。

对于 session 我们需要做什么呢?

这时候,我们有了 cookie,你把数据放在里面,下一个请求的时候就可以读取了。那和 session 有什么不同呢? 默认情况下,在 Rails 里面是没有什么不同的。只不过 Rails 做了更多处理来保证 cookie 数据的安全。 除了这个以外,它工作的和你期待的一样。你的 Rails 应用把一些数据放 cookie,然后你从 cookie 里面读 取这些数据。但是 cookie 也不是 session 最好的存储方案。

  • 在 cookie 里面你最多只能存储 4K 的数据 一般情况下是足够了,但有时也会不够。
  • 每次发送请求都会发送 cookie。 如果 cookie 比较大意味着request和response的请求也会比较大,将会影响网站的访问速度。
  • 如果你不小心泄露了你的 secret_key_base, 那就意味着你的用户可以伪造 cookie 入侵的你网站。
  • 存储一些不安全的数据到 cookie。

如果你对这些问题都很小心的话,不是什么大问题。如果当年由于某些原因不想把 session 数据存储到 cookie里面的 话, rails 也提供了其他方式供你选择。

其他存储方式

虽然session数据不存储在 cookie 里面,但他们工作的流程基本类似。下面是实际的一个例子来解释下:

把 session 数据存储在数据库里面 1. 当年使用 session[:current_user_id] = 1 这个赋值。 2. Rails 将会创建一个 sessions 表产生一个随机的 session Id。(09497d46978bf6f32265fefb5cc52264) 3. 他将会存储 {current_user_id: 1} (Base 64编码)到 session 的 data 属性里面。 4. 而且将会返回 session id,然后将这个 id (09497d46978bf6f32265fefb5cc52264) 存到 cookie 里面发送到浏览器。

下次浏览器发送请求的时候

  1. 将会把 cookie 发送到服务器端:
1
2
Cookie: _my_app_session=09497d46978bf6f32265fefb5cc52264;
path=/; HttpOnly
  1. 当你使用 session[:current_user_id] 取值的时候
  2. Rails 会使用这个 id 去 sessions 表里面查询出对应的 session 值。
  3. 最后从找到的记录的 data 属性里面返回 current_user_id

不管你的 session 存储在 数据库还是 memcache,还是 Redis 里面。大多数都是 按照这样的流程在工作。这个时候 cookie 只是存储的 session ID, rails 会 自己根据这个 ID 去找 session 里面存储的数据。

cookie,cache 还是数据库存储呢?

首先存储到 cookie 是最简单的,他不需要你设置什么额外的操作。如果不想存在 cookie,可以 选择存储到 数据库 或 cache 里面。

存储 session 到 cache

你可能已经使用 memcache 存储你的一些页面缓存。cache 是第二方便存储 session 的地方。 你不需要担心 session 过度增长的问题,因为旧的 session 会自动被删除掉如果 cache 变得很大。 而且他访问速度很快,就相当于访问内存数据一样。 但也有缺点的:

  1. 如果你想保存旧的 session 数据,可能你并不希望被删除掉。
  2. 你的 session 将会占空间,如果你没有足够的内存,会遇到内存很多被 占用的情况,或者 session 很早就过期了。
  3. 如果你想重置你的 cache,可能由于 rails 升级之类导致的,这样会导致所有人的 session 都失效了。

存储到数据库

如果你想保存旧的 session 直到过期之前,你可以考虑存储到数据库。例如 Redis 或者 ActiveRecord。 但是数据库存储也有缺点的:

  1. 如果存储到数据库,session 不会自动清理的。需要你自己手动做的。
  2. 你必须知道如果很多 session 数据的话,数据库有什么影响? 例如: redis 的话,你必须保证有足够大的内存,不要一会就占满内存了。

  3. 你必须得小心,可能由于网络攻击或者网络蜘蛛导致产生大量无用的 session 数据,浪费了你的数据库空间。

大多数问题可能发生的比较少,但你必须有和这个意识。

该如何选择呢?

如果你确定 cookie 里面保存的缺点不会是问题的话,那就首选 cookie吧,毕竟不需要什么特殊配置,而且也不需要 特别维护。

cookie 之外,我一般首选 cache,然后是数据库。因为我认为 session 只是临时数据,所以放 cache 里面好一点。

Rails 开发中经常犯的错误

1 - 在 Model 以外的地方做数据库查询

不好的例子

1
2
3
4
5
6
7
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.where(active: true).order(:last_login_at)
  end

end

上面这段代码复用率不高并且不易测试。如果你在其他地方也想做同样的查询,将会产生重复代码。

比较好的例子

上面的查询我们可以将 controller 里面的查询 通过 model 的 scope 来代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# app/models/user.rb
class User < ActiveRecord::Base

  scope :active, -> { where(active: true) }
  scope :by_last_login_at, -> { order(:lasst_login_at) }

end

# app/controllers/users_controller.rb
class UsersController < ApplicationController

  def index
    @users = User.active.by_last_login_at
  end

end

记住如果使用 where, order, joins, includes, group, having 等查询的话,记得放 model 里面去。

2 - 在 View 里有业务逻辑处理

不好的例子

1
2
3
4
5
6
7
8
<!-- app/views/posts/show.html.erb -->
...

<h2>
  <%= "#{@comments.count} Comment#{@comments.count == 1 ? '' : 's'}" %>
</h2>

...

上面这段代码看起来没有什么错误,但是看上去 Ruby 代码使得 HTML 可读性不是很好,你应该 停止做这样的事情,这个同样会使得代码复用性不够高,而且不方便独立测试。

比较好的例子

将上面的方法独立到 helper 里面去。

1
2
3
4
5
6
7
8
9
10
11
# app/helpers/comments_helper.rb
module CommentsHelper
  def comments_count(comments)
    "#{comments.count} Comment#{comments.count == 1 ? '' : 's'}"
  end
end

<!-- app/views/posts/show.html.erb -->
<h2>
  <%= comments_count(@comments) %>
</h2>

改完之后会发现 html 变得简单,可读性变好,helper 里面的代码可以被其他地方使用了, 测试也更加方便了。

另外解决这个问题的方法是使用 decorators,具体使用可以参考 draper 这个 Gem。

3 - 使用无意义的变量名或方法名

不好的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# app/models/topic.rb
class Topic < ActiveRecord::Base

  def self.r_topics(questions)
    rt = []

    questions.each do |q|
      topics = q.topics

      topics.each do |t|
        if t.enabled?
          rt << t
        end
      end
    end

    Topic.where(id: rt)
  end

end

这个代码主要不好的地方是你需要花很多时间去理解里面的业务逻辑, r_topics 方法名什么意思? tr 变量干嘛的? 从第一眼里面无法 猜测到作者的企图。

比较好的例子

我们通过使用有意义的名字定义变量和方法,这个将会帮助开发者非常容易理解你的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# app/models/topic.rb
class Topic < ActiveRecord::Base

  def self.related(questions)
    related_topics = []

    questions.each do |question|
      topics = question.topics

      topics.each do |topic|
        if topic.enabled?
          related_topics << topic
        end
      end
    end

    Topic.where(id: related_topics)
  end

end

这样的好处是:

  • 第一次看到这个方法名,就能推测出这个方法会返回什么。 一些关于某个 question 的 topic 集合。
  • related_topics 是存储 question 相关的 topic 的一个数组, 而上面方法里面的 rt 根本就不知道什么意思。
  • 使用 topic 代替变量 t, question 代替 q。会让第一次读你代码的人 非常容易理解,这样就可以让代码来描述自己,这才是最好的。

4 - 使用 Unless 或 否定表达式 在判断条件里

不好的例子

1
2
3
4
5
6
7
8
9
10
# app/services/charge_user.rb
class ChargeUser

  def self.perform(user, amount)
    return false unless user.enabled?

    PaymentGateway.charge(user.account_id, amount)
  end

end

这个代码看起来并不是很难理解,但是使用了unless将会给代码增加些一点复杂度,因为必须要反响思考。

比较好的例子

如果我们使用 if 的话代码会更加容易理解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# app/models/user.rb
class User < ActiveRecord::Base

  def disabled?
    !enabled?
  end

end

# app/services/charge_user.rb
class ChargeUser

  def self.perform(user, amount)
    return false if user.disabled?

    PaymentGateway.charge(user.account_id, amount)
  end

end

是不是感觉代码可读性更加好了呢?记住尽量使用 if 或 肯定表达式,如果没有肯定表达式, 可以增加一个,像上面 User Model 改的那样。

5 - controller 有很多业务逻辑判断

不好的例子

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
# app/models/user.rb
class User < ActiveRecord::Base

  def enable!
    update(enabled: true)
  end

end

# app/controllers/users_controller.rb
class UsersController < ApplicationController

  def enable
    user = User.find(params[:id])

    if user.disabled?
      user.enable!
      message = "User enabled"
    else
      message = "User already disabled"
    end

    redirect_to user_path(user), notice: message
  end

end

这段问题是你在更新 enabled 之前会查询是不是 disabled。这个判断不应该放在 controller 里面和其他代码在一起。

比较好的例子

应该将这个判断逻辑移动到 model 里面去。

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
# app/models/user.rb
class User < ActiveRecord::Base

  def enable!
    if disabled?
      update(enabled: true)
    end
  end

end

# app/controllers/users_controller.rb
class UsersController < ApplicationController

  def enable
    user = User.find(params[:id])

    if user.enable!
      message = "User enabled"
    else
      message = "User already disabled"
    end

    redirect_to user_path(user), notice: message
  end

end

这样 controller 代码将更加简单。

6 - 使用复杂的判断条件

不好的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def destroy
    post = Post.find(params[:id])

    if post.enabled? && (user.own_post?(post) || user.admin?)
      post.destroy
      message = "Post destroyed."
    else
      message = "You're not allow to destroy this post."
    end

    redirect_to posts_path, notice: message
  end

end

上面的判断问了太多的条件了,其实总结起来就是判断这个用户是否可以删除。

比较好的例子

这个判断条件如果我们想以后复用方便的话,最好是移动到 model 里面去。

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
# app/models/user.rb
class User < ActiveRecord::Base

  def can_destroy_post?(post)
    post.enabled? && (own_post?(post) || admin?)
  end

end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController

  def destroy
    post = Post.find(params[:id])

    if user.can_destroy_post?(post)
      post.destroy
      message = "Post destroyed."
    else
      message = "You're not allow to destroy this post."
    end

    redirect_to posts_path, notice: message
  end

end

所以当你什么时候遇到有 && 或 || 的条件判断的时候,你可以考虑把这个条件抽象成一个方法, 为之后复用使用。

7 - 在 Model 的实例方法里面没有必要使用 self

不太好的例子

1
2
3
4
5
6
7
8
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    "#{self.first_name} #{self.last_name}"
  end

end

上面的方法比较简单,但没有必要使用 self ,这样会使代码看上去更加简单易读。

比较好的例子

在实例方法里面是没有必要使用 self 的,除非是在赋值的时候必须加上 self。

1
2
3
4
5
6
7
8
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    "#{first_name} #{last_name}"
  end

end

8 - 使用条件判断并且返回

不太好的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    if name
      name
    else
      "No name"
    end
  end

end

# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    name ? name : "No name"
  end

end

这段代码的问题是增加了控制逻辑,其实是没有必要的。

比较好的例子

下面这种很简单的方式就可以搞定了。

1
2
3
4
5
6
7
8
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    name || "No name"
  end

end

|| 和 && 操作符是功能非常强大的,他们可以帮助你简化很多代码。

Ruby 2.2 中的新方法

目前围绕 Ruby 2.2 来说,讨论的最多的是垃圾回收的更新。GC 目前会清除掉 symbol,并且 在减少暂停时间的方面有算法的改善。这些改变确实令人感到兴奋,但是其实还是也增加了一些新方法。 让我们看看 2.2 里面增加什么新玩意。

binding#local_variables

如果你想知道一个 scope 范围内有哪些本地变量的话,就可以使用这个方法获取。

1
2
3
4
5
6
7
8
9
10
11
def addition(x, y)
  puts binding.local_variables.inspect
  z = x + y
  puts binding.local_variables.inspect
  z
end

> addition(2, 3)
[:x, :y, :z]
[:x, :y, :z]
=> 5

binding#receiver

这个方法可以让你知道调用方法的对象是谁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Cat
  def self.type
    binding.receiver
  end
end

> Cat.type
Cat

class Tiger < Cat
end

> Tiger.type
Tiger

Dir#fileno

得到文件夹的文件描述符。文件描述符在形式上是一个非负整数。实际上,它是一个索引值, 指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时, 内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。 但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。所以此方法在windows里面运行,将会报错。 习惯上,标准输入(standard input)的文件描述符是 0,标准输出(standard output)是 1, 标准错误(standard error)是 2。

1
2
3
4
> $stdout.fileno
1
> Dir.new('.').fileno
8

Enumerable#slice_after

这个方法是对目前已存在的方法slice_before的相对的实现。我们先看看slice_before是怎么工作的。

slice_before方法是用来按照一定的条件分割数组这样的enumerables。举个例子看下:

1
2
> [1, 'a', 2, 'b', 'c', 3, 'd', 'e', 'f'].slice_before { |e| e.is_a?(Integer) }.to_a
[[1, "a"], [2, "b", "c"], [3, "d", "e", "f"]]

可以看到,这个方法在1,2,3前面做了分割。

其实也是可以传一个参数代替block的,如果是一个参数的话,参数是使用===运算符来检查的(和case一样)。 所以我们可以得到同样的结果如果我们传入Integer参数。

1
2
> [1, 'a', 2, 'b', 'c', 3, 'd', 'e', 'f'].slice_before(Integer).to_a
[[1, "a"], [2, "b", "c"], [3, "d", "e", "f"]]

那再看看slice_after的结果,就可以非常容易理解了。

1
2
> [1, 'a', 2, 'b', 'c', 3, 'd', 'e', 'f'].slice_after(Integer).to_a
[[1], ["a", 2], ["b", "c", 3], ["d", "e", "f"]]

Enumerable#slice_when

一个非常有趣的方法是slice_when,不像slice_after方法,这个方法只接受block。这个block接受的是 2个元素,根据block返回true来分割。

1
2
> [1, 3, 4, 5, 7, 8, 9, 10, 12].slice_when { |a, b| a + 1 != b }.to_a
[[1], [3, 4, 5], [7, 8, 9, 10], [12]]

这个方法也可以用来查找一个数组里面某个数字出现的频率。

1
2
> Array.new(10) { rand(3) + 1 }.sort.slice_when(&:!=).map { |x| [x.first, x.size] }
[[1, 4], [2, 4], [3, 2]]

从结果可以看出,1 和 2 分别出现了 4 次,3 出现了 2 次。

Float#next_float, Float#prev_float

这2个方法返回可表现的下一个或者前一个浮点数。注意并不是所有的浮点数都可以表现出来。

1
2
> 1.0.next_float
1.0000000000000002

可以注意到忽略了1.0000000000000001,因为1.0000000000000001不能通过Float表现出来。

1
2
> 1.0000000000000001
1.0

还有需要注意的是2个值之间的间隔并不一定是相等的。

1
2
3
4
5
> 2.0.prev_float
1.9999999999999998

> 2.0.next_float
2.0000000000000004

这2个方法是用来发现ULP(unit of least precision)的值是非常方便的,但实际开发中有可能很少会用到, 但是知道的话确实对于更好。

File.birthtime, File#birthtime, File::Stat#birthtime

我们已经有了文件的atime,ctimemtime方法,2.2里面增加了文件的birthtime的方法。即文件的创建时间。

1
2
3
4
5
6
7
8
> File.new('test', 'w').birthtime
2015-01-06 19:24:44 -0600

> File.birthtime('test')
2015-01-06 19:24:44 -0600

> File::Stat.new('test').birthtime
2015-01-06 19:24:44 -0600

Kernel#itself

一个返回本身自己的方法。可能你会问,这个方法有什么用?用的比较多的是group方法。例:

1
2
3
4
5
> 1.itself
1

> [2, 3, 3, 1, 2, 3, 3, 1, 1, 2].group_by(&:itself)
{2=>[2, 2, 2], 3=>[3, 3, 3, 3], 1=>[1, 1, 1]}

在map或select方法里面也是可以使用的。

Method#curry(arity)

返回一个方法的proc,arity是Proc接受的参数的个数。如果参数不足,会返回另外一个Proc。 只有当参数足够了的话才会调用方法。例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def foo(a,b,c)
  [a, b, c]
end

proc  = self.method(:foo).curry
proc2 = proc.call(1, 2)          #=> #<Proc>
proc2.call(3)                    #=> [1,2,3]

def vararg(*args)
  args
end

proc = self.method(:vararg).curry(4)
proc2 = proc.call(:x)      #=> #<Proc>
proc3 = proc2.call(:y, :z) #=> #<Proc>
proc3.call(:a)             #=> [:x, :y, :z, :a]

Method#super_method

看着这个方法的名字,估计也猜的出来是干嘛的,这个方法就是调用父类的方法。如果父类 没有这个方法,将会返回nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Cat
  def speak
    'meow'
  end
end

class Tiger < Cat
  def speak
    'roar'
  end
end

> Tiger.new.method('speak')
#<Method: Tiger#speak>
> Tiger.new.method('speak').super_method
#<Method: Cat#speak>
> Cat.new.method('speak').super_method
nil

Hash支持字符串 + 冒号作为 key 了

在 Ruby 2.2 之前,如果定义以字符串作为 key 的 Hash 的时候,是不可以写成这样的。 2.2 里面已经支持这种写法了。

1
{"key": "value"}