【问题标题】:Rails ActiveRecord WHERE EXISTS queryRails ActiveRecord WHERE EXISTS 查询
【发布时间】:2015-07-31 20:39:41
【问题描述】:

我有一个返回我需要的 SQL 查询,但我无法将其转换为“Rails 方式”的活动记录查询。

我的 SQL 查询是:

SELECT * from trips 
WHERE trips.title LIKE "%Thailand%"
AND EXISTS (SELECT * from places WHERE places.trip_id = trips.id AND places.name LIKE "%Bangkok%")
AND EXISTS (SELECT * from places WHERE places.trip_id = trips.id AND places.name LIKE "%Phuket%")

我正在使用 Rails 尝试这样的事情:

@trips=Trip.where("trips.title LIKE ?", "%Thailand%")
@trips=@trips.includes(:places).where(("places.name LIKE ?","%Bangkok%").exists?) => true, ("places.name LIKE ?","%Phuket%").exists?) => true)

但它似乎不起作用,我不知道该尝试什么。

【问题讨论】:

  • 您使用的是哪个版本的 Rails?
  • 我使用的是 Rails 4.2.1
  • 我正在为您提供更长的答案,但由于您在 SQL 中使用了 LIKE,您可能希望查看 stackoverflow.com/questions/30042146/…,因为 LIKE 在不同的数据库中表现不同(尽管这这不是您遇到此查询问题的原因 - 只是给您一个快速警告)。

标签: mysql ruby-on-rails


【解决方案1】:

RAILS 5/6 编辑:https://github.com/rails/rails/pull/29619 开始,Rails 开始不鼓励我原来的答案直接调用 .exists。我已经更新它以使用新语法,它首先调用 arel 代理 (.arel.exists)。此外,从 Rails 5 开始,哈希条件在 EXISTS 子句中也可以正常工作。

考虑到所有这些,现在纯 ActiveRecord 方法是:

Trip.where("trips.title LIKE ?", "%Thailand%")
    .where( Place.where('trip_id = trips.id AND name LIKE ?', '%Bangkok%').arel.exists )
    .where( Place.where('trip_id = trips.id AND name LIKE ?', '%Phuket%').arel.exists )

如果这看起来有点吓人,你还有其他选择:

  • 您可以只使用.where('EXISTS(SELECT 1 FROM places WHERE trip_id = trips.id AND name LIKE ?', '%Bangkok%'),将SQL 直接嵌入到您的应用程序中。它没有那么时髦或酷,但从长远来看,它可能更易于维护——它不太可能被弃用或停止与未来版本的 rails 一起使用。
  • 选择一个 gem,比如 activerecord_where_assoc,它可以使语法更简洁,并且可以避免犯一些简单的错误(比如没有像我原来的答案那样正确地确定 EXISTS 查询的范围)。

【讨论】:

  • 这是最接近我最初尝试的解决方案,并且似乎是最简单的解决方案。你提到的警告并没有打扰我,因为我只需要使用需要文字字符串的 LIKE (据我所知)。
  • 对于将要生成的 SQL,这个答案实际上似乎是错误的。原始查询在 EXISTS 中有places.trip_id = trips.id,而这里生成的查询没有,并且有一个随机的不需要的 JOIN。因此,它似乎只是检查具有这些名称的地点是否存在,并且 Trip 是否无条件地链接到任何地点。实际为此使用 gem 的另一个原因:)
  • @MaximeLapointe 感谢您指出这一点——我很惊讶它花了将近五年的时间!我已经更新了答案以反映 Rails 当前的行为,并且还包含了关于您的 gem 的注释。
【解决方案2】:

使用Where Exists gem(注意:我是它的作者):

Trip.where("trips.title LIKE ?", "%Thailand%")
  .where_exists(:places, ['name LIKE ?', '%Bangkok%'])
  .where_exists(:places, ['name LIKE ?', '%Phuket%'])

【讨论】:

  • 不知道这个存在,看起来很棒!
  • 我知道你在那里做了什么:)
【解决方案3】:

这是我第一次尝试的重构版本,结果更接近您的预期目标(我对第一次尝试的误导表示歉意)。

app/models/trip.rb:

scope :with_title, ->(title) { where(arel_table[:title].matches('%' + title + '%')) }

def self.has_place(place_name)
  _trips = Trip.arel_table
  _places = Place.arel_table
  where(Place.joins(:trip).where(_places[:name].eq(place_name)).exists)
end

然后你可以写:

Trip.for_title('Thailand').has_place('Bangkok').has_place('Phuket')

这将给出以下 SQL:

SELECT "trips".* FROM "trips" WHERE ("trips"."title" LIKE '%Thailand%') AND (EXISTS (SELECT "places".* FROM "places" INNER JOIN "trips" ON "trips"."id" = "places"."trip_id" WHERE "places"."name" = 'Bangkok')) AND (EXISTS (SELECT "places".* FROM "places" INNER JOIN "trips" ON "trips"."id" = "places"."trip_id" WHERE "places"."name" = 'Phuket'))

这使您可以灵活地组合查询,而不必依赖查询中的硬编码 SQL,并且可以在数据库引擎之间移植。

此解决方案假定 Place belongs_to Trip(及其补充)。如果关联不同,请调整 joins 子句。

此解决方案的部分灵感来自 How to do "where exists" in ArelMake search NOT case sensitive on my rails app

【讨论】:

    【解决方案4】:

    不使用 gem 会更容易出错。当前接受的答案(在撰写本文时)是手动完成的,实际上并没有生成正确的 SQL。 (它已被修复,但这突出了出错的风险。)

    另一个可以做到这一点的宝石:activerecord_where_assoc(我是作者)

    有了它,你可以用这种方式做你想做的事:

    Trip.where("trips.title LIKE ?", "%Thailand%")
        .where_assoc_exists(:places, ['name LIKE ?', '%Bangkok%'])
        .where_assoc_exists(:places, ['name LIKE ?', '%Phuket%'])
    

    activerecord_where_assoc 更强大,带有full documentation,以及针对 Rails 4.2 到 6.0 的 CI 测试。这是introduction

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多