Sinatra 探险 (2)
在上一篇的结束, 在一个 Sinatra 项目中, 我用 Bundler 管理了所有 gem, 编写了 config.ru 让支持 Rack 的 Web 应用服务器(Puma)跑了起来, 同时使用 Sinatra::Base 的方式搭建了这个 Sinatra 应用, 接下来要处理其他的东西了.
Mysql 与连接池
在 Gemfile 文件中, 我已经将熟悉的 gem 'mysql'
添加进去了(因为熟悉 mysql 所以没使用 mysql2), 想想, 如果我使用的是 puma, 多线程处理的话是不是得为 mysql 的链接创建一个 pool ? 让 mysql 链接能够重复使用呢? 恩, 然后就在 github 中搜索 “pool”, 我找到了 connection_pool, 看了一下 commit 中有 , 因为知道他是 Sidekiq 的作者, 所以相信这个 gem 应该挺可靠的. 粗略看了一下, connection_pool 的使用通用性质的 pool 化的 gem, 他可以很多你想象得到的东西给 pool 化, 例如各种 conn (redis? memcached? mysql conn?), 再次感叹一下 ruby 提供的 magic. 因为这里会使用纯 SQL 查询, 所以就简化了, 将 DB 的初始化放到了 config.ru 文件中.
require "bundler"
Bundler.require
require './app'
Dir['./model/**/*.rb'].map { |f| require f }
class DB
@pool = ConnectionPool.new(size: 3, timeout: 5) do
conn = Mysql.new('localhost', ENV['username'], ENV['password'], 'dbname')
conn.query("SET NAMES UTF8")
conn
end
# 下面的方法从 connection_pool 暴露出来的
class << self
# 使用闭包的形式调用
def with(&block)
@pool.with &block
end
# 直接调用
def method_missing(name, *args, &block)
@pool.with do |connection|
connection.send(name, *args, &block)
end
end
end
end
run App
这样以后, 我在这个 Sinatra 项目的关联的代码中可以这样使用:
DB.query("SELECT now()").fetch_row
DB.with { |conn| … }
返回 JSON
因为此应用定位的是使用 Sinatra 计算并暴露一些 DB 数据, 建立 JSON API 来进行交互, 所以需要返回 JSON. 在刚刚开始的时候, 我相当直接的 gem 'json'
然后在需要的地方用个 object.to_json
或者 JSON.generte(obj)
来返回 JSON 值, 当我看过 Sinatra::JSON 我发现我错过了它.. 然后, 我就在 app.rb 中重新认识了它, 同样 Gemfile 也修改了:
app.rb
class App < Sinatra::Base
register Sinatra::Contrib
get '/' do
json {foo: "hello"}
end
get '/h' do "Hello World!" end
end
Gemfile
# A sample Gemfile
source "http://ruby.taobao.org"
gem 'sinatra', '~> 1.3.3'
gem 'sinatra-contrib', '~> 1.3.2'
gem 'puma', '~> 1.6.3'
gem 'connection_pool', '~> 0.9.3'
gem 'mysql', '~> 2.9.0'
gem 'json'
使用 Sinatra::JSON , 除了会自行转换 json 字符串, 而且还会自动帮我设置正确的 Content-Type 哦.
Hot reload with rerun
这些弄好以后, 我就开始编写 models/ 目录中的详细业务逻辑了, 也就在这时, 我发现我改动了 model 中的代码刷新页面而没有变化 T_T 我感觉很不习惯, 有点失落的感觉.. 然后 Google 搜索 “Sinatra hot reload” 就找到了 Frequently Asked Questions 然后就顺藤摸瓜, 知道了一个叫 rerun 的 gem, 定睛一看介绍, 他就是为使用 Sinatra 而出现的 囧 .
gem install rerun
安装好 rerun , 接下来运行的时候与 Sinatra 的文档中有点不一样, 因为我需要重复执行的命令不是 rakeup
而应该是 puma
, 所以将 rerun 'rackup'
改为 rerun 'puma -p 3000'
(习惯 3000 端口了)
wyatt sample$ rerun 'puma -p 3000'
00:18:37 [rerun] sample launched
Puma 1.6.3 starting...
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
[Listen warning]:
Missing dependency 'rb-fsevent' (version '~> 0.9.1')!
Please add the following to your Gemfile to satisfy the dependency:
gem 'rb-fsevent', '~> 0.9.1'
For a better performance, it's recommended that you satisfy the missing dependency.
Listen will be polling changes. Learn more at https://github.com/guard/listen#polling-fallback.
00:18:39 [rerun] Watching ./**/*.{rb,js,css,scss,sass,erb,html,haml,ru} using Polling adapter
好吧, 看到一个 missing 的警告, 就像他介绍的一样, 用不用自行决定吧, 我就没弄了因为应用文件不多. 我们来修改一下 app.rb 文件, 然后就会看到新增加
00:20:57 [rerun] Change detected: 1 modified
00:20:57 [rerun] Sending signal TERM to 981
- Gracefully stopping, waiting for requests to finish
00:20:57 [rerun] Osticketadapter restarted
Puma 1.6.3 starting...
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
恩, 成功啦~
其实在看 Frequently Asked Questions 的时候有一个 Sinatra::Reloader, 为什么没有用他呢? 因为他不会 reload 整个 app 的代码 T_T 也就是说, 我修改 app.rb 可以 reload, 可是我修改 models 中的代码就不行了, 所以还是使用了 rerun.
动态添加实例变量
由于我想将查询出来的结果动态的设置进入 model 所以, 就去寻找 ruby 文档看有没有动态设置元素的方法, 然后就寻找到一个 instance_variable_set
方法
ticket.rb
class Ticket
include JSON
# 需要在构造函数这样做, 否则 messages/responses 会失效
def initialize(row)
# 自动根据查询出来的 attr 设置实例变量, 为了进行 json 序列化
row.each { |k,v| self.instance_variable_set("@#{k}", v.force_encoding("utf-8")) }
end
class << self
def id
tickets = []
DB.query("SELECT ticket_id, ticketId, email, name, subject FROM ost_ticket
LIMIT 10, 10").each_hash { |row| tickets << Ticket.new(row) }
tickets
end
end
end
然后将修改 app.rb
app.rb
class App < Sinatra::Base
register Sinatra::Contrib
get '/' do
JSON.generate(Ticket.id)
end
get '/h' do "Hello World!" end
end
这样, 当服务器启动的之后, 访问根目录就能得到结果了.
ps: instance_variable_set
部分是很长时候以后在阅读 Ruby 元编程 后过来写的, 这个时候发现自己原来的理解是相当相当的简单, 也不太值得详细的去写, 也为了让这篇文章有一个结尾, 还是回来就原来理解的 instance_variable_set
部分做了一个总结, 随着对 Ruby 的不断学习, 感觉真的太棒了.