activerecord_where_assoc 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +3 -0
- data/README.md +11 -3
- data/lib/active_record_where_assoc.rb +1 -2
- data/lib/active_record_where_assoc/core_logic.rb +2 -2
- data/lib/active_record_where_assoc/query_methods.rb +58 -53
- data/lib/active_record_where_assoc/version.rb +1 -1
- metadata +3 -5
- data/ALTERNATIVES_PROBLEMS.md +0 -231
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 455c7e8523f81f043fe2933c78e54e3799f66f01bff81dd34d21185c3b268aaf
|
4
|
+
data.tar.gz: 1895e78079d3f9f06f532e01dcd7313a9c7fa27e3f38ec411359a1352fd755db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fecba216f37489edaaf31945436228781fae1a102a336e93e3958b47305ca04493981b6c8291743cdfd54db236560a2c3027cae61c4988997f573368b3ad7770
|
7
|
+
data.tar.gz: d2011c3dbee23ab3244df38e2069a5bbac4ac8c062ed4028af3090bb91222f31f98132bc62e5db0804af34736310b9d1e763dc82b5cc99b1d231fb24c1553e9e
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -20,6 +20,8 @@ my_user.posts.where_assoc_count(5, :>=, :comments) { |comments| comments.not_spa
|
|
20
20
|
|
21
21
|
These allow for powerful, chainable, clear and easy to reuse queries. (Great for scopes)
|
22
22
|
|
23
|
+
Here is an [introduction to this gem](INTRODUCTION.md).
|
24
|
+
|
23
25
|
You avoid many [problems with the alternative options](ALTERNATIVES_PROBLEMS.md).
|
24
26
|
|
25
27
|
Here are [many examples](EXAMPLES.md), including the generated SQL queries.
|
@@ -54,6 +56,12 @@ Or install it yourself as:
|
|
54
56
|
|
55
57
|
$ gem install activerecord_where_assoc
|
56
58
|
|
59
|
+
## Documentation
|
60
|
+
|
61
|
+
The [documentation is nicely structured](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/QueryMethods.html)
|
62
|
+
|
63
|
+
If you prefer to see it in the code, [everything is in this file](https://github.com/MaxLap/activerecord_where_assoc/blob/master/lib/active_record_where_assoc/query_methods.rb)
|
64
|
+
|
57
65
|
## Usage
|
58
66
|
|
59
67
|
The [documentation is nicely structured](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/QueryMethods.html)
|
@@ -95,7 +103,7 @@ where_assoc_count(left_operand, operator, association_name, conditions, options,
|
|
95
103
|
Post.where_assoc_count(5, :<=, :comments, is_spam: true)
|
96
104
|
```
|
97
105
|
* `where_assoc_count`'s additional arguments
|
98
|
-
The order of the parameters of `#where_assoc_count`
|
106
|
+
The order of the parameters of `#where_assoc_count` may seem confusing, but you will get used to it. It helps to remember: the goal is to do: `5 < (SELECT COUNT(*) FROM ...)`, the number is first, then operator, then the association and its conditions.
|
99
107
|
* left_operand:
|
100
108
|
* a number
|
101
109
|
* a string of SQL to embed in the query
|
@@ -171,7 +179,7 @@ All the methods always chain nested associations using an `EXISTS` when they hav
|
|
171
179
|
|
172
180
|
### Using `#from` in scope
|
173
181
|
|
174
|
-
If you want to use a scope / condition which uses `#from`, then you need to use the [:never_alias_limit](#
|
182
|
+
If you want to use a scope / condition which uses `#from`, then you need to use the [:never_alias_limit](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/QueryMethods.html#module-ActiveRecordWhereAssoc::QueryMethods-label-3Anever_alias_limit+option) option to avoid `#where_assoc_*` being overwritten by your scope and getting a weird exception / wrong result.
|
175
183
|
|
176
184
|
## Known issues/limitations
|
177
185
|
|
@@ -180,7 +188,7 @@ On MySQL databases, it is not possible to use `has_one` associations and associa
|
|
180
188
|
|
181
189
|
I do not know of a way to do a SQL query that can deal with all the specifics of `has_one` for MySQL. If you have one, then please suggest it in an issue/pull request.
|
182
190
|
|
183
|
-
In order to work around this, you must use the [ignore_limit](#
|
191
|
+
In order to work around this, you must use the [ignore_limit](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/QueryMethods.html#module-ActiveRecordWhereAssoc::QueryMethods-label-3Aignore_limit+option) option. The behavior is less correct, but better than being unable to use the gem.
|
184
192
|
|
185
193
|
### has_* :through vs limit/offset
|
186
194
|
For `has_many` and `has_one` with the `:through` option, `#limit` and `#offset` are ignored. Note that `#limit` and `#offset` of the `:source` and of the `:through` side are applied correctly.
|
@@ -29,7 +29,6 @@ require_relative "active_record_where_assoc/querying"
|
|
29
29
|
ActiveSupport.on_load(:active_record) do
|
30
30
|
ActiveRecord.eager_load!
|
31
31
|
|
32
|
-
|
33
|
-
ActiveRecord::Relation.send(:include, ActiveRecordWhereAssoc::QueryMethods)
|
32
|
+
ActiveRecord::Relation.include(ActiveRecordWhereAssoc::QueryMethods)
|
34
33
|
ActiveRecord::Base.extend(ActiveRecordWhereAssoc::Querying)
|
35
34
|
end
|
@@ -284,7 +284,7 @@ module ActiveRecordWhereAssoc
|
|
284
284
|
end
|
285
285
|
msg << "This is not supported by ActiveRecord when doing joins, but it is by WhereAssoc. However, "
|
286
286
|
msg << "you must pass the :poly_belongs_to option to specify what to do in this case.\n"
|
287
|
-
msg << "See https://github.
|
287
|
+
msg << "See https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/QueryMethods.html#module-ActiveRecordWhereAssoc::QueryMethods-label-3Apoly_belongs_to+option"
|
288
288
|
raise ActiveRecordWhereAssoc::PolymorphicBelongsToWithoutClasses, msg
|
289
289
|
else
|
290
290
|
if on_poly_belongs_to.is_a?(Class) && on_poly_belongs_to < ActiveRecord::Base
|
@@ -315,7 +315,7 @@ module ActiveRecordWhereAssoc
|
|
315
315
|
msg = String.new
|
316
316
|
msg << "Associations and default_scopes with a limit or offset are not supported for MySQL (this includes has_many). "
|
317
317
|
msg << "Use ignore_limit: true to ignore both limit and offset, and treat has_one like has_many. "
|
318
|
-
msg << "See https://github.com/MaxLap/activerecord_where_assoc
|
318
|
+
msg << "See https://github.com/MaxLap/activerecord_where_assoc#mysql-doesnt-support-sub-limit for details."
|
319
319
|
raise MySQLDoesntSupportSubLimitError, msg
|
320
320
|
end
|
321
321
|
|
@@ -1,8 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "active_record_compat"
|
4
|
-
require_relative "exceptions"
|
5
|
-
|
6
3
|
# See ActiveRecordWhereAssoc::QueryMethods
|
7
4
|
module ActiveRecordWhereAssoc
|
8
5
|
# This module adds new variations of +#where+ to your Models/relations/associations/scopes.
|
@@ -134,6 +131,8 @@ module ActiveRecordWhereAssoc
|
|
134
131
|
# Post.where_assoc_exists(:comments) { where(author_id: self.foo(:bar)) }
|
135
132
|
# # THESE ARE WRONG!
|
136
133
|
#
|
134
|
+
# If both +condition+ and +block+ are given, the conditions are applied first, and then the block.
|
135
|
+
#
|
137
136
|
# === Options
|
138
137
|
# Some options are available to tweak how queries are generated. The default values of the options
|
139
138
|
# can be changed globally:
|
@@ -150,53 +149,53 @@ module ActiveRecordWhereAssoc
|
|
150
149
|
# Note, if you don't need a condition, you must pass nil as condition to provide options:
|
151
150
|
# Post.where_assoc_exists(:comments, nil, ignore_limit: true)
|
152
151
|
#
|
153
|
-
#
|
154
|
-
#
|
155
|
-
#
|
156
|
-
#
|
157
|
-
#
|
158
|
-
#
|
159
|
-
#
|
160
|
-
#
|
161
|
-
#
|
162
|
-
#
|
163
|
-
#
|
164
|
-
#
|
165
|
-
#
|
166
|
-
#
|
167
|
-
#
|
168
|
-
#
|
169
|
-
#
|
170
|
-
#
|
171
|
-
#
|
172
|
-
#
|
173
|
-
#
|
174
|
-
#
|
175
|
-
#
|
176
|
-
#
|
177
|
-
#
|
178
|
-
#
|
179
|
-
#
|
180
|
-
#
|
181
|
-
#
|
182
|
-
#
|
183
|
-
#
|
184
|
-
#
|
185
|
-
#
|
186
|
-
#
|
187
|
-
#
|
188
|
-
#
|
189
|
-
#
|
190
|
-
#
|
191
|
-
#
|
192
|
-
#
|
193
|
-
#
|
194
|
-
#
|
195
|
-
#
|
196
|
-
#
|
197
|
-
#
|
198
|
-
#
|
199
|
-
#
|
152
|
+
# ===== :ignore_limit option
|
153
|
+
# When true, +#limit+ and +#offset+ that are set from default_scope, on associations, and from
|
154
|
+
# +#has_one+ are ignored. <br>
|
155
|
+
# Removing the limit from +#has_one+ makes them be treated like a +#has_many+.
|
156
|
+
#
|
157
|
+
# Main reasons to use ignore_limit: true
|
158
|
+
# * Needed for MySQL to be able to do anything with +#has_one+ associations because MySQL
|
159
|
+
# doesn't support sub-limit. <br>
|
160
|
+
# See {MySQL doesn't support limit}[https://github.com/MaxLap/activerecord_where_assoc#mysql-doesnt-support-sub-limit] <br>
|
161
|
+
# Note, this does mean the +#has_one+ will be treated as if it was a +#has_many+ for MySQL too.
|
162
|
+
# * You have a +#has_one+ association which you know can never have more than one record and are
|
163
|
+
# dealing with a heavy/slow query. The query used to deal with +#has_many+ is less complex, and
|
164
|
+
# may prove faster.
|
165
|
+
# * For this one special case, you want to check the other records that match your has_one
|
166
|
+
#
|
167
|
+
# ===== :never_alias_limit option
|
168
|
+
# When true, +#where_assoc_*+ will not use +#from+ to build relations that have +#limit+ or +#offset+ set
|
169
|
+
# on default_scope or on associations or for +#has_one+. <br>
|
170
|
+
# This allows changing the from as part of the conditions (such as for a scope)
|
171
|
+
#
|
172
|
+
# Main reasons to use this: you have to use +#from+ in the block of +#where_assoc_*+ method
|
173
|
+
# (ex: because a scope needs +#from+).
|
174
|
+
#
|
175
|
+
# Why this isn't the default:
|
176
|
+
# * From very few tests, the aliasing way seems to produce better plans.
|
177
|
+
# * Using aliasing produces a shorter query.
|
178
|
+
#
|
179
|
+
# ===== :poly_belongs_to option
|
180
|
+
# Specify what to do when a polymorphic belongs_to is encountered. Things are tricky because the query can
|
181
|
+
# end up searching in multiple Models, and just knowing which ones to look into can require an expensive query.
|
182
|
+
# It's also possible that you only want to search for those that match some specific Models, ignoring the other ones.
|
183
|
+
# [:pluck]
|
184
|
+
# Do a +#pluck+ in the column to detect to possible choices. This option can have a performance cost for big tables
|
185
|
+
# or when the query if done often, as the +#pluck+ will be executed each time
|
186
|
+
# [model or array of models]
|
187
|
+
# Specify which models to search for. This avoids the performance cost of +#pluck+ and can allow to filter some
|
188
|
+
# of the choices out that don't interest you. <br>
|
189
|
+
# Note, these are not instances, it's actual models, ex: <code>[Post, Comment]</code>
|
190
|
+
# [a hash]
|
191
|
+
# The keys must be models (same behavior as an array of models). <br>
|
192
|
+
# The values are conditions to apply only for key's model.
|
193
|
+
# The conditions are either a proc (behaves like the block, but only for that model) or the same things +#where+
|
194
|
+
# can receive. (String, Hash, Array, nil). Ex:
|
195
|
+
# List.where_assoc_exists(:items, nil, poly_belongs_to: {Car => "color = 'blue'",
|
196
|
+
# Computer => proc { brand_new.where(core: 4) } })
|
197
|
+
# [:raise]
|
198
|
+
# (default) raise an exception when a polymorphic belongs_to is encountered.
|
200
199
|
module QueryMethods
|
201
200
|
# :section: Basic methods
|
202
201
|
|
@@ -342,7 +341,13 @@ module ActiveRecordWhereAssoc
|
|
342
341
|
# The operator to use, one of these symbols: <code> :< :<= :== :!= :>= :> </code>
|
343
342
|
#
|
344
343
|
# [association_name]
|
345
|
-
# The association that must
|
344
|
+
# The association that must have a certain number of occurrences <br>
|
345
|
+
# Note that if you use an array of association names, the number of the last association
|
346
|
+
# is what is counted.
|
347
|
+
#
|
348
|
+
# # Users which have received at least 5 comments total (can be spread on all of their posts)
|
349
|
+
# User.where_assoc_count(5, :<=, [:posts, :comments])
|
350
|
+
#
|
346
351
|
# See ActiveRecordWhereAssoc::QueryMethods@Association
|
347
352
|
#
|
348
353
|
# [condition]
|
@@ -357,8 +362,8 @@ module ActiveRecordWhereAssoc
|
|
357
362
|
# More complex conditions the associated record must match (can also use scopes of the association's model) <br>
|
358
363
|
# See ActiveRecordWhereAssoc::QueryMethods@Block
|
359
364
|
#
|
360
|
-
# The order of the parameters may seem confusing. But you will get used to it.
|
361
|
-
#
|
365
|
+
# The order of the parameters may seem confusing. But you will get used to it. It helps
|
366
|
+
# to remember that the goal is to do:
|
362
367
|
# 5 < (SELECT COUNT(*) FROM ...)
|
363
368
|
# So the parameters are in the same order as in that query: number, operator, association.
|
364
369
|
#
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord_where_assoc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maxime Handfield Lapointe
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -172,7 +172,6 @@ executables: []
|
|
172
172
|
extensions: []
|
173
173
|
extra_rdoc_files: []
|
174
174
|
files:
|
175
|
-
- ALTERNATIVES_PROBLEMS.md
|
176
175
|
- CHANGELOG.md
|
177
176
|
- EXAMPLES.md
|
178
177
|
- LICENSE.txt
|
@@ -204,8 +203,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
204
203
|
- !ruby/object:Gem::Version
|
205
204
|
version: '0'
|
206
205
|
requirements: []
|
207
|
-
|
208
|
-
rubygems_version: 2.6.11
|
206
|
+
rubygems_version: 3.0.3
|
209
207
|
signing_key:
|
210
208
|
specification_version: 4
|
211
209
|
summary: Make ActiveRecord do conditions on your associations
|
data/ALTERNATIVES_PROBLEMS.md
DELETED
@@ -1,231 +0,0 @@
|
|
1
|
-
There are multiple ways of achieving results similar to what this gems does using either only built-in ActiveRecord functionalities or other gems.
|
2
|
-
|
3
|
-
This is a list of some of those alternatives, explaining what issues they have or reasons to prefer this gem over them.
|
4
|
-
|
5
|
-
|
6
|
-
## Too long; didn't read
|
7
|
-
|
8
|
-
**Use this gem, you will avoid problems and save time**
|
9
|
-
|
10
|
-
* No more having to choose, case by case, which way has the less problems.
|
11
|
-
Just use `#where_assoc_*` each time and avoid every problems.
|
12
|
-
* Need less raw SQL, which means less code, more clarity and less maintenance.
|
13
|
-
* Generates a single `#where`. No weird side-effects things like `#eager_load` or `#join`
|
14
|
-
This makes well-behaved scopes, you can even have multiple conditions on the same association
|
15
|
-
* Handles recursive associations correctly.
|
16
|
-
* Handles has_one correctly (Except [MySQL has a limitation](README.md#mysql-doesnt-support-sub-limit)).
|
17
|
-
* Handles polymorphic belongs_to
|
18
|
-
|
19
|
-
## Short version
|
20
|
-
|
21
|
-
Summary of the problems of the alternatives that `activerecord_where_assoc` solves. The following sections go in more details.
|
22
|
-
|
23
|
-
* every alternatives (except raw SQL):
|
24
|
-
* treat `has_one` like a `has_many`.
|
25
|
-
* can't handle recursive associations. (ex: parent/children)
|
26
|
-
* no simple way of checking for more complex counts. (such as `less than 5`)
|
27
|
-
* `joins` / `includes`:
|
28
|
-
* doing `not exists` with conditions requires a `LEFT JOIN` with the conditions as part of the `ON`, which requires raw SQL.
|
29
|
-
* checking for 2 sets of conditions on different records of the same association won't work. (so your scopes can be incompatible)
|
30
|
-
* can't be used with Rails 5's `or` unless both sides do the same `joins` / `includes` / `eager_load`.
|
31
|
-
* Doesn't work for polymorphic belongs_to.
|
32
|
-
* `joins`:
|
33
|
-
* `has_many` may return duplicate records.
|
34
|
-
* using `uniq` / `distinct` to solve duplicate rows is an unexpected side-effect when this is in a scope.
|
35
|
-
* `includes`:
|
36
|
-
* triggers eagerloading, which makes your `scope` have unexpected bad performances if it's not necessary.
|
37
|
-
* when using a condition, the eagerloaded records are also filtered, which is very bug-prone when in a scope.
|
38
|
-
* raw SQL:
|
39
|
-
* verbose, less clear on the goal of the queries (you don't even name the association the query is about).
|
40
|
-
* need to repeat conditions from the association / default_scope.
|
41
|
-
* `where_exists` gem:
|
42
|
-
* can't use scopes of the association's model.
|
43
|
-
* can't go deeper than one level of association.
|
44
|
-
|
45
|
-
## Common problems to most alternatives
|
46
|
-
|
47
|
-
These are problems that affect most alternatives. Details are written in this section and just referred to by a one liner when they apply to an alternative.
|
48
|
-
|
49
|
-
### Treating has_one like has_many
|
50
|
-
|
51
|
-
Every alternative treats a has_one just like a has_many. So if any of the records (instead of only the first) matches your condition, you will get a match.
|
52
|
-
|
53
|
-
And example to clarify:
|
54
|
-
|
55
|
-
```ruby
|
56
|
-
class Person < ActiveRecord::Base
|
57
|
-
has_many :addresses
|
58
|
-
has_one :current_address, -> { order("effective_date DESC") }, class_name: 'Address'
|
59
|
-
end
|
60
|
-
|
61
|
-
# This correctly matches only those whose current_address is in Montreal
|
62
|
-
Person.where_assoc_exists(:current_address, city: 'Montreal')
|
63
|
-
|
64
|
-
# Every alternatives (except raw SQL):
|
65
|
-
# Matches those that have had an address in Montreal, no matter when
|
66
|
-
Person.where_assoc_exists(:addresses, city: 'Montreal')
|
67
|
-
```
|
68
|
-
|
69
|
-
The general version of this problem is the handling of `limit` and `offset` on associations and in default_scopes. where_assoc_exists handle those correctly and only checks the records that match the limit and the offset.
|
70
|
-
|
71
|
-
Note: [MySQL has a limitation](README.md#mysql-doesnt-support-sub-limit), this makes handling has_one correctly not possible with MySQL.
|
72
|
-
|
73
|
-
### Raw SQL joins or sub-selects
|
74
|
-
|
75
|
-
Having to write the joins and conditions in raw SQL is more painful and more error prone than having a method do it for you. It hides the important details of what you are doing in a lot of verbosity.
|
76
|
-
|
77
|
-
If there are conditions set on either the association or a default_scope of the model, then you must rewrite those conditions in your manual joins and your manual sub-selects. Worst, if you add/change those conditions on the association / default_scope, then you must find every raw SQL that apply and do the same operation.
|
78
|
-
|
79
|
-
```ruby
|
80
|
-
class Post < ActiveRecord::Base
|
81
|
-
# Any raw SQL doing a join or sub-select on public_comments, if it want to be representative,
|
82
|
-
# must repeat "public = true".
|
83
|
-
has_many :public_comments, -> { where(public: true) }, class_name: 'Comment'
|
84
|
-
end
|
85
|
-
|
86
|
-
class Comment < ActiveRecord::Base
|
87
|
-
# Any raw SQL doing a join or sub-select to this model, if it want to be representative,
|
88
|
-
# must repeat "deleted_at IS NULL".
|
89
|
-
default_scope -> { where(deleted_at: nil) }
|
90
|
-
end
|
91
|
-
```
|
92
|
-
|
93
|
-
All of this is avoided by where_assoc_* methods.
|
94
|
-
|
95
|
-
### Unable to handle recursive associations
|
96
|
-
|
97
|
-
When you have recursive associations such as parent/children, you must compare multiple rows of the same table. To do this, you have no choice but to write your own raw SQL to, at the very least, do a SQL join with an alias.
|
98
|
-
|
99
|
-
This brings us back to the [raw SQL joins](#raw-sql-joins-or-sub-selects) problem.
|
100
|
-
|
101
|
-
`where_assoc_*` methods handle this seemlessly.
|
102
|
-
|
103
|
-
### Unable to handle polymorphic belongs_to
|
104
|
-
|
105
|
-
When you have a polymorphic belongs_to, you can't use `joins` or `includes` in order to do queries on it. You have to use manual SQL ([raw SQL joins](#raw-sql-joins-or-sub-selects)) or a gem that provides the feature, such as `activerecord_where_assoc`.
|
106
|
-
|
107
|
-
## ActiveRecord only
|
108
|
-
|
109
|
-
Those are the common ways given in stack overflow answers.
|
110
|
-
|
111
|
-
### Using `joins` and `where`
|
112
|
-
|
113
|
-
```ruby
|
114
|
-
Post.where_assoc_exists(:comments, is_spam: true)
|
115
|
-
Post.joins(:comments).where(comments: {is_spam: true})
|
116
|
-
```
|
117
|
-
|
118
|
-
* If the association maps to multiple records (such as with a has_many), then the the relation will return one record for each matching association record. In this example, you would get the same post twice if it has 2 comments that are marked as spam.
|
119
|
-
Using `uniq` can solve this issue, but if you do that in a scope, then that scope unexpectedly adds a DISTINCT to your query, which can lead to unexpected results if you actually wanted duplicates for a different reason.
|
120
|
-
|
121
|
-
* Doing the opposite is a lot more complicated, as seen below. You have to include your conditions directly in the join and use a LEFT JOIN, this means writing the whole thing in raw SQL, and then you must check for the id of the association to be empty.
|
122
|
-
|
123
|
-
```ruby
|
124
|
-
Post.where_assoc_not_exists(:comments, is_spam: true)
|
125
|
-
Post.joins("LEFT JOIN comments ON posts.id = comments.post_id AND comments.is_spam = true").where(comments: {id: nil})
|
126
|
-
```
|
127
|
-
|
128
|
-
Writing a raw join like that has yet more problems: [raw SQL joins](#raw-sql-joins-or-sub-selects)
|
129
|
-
|
130
|
-
* If you want to have another condition referring to the same association (or just the same table), then you need to write out the SQL for the second join using an alias. Therefore, your scopes are not even compatible unless each of them has a join with a unique alias.
|
131
|
-
|
132
|
-
```ruby
|
133
|
-
# We want to be able to match either different or the same records
|
134
|
-
Post.where_assoc_exists(:comments, is_spam: true)
|
135
|
-
.where_assoc_exists(:comments, is_reported: true)
|
136
|
-
|
137
|
-
# Please don't ever do this, this just shows how painful it would be
|
138
|
-
# If you reach the need to do this but won't use where_assoc_exists,
|
139
|
-
# go for a regular #where("EXISTS( SELECT ...)")
|
140
|
-
Post.joins(:comments).where(comments: {is_spam: true})
|
141
|
-
.joins("JOIN comments comments_for_reported ON posts.id = comments_for_reported.post_id")
|
142
|
-
.where(comments_for_reported: {is_reported: true})
|
143
|
-
```
|
144
|
-
|
145
|
-
* Cannot be used with Rails 5's `or` unless both side do the same `joins`.
|
146
|
-
* [Treats has_one like a has_many](#treating-has_one-like-has_many)
|
147
|
-
* [Can't handle recursive associations](#unable-to-handle-recursive-associations)
|
148
|
-
* [Can't handle polymorphic belongs_to](#unable-to-handle-polymorphic-belongs_to)
|
149
|
-
|
150
|
-
### Using `includes` (or `eager_load`) and `where`
|
151
|
-
|
152
|
-
This solution is similar to the `joins` one above, but avoids the need for `uniq`. Every other problems of the `joins` remain. You also add other potential issues.
|
153
|
-
|
154
|
-
```ruby
|
155
|
-
Post.where_assoc_exists(:comments, is_spam: true)
|
156
|
-
Post.eager_load(:comments).where(comments: {is_spam: true})
|
157
|
-
```
|
158
|
-
|
159
|
-
* You are triggering the loading of potentially lots of records that you might not need. You don't expect a scope like `have_reported_comments` to trigger eager loading. This is a performance degradation.
|
160
|
-
|
161
|
-
* The eager loaded records of the association are actually also filtered by the conditions. All of the posts returned will only have the comments that are spam.
|
162
|
-
This means if you iterate on `Post.have_reported_comments` to display each of the comments of the posts that have at least one reported comment, you are actually only going to display the reported comments. This may be what you wanted to do, but it clearly isn't intuitive.
|
163
|
-
|
164
|
-
* Cannot be used with Rails 5's `or` unless both side do the same `includes` or `eager_load`.
|
165
|
-
|
166
|
-
* [Treats has_one like a has_many](#treating-has_one-like-has_many)
|
167
|
-
* [Can't handle recursive associations](#unable-to-handle-recursive-associations)
|
168
|
-
* [Can't handle polymorphic belongs_to](#unable-to-handle-polymorphic-belongs_to)
|
169
|
-
|
170
|
-
* Simply cannot be used for complex cases.
|
171
|
-
|
172
|
-
Note: using `includes` (or `eager_load`) already does a LEFT JOIN, so it is pretty easy to do a "not exists", but only if you don't need any condition on the association (which would normally need to be in the JOIN clause):
|
173
|
-
|
174
|
-
```ruby
|
175
|
-
Post.where_assoc_exists(:comments)
|
176
|
-
Post.eager_load(:comments).where(comments: {id: nil})
|
177
|
-
```
|
178
|
-
|
179
|
-
### Using `where("EXISTS( SELECT... )")`
|
180
|
-
|
181
|
-
This is what is gem does behind the scene, but doing it manually can lead to troubles:
|
182
|
-
|
183
|
-
* Problems with writing [raw SQL sub-selects](#raw-sql-joins-or-sub-selects)
|
184
|
-
|
185
|
-
* Unless you do a quite complex nested sub-selects, you will [treat has_one like a has_many](#treating-has_one-like-has_many)
|
186
|
-
|
187
|
-
|
188
|
-
## Gems
|
189
|
-
|
190
|
-
### where_exists
|
191
|
-
|
192
|
-
https://github.com/EugZol/where_exists
|
193
|
-
|
194
|
-
An interesting gem that also does `EXISTS (SELECT ... )` behind the scene. Solves most issues from ActiveRecord only alternatives, but appears less powerful than where_assoc_exists.
|
195
|
-
|
196
|
-
* where_exists supports polymorphic belongs_to only by always doing a `pluck` everytime. In some situation could be a slow query if there is a lots of rows to scan. where_assoc also allows directly specifying the classes manually, avoiding the pluck and possibly filtering the choices.
|
197
|
-
|
198
|
-
* Unable to use scopes of the association's model.
|
199
|
-
```ruby
|
200
|
-
# There is no equivalent for this (admins is a scope on User)
|
201
|
-
Comment.where_assoc_exists(:author, &:admins)
|
202
|
-
```
|
203
|
-
|
204
|
-
* Cannot use a block for more complex conditions
|
205
|
-
```ruby
|
206
|
-
# There is no equivalent for this
|
207
|
-
Comment.where_assoc_exists(:author) { admins.where("created_at <= ?", 1.month.ago) }
|
208
|
-
```
|
209
|
-
|
210
|
-
* Unable to dig deeper in the associations
|
211
|
-
Note: it does follow :through associations so doing a custom associations for your need can be a workaround.
|
212
|
-
|
213
|
-
```ruby
|
214
|
-
# There is no equivalent for this (Users that have posts with at least a comments)
|
215
|
-
User.where_assoc_exists([:posts, :comments])
|
216
|
-
```
|
217
|
-
|
218
|
-
* Has no equivalent to `where_assoc_count`
|
219
|
-
```ruby
|
220
|
-
# There is no equivalent for this (posts with more than 5 comments)
|
221
|
-
Post.where_assoc_count(:comments, :>, 5)
|
222
|
-
```
|
223
|
-
|
224
|
-
* [Treats has_one like a has_many](#treating-has_one-like-has_many)
|
225
|
-
|
226
|
-
* [Can't handle recursive associations](#unable-to-handle-recursive-associations)
|
227
|
-
|
228
|
-
* `where_exists` is shorter than `where_assoc_exists`, but it is also less obvious about what it does.
|
229
|
-
In any case, it is trivial to alias one name to the other one.
|
230
|
-
|
231
|
-
* where_exists supports Rails 4.2 and up, while where_assoc supports Rails 4.1 and up.
|