【问题标题】:Filtering model with HABTM relationship具有HABTM关系的过滤模型
【发布时间】:2013-05-01 19:17:12
【问题描述】:

我有 2 个模型 - 餐厅和特色。它们通过 has_and_belongs_to_many 关系连接。它的要点是您的餐厅具有许多功能,例如外卖、比萨饼、三明治、沙拉吧、素食选项……所以现在当用户想要过滤餐厅并假设他检查比萨饼和外卖时,我想显示所有兼具这两种功能的餐厅;比萨饼,送货,也许还有更多,但它必须有比萨饼和送货。

如果我做一个简单的.where('features IN (?)', params[:features]),我(当然)会得到那些有的餐厅——所以或披萨或外卖或两者兼有——这根本不是我想要的。

我的 SQL/Rails 知识有点有限,因为我是新手,但我问了一个朋友,现在我有了这个可以完成工作的 huuuge SQL:

Restaurant.find_by_sql(['SELECT restaurant_id FROM (
                                                  SELECT features_restaurants.*, ROW_NUMBER() OVER(PARTITION BY restaurants.id ORDER BY features.id) AS rn FROM restaurants
                                                  JOIN features_restaurants ON restaurants.id = features_restaurants.restaurant_id
                                                  JOIN features ON features_restaurants.feature_id = features.id
                                                  WHERE features.id in (?)
                                                ) t
                                                WHERE rn = ?', params[:features], params[:features].count])

所以我的问题是:有没有更好的 - 甚至更多的 Rails - 这样做的方式?你会怎么做?

哦,顺便说一句,我在 Heroku 上使用 Rails 4,所以它是 Postgres DB。

【问题讨论】:

    标签: sql ruby-on-rails ruby postgresql has-and-belongs-to-many


    【解决方案1】:

    这是一个 set-iwthin-sets 查询的示例。我提倡使用group byhaving 解决这些问题,因为这提供了一个通用框架。

    在您的情况下,这是如何工作的:

    select fr.restaurant_id
    from features_restaurants fr join
         features f
         on fr.feature_id = f.feature_id
    group by fr.restaurant_id
    having sum(case when f.feature_name = 'pizza' then 1 else 0 end) > 0 and
           sum(case when f.feature_name = 'delivery' then 1 else 0 end) > 0
    

    having 子句中的每个条件都计入其中一项功能的存在——“pizza”和“delivery”。如果这两个特征都存在,那么您将获得 restaurant_id。

    【讨论】:

    • 这对我现有的查询没有太大的改进,也不再是我想要的“rails 友好”。
    【解决方案2】:

    您的features 表中有多少数据?它只是一个 id 和名称的表吗?

    如果是这样,并且您愿意做一些非规范化,您可以通过将特征编码为 restaurant 上的文本数组来更轻松地做到这一点。

    使用此方案,您的查询归结为

    select * from restaurants where restaurants.features @> ARRAY['pizza', 'delivery']
    

    如果您想维护您的特征表,因为它包含有用的数据,您可以将特征 ID 数组存储在餐厅中并执行如下查询:

    select * from restaurants where restaurants.feature_ids @> ARRAY[5, 17]
    

    如果您事先不知道 id,并且希望在一个查询中全部完成,那么您应该能够按照以下方式做一些事情:

    select * from restaurants where restaurants.feature_ids @> (
      select id from features where name in ('pizza', 'delivery')
    ) as matched_features
    

    最后一个查询可能需要更多考虑...

    无论如何,如果您想了解更多详细信息,我实际上已经写了一篇关于Tagging in Postgres and ActiveRecord 的非常详细的文章。

    【讨论】:

    • 谢谢。自从你提供 RailsConf 演示以来,我一直在研究这个数组的东西。现在我只需要弄清楚如何迁移所有内容:)
    • 迁移可能非常简单。更改餐厅表并添加一个数组列。下一次更新查询大致类似于update restaurants set features = features_agg from (select restaurant_id, array_agg(name) as features_agg from restaurants_features left join features on features.id = restaurants_features.feature_id group by restaurant_id) 或至少类似的查询。抱歉,如果格式很奇怪,不确定您可以在评论中做多少。
    • Rails 4 rc1 不适用于数组迁移,所以现在 rc2 出现了,我尝试了这个,它运行良好。而且 SQL 的可读性要好得多:P Tnx 再次!
    【解决方案3】:

    这不是“复制和粘贴”解决方案,但如果您考虑以下步骤,您将获得快速工作的查询。

    • index feature_name 列(我假设列 feature_id 在两个表上都有索引)
    • 将每个feature_name 参数放入exists()

      select fr.restaurant_id
      from
          features_restaurants fr
      where
          exists(select true from features f where fr.feature_id = f.feature_id and f.feature_name = 'pizza') 
          and
          exists(select true from features f where fr.feature_id = f.feature_id and f.feature_name = 'delivery')
      group by 
          fr.restaurant_id
      

    【讨论】:

    • 这也不是 Rails 的方式。正如我所说,我的查询已经有效,我只是想知道是否有更漂亮的方法。用户最多可以选择 20 项功能,这样您的查询就会很快变得丑陋且难以阅读。
    • 我同意这不是 Rails 方式,但我认为基础 - sql 效率不高,这是主要问题。
    【解决方案4】:

    也许你在向后看?

    也许尝试合并每个功能返回的餐馆。

    简化:

    pizza_restaurants = Feature.find_by_name('pizza').restaurants
    delivery_restaurants = Feature.find_by_name('delivery').restaurants
    
    pizza_delivery_restaurants = pizza_restaurants & delivery_restaurants
    

    显然,这是一个单实例解决方案。但它说明了这个想法。

    更新

    这是一种无需编写 SQL 即可提取所有过滤器的动态方法(即“Railsy”方式)

    def get_restaurants_by_feature_names(features)
      # accepts an array of feature names
      restaurants = Restaurant.all
      features.each do |f|
        feature_restaurants = Feature.find_by_name(f).restaurants
        restaurants = feature_restaurants & restaurants
      end
    
      return restaurants
    end
    

    【讨论】:

    • 这可行,但也会产生许多查询并使事情复杂化 - 用户最多可以选择 20 个功能。
    • @MihaRekar 好的,然后用它制作一个方法以使其动态化。给我一分钟,我会整理一个例子
    • 让它更“有轨范”有什么意义?我认为 Ruby 比任何数据库慢 100 倍 - 你在数据库中做的越多越好,只有你可以尝试简化 sql。不要误会我的意思——我喜欢 activerecord 的懒惰 where().joins().select()——但上面的 Restaurant.allFeature.findN+1 的问题。
    • OP 要求提供更类似于 Rails 的解决方案。这通常意味着依赖 AR。我敢肯定,肯定有比我更好的答案。
    【解决方案5】:

    因为它是一个 AND 条件(OR 条件在 AREL 中会变得很冒险)。我重新阅读了您陈述的问题并忽略了 SQL。我想这就是你想要的。

    # in Restaurant
    has_many :features
    
    # in Feature
    has_many :restaurants
    
    # this is a contrived example. you may be doing something like 
    # where(name: 'pizza'). I'm just making this condition up. You
    # could also make this more DRY by just passing in the name if 
    # that's what you're doing. 
    
    def self.pizza
      where(pizza: true) 
    end
    
    def self.delivery
      where(delivery: true)  
    end 
    
    # query 
    Restaurant.features.pizza.delivery
    

    基本上,您使用“.features”调用关联,然后使用在特征上定义的 self 方法。希望我没有误解原来的问题。

    干杯!

    【讨论】:

    • 这可行,但也会产生许多查询并使事情复杂化 - 用户最多可以选择 20 个功能。
    • ActiveRecord 的 AREL 应该做一个惰性查询。
    • 这是一个四字查询,而不是难以管理的 7 行 SQL 语句。复杂性是相对的。 IMO 我认为您应该能够使用 .explain 来查看查询 weblog.rubyonrails.org/2011/12/6/… 您也可以使用 .include(:features) 来限制重复。虽然这会让你的记忆膨胀。或者您可以使用 AR 的“find_in_batches”底线不要过早优化,因为这是所有编程邪恶的根源
    【解决方案6】:
    Restaurant
      .joins(:features)
      .where(features: {name: ['pizza','delivery']})
      .group(:id)
      .having('count(features.name) = ?', 2)
    

    这似乎对我有用。不过我用 SQLite 试过了。

    【讨论】:

    • 这不会选择一家拥有比萨、外卖和露台的餐厅,因为它有 3 个功能。
    • 用你的params[:features]代替['pizza','delivery'],用params[:features].count代替2
    • 是的,我知道,但正如我所说,这不会选择具有所有要求的功能以及更多功能的餐厅。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-21
    • 2013-06-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多