简化查询复合输出的归一化数据

时间:2012-09-13 00:00:28

标签: ruby ruby-on-rails-3 rest activerecord database-normalization

在Rails 3应用程序中,我们有一个高度规范化的数据库模式,这是出于多种原因所必需的。此外,我们需要提供一些只读RESTful路由,其虚拟资源来自多个模型的“组合”(非规范化)数据,我们必须将结果显示为JSON文档。

例如,我们会有像State,City和Neighborhood这样的模型,每个模型都有自己的数据和关联。 “RESTful资源”是邻居,但我们总是希望包含关联的州和城市的名称。因此,对URI“/neighborhood/nm/albuquerque”的“GET”请求将返回新墨西哥州阿尔伯克基所有邻域的JSON数组,例如:

[{"state":"NM","city":"Albuquerque","neighborhood":"North Valley"},
 {"state":"NM","city":"Albuquerque","neighborhood":"Northeast Heights"}, //... ]

但是,省略URI(“GET /neighborhood/nm”)中的城市名称将返回新墨西哥州所有城市中所有社区的列表。

在这种情况下生成数据库查询的Rails 3首选方法是什么?

我能想象的最直接的方法是生成一个自定义SQL查询,从数据库中选择必要的数据并将结果记录作为JSON返回,而不执行任何ActiveRecord对象反序列化(因为我们只需要原始数据而不进行处理) ,例如:

def neighborhood # our Action in the target Controller...
  state, city, hood = params[:state], params[:city], params[:hood]
  query = <<-__HERE__
    SELECT s.name AS state, c.name AS city, n.name AS neighborhood
    FROM states AS s, cities AS c, neighborhoods AS n
    WHERE n.city_id=c.id AND c.state_id=s.id
  __HERE__
  query += ' AND s.slug=' + ActiveRecord::Base.sanitize(state) if state
  query += ' AND c.slug=' + ActiveRecord::Base.sanitize(city) if city
  query += ' AND n.slug=' + ActiveRecord::Base.sanitize(hood) if hood
  query += ' ORDER BY state, city, neighborhood ASC'
  render :json => ActiveRecord::Base.connection.select_all(query)
end

这个解决方案在概念上是直截了当的,并且依赖于数据库来进行繁重的工作,但感觉明显不是Rails-ish。我尝试使用Arel / AR查询和“as_json”覆盖的组合来实现相同的效果但是在经过一段时间和挫折后我似乎无法正确(并且高效)。

我是否在Rails 3中遗漏了大,新,酷的东西,或者这只是一种快速而肮脏的解决方案才是正确的方法?

1 个答案:

答案 0 :(得分:1)

您可能想尝试使用json构建器。几个轨道广播:

http://railscasts.com/episodes/322-rabl

http://railscasts.com/episodes/320-jbuilder

直接sql有点un-rails-ish。如果您不需要性能,基本查询将更容易填充一些变量,然后使用构建器来制作json。

如果您遇到性能问题,请在“解决”之前使用分析器找到问题!

修改 对于查询,您可以使用连接和包含。我假设州有很多城市,城市有很多社区。

query = Neighborhood.includes(:city => :state)
query = query.order('states.name, cities.name, neighborhoods.name ASC')
query = query.where("cities.name =?", params[:city]) if params[:city]
query = query.where("neighborhoods.name =?", params[:hood]) if params[:hood]
query = query.where("states.name =?", params[:state]) if params[:state]

从那里你可以构建你的json数据......

query.each do |hood|
  # code to build json row using:   hood.name, hood.city.name, and hood.city.state.name
end

更好的是,定义一些方法来帮助:

class Neighborhood ...
  def city_name
    city ? city.name : "No city"
  end

  def state_name
    (city && city.state) ? city.state.name : "No state"
  end
end

有了这个,我现在看到一个建筑师是矫枉过正的。只需映射一个哈希数组,然后调用to_json:

query = Neighborhood.includes(:city => :state)
query = query.order('states.name, cities.name, neighborhoods.name ASC')
query = query.where("cities.name =?", params[:city]) if params[:city]
query = query.where("neighborhoods.name =?", params[:hood]) if params[:hood]
query = query.where("states.name =?", params[:state]) if params[:state]

json = query.map { |hood|
  {"state" => hood.state_name, "city" => hood.city_name, "neighborhood" => hood.name}
}.to_json

(未经测试,但我很确定已经接近了)