最近公司的 Rails 项目里面有非 ActiveRecord 的数据库需要用 SQL 查询,比如 Amazon Redshift 之类的,这些表没有 Model,实现的时候查询基本上是手写 SQL,纯字符串拼接那种。
随着业务越来越复杂,SQL 拼接也越来越复杂,于是开始质疑为何不用类似 AcitveRecord 那种 DSL 的查询方式 where("name = ?", params[:name]).where(...).limit(100)
。
然而我查询了 ActiveRecord 以及 Ruby 社区现成的 Gem 发现,干这个事情的居然没一个能满足的,我一开始本以为 AcitveRecord 的 Arel 可以干这个事情,结果尝试下来看,那个查询方式和 ActiveRecord 的 DSL 用法不一样,而其他社区里面做这类事情的比如 mini_sql 也是以执行为主的,需要依赖 SQL Connection。
于是我按照我们的场景构造了一个简化的 SQL Builder,让它只干一件事情:“基于 DSL 生成 SQL 语句”
往 Gemfile 增加:
gem "sql-builder"
一个简单的查询
SQLBuilder.new("SELECT * FROM users")
.where("name = ?", "hello world")
.where("status != ?", 1)
.order("created_at desc")
.order("id asc")
.page(1)
.per(20)
.to_sql
=> "SELECT * FROM users WHERE name = 'hello world' AND status != 1 ORDER BY created_at desc, id asc LIMIT 20 OFFSET 0"
结合实际业务场景,我们的 SQL 条件可能会很复杂:
query = SQLBuilder.new("SELECT users.name, users.age, user_profiles.bio, user_profiles.avatar FROM users INNER JOIN user_profiles ON users.id = user_profiles.user_id")
# conditions by params
query.where("age >= ?", params[:age]) unless params[:age].blank?
query.where("status = ?", params[:status]) unless params[:status].nil?
if params[:created_at_from] && params[:created_at_to]
query.where("created_at >= ? and created_at <= ?", params[:created_at_from], params[:created_at_to])
end
query.order("id desc").limit(100).to_sql
=> "SELECT users.name, users.age, user_profiles.bio, user_profiles.avatar FROM users INNER JOIN user_profiles ON users.id = user_profiles.user_id WHERE age >= 18 AND status = 3 AND created_at >= '2020-01-03 10:54:08 +0800' and created_at <= '2020-01-03 10:54:08 +0800' ORDER BY id desc LIMIT 100 OFFSET 0"
从上面这段你可以看到,那些复杂的 SQL 依然还是你自己写的,SQLBuilder 只是在 where
、order
、limit
这些场景可以帮你处理复杂逻辑,因此,它几乎不会因为内部生成了某些 SQL 数据库不支持的语法。