在 Fish Shell 里面设置 Goenv

习惯了使用 rbenv 管理 Ruby 版本,现在使用 golang 了。就想使用上了 goenv 来方便管理 golang 版本。 我使用的是 fish shell,这边记录下如何设置 goenv 在 fish shell 环境下。

安装 goenv

mac 用户

1
brew install goenv

或者

1
git clone git@github.com:syndbg/goenv.git ~/.goenv

设置 config.fish

1
vim ~/.config/fish/config.fish

添加以下配置:

1
2
3
4
eval (goenv init - | source)
set -x PATH $HOME/.goenv/bin $PATH
set -gx PATH ‘/Users/grant/.goenv/shims’ $PATH
set -gx GOENV_SHELL fish

大功告成, 重开一个 shell 窗口,将自动加载 goenv。你可以随意安装 golang 版本了。

1
2
goenv install 1.10.3
goenv global 1.10.3

Rails 和 Wordpress 配合 Nginx 配置

由于项目需要公司主页需要使用 Wordpress,但是后台管理还是使用的 Rails 程序。 所以需要 nginx 修改配置才能达到效果。

Nginx 配置如下

Wordpress 是配置在 http://abc.com/www 子路径下。需要在 Rails 的主页处理的时候,自动跳转至此 URL。 wordpress 的服务器文件是放在 /home/deploy/apps/abc_home/www 下面。

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
server {
        listen 443;
        server_name  abc.com www.abc.com;
        ssl on;
        ssl_certificate   /etc/nginx/cert/xxx.pem;
        ssl_certificate_key  /etc/nginx/cert/xxx.key;
        ssl_session_timeout 5m;
        ssl_ciphers XXX;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        passenger_enabled on;
        passenger_min_instances 6;
        passenger_max_request_queue_size 300;
        rails_env    production;
        root         /home/deploy/apps/abc/current/public;


       location = /www { rewrite ^ /www/ last; }

       location ^~ /www/ {
          root /home/deploy/apps/abc_home/;
          index index.php;

          if (!-e $request_filename) {
            rewrite  ^(.*)$  /www/index.php?q=$1  last;
          }

          location ~ \.php(?|$) {
            include /etc/nginx/fastcgi_params;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
         }
      }
}

server {
        listen 80;
        server_name  abc.com www.abc.com;
        return 301 https://$server_name$request_uri;
}

如何部署 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 里面好一点。