activerecord_where_assoc 0.1.3 → 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/EXAMPLES.md +150 -67
- data/README.md +107 -129
- data/lib/active_record_where_assoc.rb +17 -6
- data/lib/active_record_where_assoc/active_record_compat.rb +42 -10
- data/lib/active_record_where_assoc/core_logic.rb +265 -130
- data/lib/active_record_where_assoc/exceptions.rb +3 -0
- data/lib/active_record_where_assoc/relation_returning_delegates.rb +12 -0
- data/lib/active_record_where_assoc/relation_returning_methods.rb +408 -0
- data/lib/active_record_where_assoc/sql_returning_methods.rb +74 -0
- data/lib/active_record_where_assoc/version.rb +1 -1
- metadata +10 -25
- data/ALTERNATIVES_PROBLEMS.md +0 -221
- data/lib/active_record_where_assoc/query_methods.rb +0 -180
- data/lib/active_record_where_assoc/querying.rb +0 -11
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Needed for delegate
|
4
|
+
require "active_support"
|
5
|
+
|
6
|
+
module ActiveRecordWhereAssoc
|
7
|
+
module RelationReturningDelegates
|
8
|
+
# Delegating the methods in RelationReturningMethods from ActiveRecord::Base to :all. Same thing ActiveRecord does for #where.
|
9
|
+
new_relation_returning_methods = RelationReturningMethods.public_instance_methods
|
10
|
+
delegate(*new_relation_returning_methods, to: :all)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,408 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# See RelationReturningMethods
|
4
|
+
module ActiveRecordWhereAssoc
|
5
|
+
# This module adds new variations of +#where+ to your Models/relations/associations/scopes.
|
6
|
+
# These variations check if an association has records, so you can check if a +Post+ has
|
7
|
+
# any +Comments+.
|
8
|
+
#
|
9
|
+
# These variations return a new relation (just like +#where+) so you can chain them with
|
10
|
+
# other scoping methods such as +#where+, +#order+, +#limit+, more of these variations, etc.
|
11
|
+
#
|
12
|
+
# The arguments common to all methods are documented here at the top.
|
13
|
+
#
|
14
|
+
# For brevity, the examples are all directly on models, such as User, Post, Comment, but
|
15
|
+
# the methods are available and behave the same on:
|
16
|
+
# * associations: <tt>my_user.posts.where_assoc_exists(:comments)</tt>
|
17
|
+
# * relations: <tt>Posts.where(serious: true).where_assoc_exists(:comments)</tt>
|
18
|
+
# * scopes: (On the Post model) <tt>scope :with_comments, -> { where_assoc_exists(:comments) }</tt>
|
19
|
+
# * models: <tt>Post.where_assoc_exists(:comments)</tt>
|
20
|
+
#
|
21
|
+
# In short: Anywhere you could use #where, you can also use the methods presented here. This includes
|
22
|
+
# with ActiveRecord's #or method.
|
23
|
+
#
|
24
|
+
# {Introduction to this gem}[https://github.com/MaxLap/activerecord_where_assoc/blob/master/INTRODUCTION.md]
|
25
|
+
# introduces each features of the gem clearly.
|
26
|
+
#
|
27
|
+
# The {gem's README.md}[https://github.com/MaxLap/activerecord_where_assoc/blob/master/README.md] contains
|
28
|
+
# known limitations and usage tips.
|
29
|
+
#
|
30
|
+
# {Many examples}[https://github.com/MaxLap/activerecord_where_assoc/blob/master/EXAMPLES.md] are available,
|
31
|
+
# including the generated SQL queries.
|
32
|
+
#
|
33
|
+
# If you need extra convincing to try this gem, this document with the problems of the other
|
34
|
+
# ways of doing this kind of filtering should help:
|
35
|
+
# {alternatives' problems}[https://github.com/MaxLap/activerecord_where_assoc/blob/master/ALTERNATIVES_PROBLEMS.md].
|
36
|
+
#
|
37
|
+
# === Association
|
38
|
+
# The associations referred here are the links between your different models. They are your
|
39
|
+
# +#belongs_to+, +#has_many+, +#has_one+, +#has_and_belongs_to_many+.
|
40
|
+
#
|
41
|
+
# This gem is about getting records from your database if their associations match (or don't
|
42
|
+
# match) a certain condition (which by default is just to exist).
|
43
|
+
#
|
44
|
+
# Every method here has an *association_name* parameter. This is the association you want to
|
45
|
+
# check if records exists.
|
46
|
+
#
|
47
|
+
# # Posts with at least one comment
|
48
|
+
# Post.where_assoc_exists(:comments)
|
49
|
+
#
|
50
|
+
# # Posts with no comments
|
51
|
+
# Post.where_assoc_not_exists(:comments)
|
52
|
+
#
|
53
|
+
# If you want, you can pass an array of associations. They will be followed in order, just
|
54
|
+
# like a has_many :through would.
|
55
|
+
#
|
56
|
+
# # Posts which have at least one comment with a reply
|
57
|
+
# # In other words: Posts which have at least one reply reachable through his comments
|
58
|
+
# Post.where_assoc_exists([:comments, :replies])
|
59
|
+
#
|
60
|
+
# === Condition
|
61
|
+
# After the +association_name+ argument, you can pass conditions on your association to
|
62
|
+
# specify which of its records you care about. For example, you could only want Posts that
|
63
|
+
# have a comment marked as spam, so all you care about are comments marked as spam.
|
64
|
+
#
|
65
|
+
# Another way to look at this is that you are filtering your association (using a +#where+)
|
66
|
+
# and checking if a record of that association is still found, and you do this for each of you records.
|
67
|
+
#
|
68
|
+
# This +condition+ argument is passed directly to +#where+, so you can pass in the following:
|
69
|
+
#
|
70
|
+
# # Posts that have at least one comment considered as spam
|
71
|
+
# # Using a Hash
|
72
|
+
# Post.where_assoc_exists(:comments, is_spam: true)
|
73
|
+
#
|
74
|
+
# # Using a String
|
75
|
+
# Post.where_assoc_exists(:comments, "is_spam = true")
|
76
|
+
#
|
77
|
+
# # Using an Array (a string and its binds)
|
78
|
+
# Post.where_assoc_exists(:comments, ["is_spam = ?", true])
|
79
|
+
#
|
80
|
+
# If the condition is blank, it is ignored (just like +#where+ does).
|
81
|
+
#
|
82
|
+
# Note, if you specify multiple associations using an Array, the conditions will only be applied
|
83
|
+
# to the last association.
|
84
|
+
#
|
85
|
+
# # Users which have a post that has a comment marked as spam.
|
86
|
+
# # is_spam is only checked on the comment.
|
87
|
+
# User.where_assoc_exists([:posts, :comments], is_spam: true)
|
88
|
+
#
|
89
|
+
# If you want something else, you will need to use a block (see below) to nest multiple calls.
|
90
|
+
#
|
91
|
+
# # Users which have a post made in the last 5 days which has comments
|
92
|
+
# User.where_assoc_exists(:posts) {
|
93
|
+
# where("created_at > ?", 5.days.ago).where_assoc_exists(:comments)
|
94
|
+
# }
|
95
|
+
#
|
96
|
+
# === Block
|
97
|
+
# The block is used to add more complex conditions. The effect is the same as the condition
|
98
|
+
# parameter. You are specifying which records in the association you care about, but using
|
99
|
+
# a block lets you use any scoping methods, such as +#where+, +#joins+, nested
|
100
|
+
# +#where_assoc_*+, scopes on the model, etc.
|
101
|
+
#
|
102
|
+
# Note that using +#joins+ might lead to unexpected results when using #where_assoc_count,
|
103
|
+
# since if the joins adds rows, it will change the resulting count. It probably makes more
|
104
|
+
# sense to, again, use one of the +where_assoc_*+ methods (they can be nested).
|
105
|
+
#
|
106
|
+
# There are 2 ways of using the block for adding conditions to the association.
|
107
|
+
#
|
108
|
+
# [A block that receives one argument]
|
109
|
+
# The block receives a relation on the target association and return a relation with added
|
110
|
+
# filters or may return nil to do nothing.
|
111
|
+
#
|
112
|
+
# # These are all equivalent. Posts which have a comment marked as spam
|
113
|
+
# # Using a where for the added condition
|
114
|
+
# Post.where_assoc_exists(:comments) { |comments_scope| comments_scope.where(is_spam: true) }
|
115
|
+
#
|
116
|
+
# # Applying a scope of the relation
|
117
|
+
# Post.where_assoc_exists(:comments) { |comments_scope| comments_scope.spam_flagged }
|
118
|
+
#
|
119
|
+
# # Applying a scope of the relation, using the &:shortcut for procs
|
120
|
+
# Post.where_assoc_exists(:comments, &:spam_flagged)
|
121
|
+
#
|
122
|
+
# [A block that receives no argument]
|
123
|
+
# Instead of receiving the relation as argument, the relation is used as the "self" of
|
124
|
+
# the block. Everything else is identical to the block with one argument.
|
125
|
+
#
|
126
|
+
# # These are all equivalent. Posts which have a comment marked as spam
|
127
|
+
# # Using a where for the added condition
|
128
|
+
# Post.where_assoc_exists(:comments) { where(is_spam: true) }
|
129
|
+
#
|
130
|
+
# # Applying a scope of the relation
|
131
|
+
# Post.where_assoc_exists(:comments) { spam_flagged }
|
132
|
+
#
|
133
|
+
# The main reason to use a block with an argument instead of without one is when you need
|
134
|
+
# to call methods on the self outside of the block, such as:
|
135
|
+
#
|
136
|
+
# Post.where_assoc_exists(:comments) { |comments| comments.where(author_id: foo(:bar)) }
|
137
|
+
# Post.where_assoc_exists(:comments) { |comments| comments.where(author_id: self.foo(:bar)) }
|
138
|
+
# # In both cases, using the version without arguments would not work, since the #foo
|
139
|
+
# # would be called on the scope that was given to the block, instead of on the caller
|
140
|
+
# # of the #where_assoc_exists method.
|
141
|
+
#
|
142
|
+
# # THESE ARE WRONG!
|
143
|
+
# Post.where_assoc_exists(:comments) { where(author_id: foo(:bar)) }
|
144
|
+
# Post.where_assoc_exists(:comments) { where(author_id: self.foo(:bar)) }
|
145
|
+
# # THESE ARE WRONG!
|
146
|
+
#
|
147
|
+
# If both +condition+ and +block+ are given, the conditions are applied first, and then the block.
|
148
|
+
#
|
149
|
+
# === Options
|
150
|
+
# Some options are available to tweak how queries are generated. The default values of the options
|
151
|
+
# can be changed globally:
|
152
|
+
#
|
153
|
+
# # Somewhere in your setup code, such as an initializer in Rails
|
154
|
+
# ActiveRecordWhereAssoc.default_options[:ignore_limit] = true
|
155
|
+
#
|
156
|
+
# Or you can pass them as arguments after the +condition+ argument.
|
157
|
+
#
|
158
|
+
# Post.where_assoc_exists(:comments, "is_spam = TRUE", ignore_limit: true)
|
159
|
+
# # Because this is 2 consecutive hashes, must use the +{}+
|
160
|
+
# Post.where_assoc_exists(:comments, {is_spam: true}, ignore_limit: true)
|
161
|
+
#
|
162
|
+
# Note, if you don't need a condition, you must pass nil as condition to provide options:
|
163
|
+
# Post.where_assoc_exists(:comments, nil, ignore_limit: true)
|
164
|
+
#
|
165
|
+
# ===== :ignore_limit option
|
166
|
+
# When true, +#limit+ and +#offset+ that are set from default_scope, on associations, and from
|
167
|
+
# +#has_one+ are ignored. <br>
|
168
|
+
# Removing the limit from +#has_one+ makes them be treated like a +#has_many+.
|
169
|
+
#
|
170
|
+
# Main reasons to use ignore_limit: true
|
171
|
+
# * Needed for MySQL to be able to do anything with +#has_one+ associations because MySQL
|
172
|
+
# doesn't support sub-limit. <br>
|
173
|
+
# See {MySQL doesn't support limit}[https://github.com/MaxLap/activerecord_where_assoc#mysql-doesnt-support-sub-limit] <br>
|
174
|
+
# Note, this does mean the +#has_one+ will be treated as if it was a +#has_many+ for MySQL too.
|
175
|
+
# * You have a +#has_one+ association which you know can never have more than one record and are
|
176
|
+
# dealing with a heavy/slow query. The query used to deal with +#has_many+ is less complex, and
|
177
|
+
# may prove faster.
|
178
|
+
# * For this one special case, you want to check the other records that match your has_one
|
179
|
+
#
|
180
|
+
# ===== :never_alias_limit option
|
181
|
+
# When true, +#where_assoc_*+ will not use +#from+ to build relations that have +#limit+ or +#offset+ set
|
182
|
+
# on default_scope or on associations or for +#has_one+. <br>
|
183
|
+
# This allows changing the from as part of the conditions (such as for a scope)
|
184
|
+
#
|
185
|
+
# Main reasons to use this: you have to use +#from+ in the block of +#where_assoc_*+ method
|
186
|
+
# (ex: because a scope needs +#from+).
|
187
|
+
#
|
188
|
+
# Why this isn't the default:
|
189
|
+
# * From very few tests, the aliasing way seems to produce better plans.
|
190
|
+
# * Using aliasing produces a shorter query.
|
191
|
+
#
|
192
|
+
# ===== :poly_belongs_to option
|
193
|
+
# Specify what to do when a polymorphic belongs_to is encountered. Things are tricky because the query can
|
194
|
+
# end up searching in multiple Models, and just knowing which ones to look into can require an expensive query.
|
195
|
+
# It's also possible that you only want to search for those that match some specific Models, ignoring the other ones.
|
196
|
+
# [:pluck]
|
197
|
+
# Do a +#pluck+ in the column to detect to possible choices. This option can have a performance cost for big tables
|
198
|
+
# or when the query if done often, as the +#pluck+ will be executed each time.
|
199
|
+
# It is important to note that this happens when the query is prepared, so using this with methods that return SQL
|
200
|
+
# (such as SqlReturningMethods#assoc_exists_sql) will still execute a query even if you don't
|
201
|
+
# use the returned string.
|
202
|
+
# [model or array of models]
|
203
|
+
# Specify which models to search for. This avoids the performance cost of +#pluck+ and can allow to filter some
|
204
|
+
# of the choices out that don't interest you. <br>
|
205
|
+
# Note, these are not instances, it's actual models, ex: <code>[Post, Comment]</code>
|
206
|
+
# [a hash]
|
207
|
+
# The keys must be models (same behavior as an array of models). <br>
|
208
|
+
# The values are conditions to apply only for key's model.
|
209
|
+
# The conditions are either a proc (behaves like the block, but only for that model) or the same things +#where+
|
210
|
+
# can receive. (String, Hash, Array, nil). Ex:
|
211
|
+
# List.where_assoc_exists(:items, nil, poly_belongs_to: {Car => "color = 'blue'",
|
212
|
+
# Computer => proc { brand_new.where(core: 4) } })
|
213
|
+
# [:raise]
|
214
|
+
# (default) raise an exception when a polymorphic belongs_to is encountered.
|
215
|
+
module RelationReturningMethods
|
216
|
+
# :section: Basic methods
|
217
|
+
|
218
|
+
# Returns a new relation with a condition added (a +#where+) that checks if an association
|
219
|
+
# of the model exists. Extra conditions the associated model must match can also be specified.
|
220
|
+
#
|
221
|
+
# You could say this is a way of doing a +#select+ that uses associations of your model
|
222
|
+
# on the SQL side, but faster and more concise.
|
223
|
+
#
|
224
|
+
# Examples (with an equivalent ruby +#select+)
|
225
|
+
#
|
226
|
+
# # Posts that have comments
|
227
|
+
# Post.where_assoc_exists(:comments)
|
228
|
+
# Post.all.select { |post| post.comments.exists? }
|
229
|
+
#
|
230
|
+
# # Posts that have comments marked as spam
|
231
|
+
# Post.where_assoc_exists(:comments, is_spam: true)
|
232
|
+
# Post.select { |post| post.comments.any? {|comment| comment.is_spam } }
|
233
|
+
#
|
234
|
+
# # Posts that have comments that have replies
|
235
|
+
# Post.where_assoc_exists([:comments, :replies])
|
236
|
+
# Post.select { |post| post.comments.any? {|comment| comment.replies.exists? } }
|
237
|
+
#
|
238
|
+
# [association_name]
|
239
|
+
# The association that must exist <br>
|
240
|
+
# See RelationReturningMethods@Association
|
241
|
+
#
|
242
|
+
# [condition]
|
243
|
+
# Extra conditions the association must match <br>
|
244
|
+
# See RelationReturningMethods@Condition
|
245
|
+
#
|
246
|
+
# [options]
|
247
|
+
# Options to alter the generated query <br>
|
248
|
+
# See RelationReturningMethods@Options
|
249
|
+
#
|
250
|
+
# [&block]
|
251
|
+
# More complex conditions the associated record must match (can also use scopes of the association's model) <br>
|
252
|
+
# See RelationReturningMethods@Block
|
253
|
+
#
|
254
|
+
# You can get the SQL string of the condition using SqlReturningMethods#assoc_exists_sql.
|
255
|
+
def where_assoc_exists(association_name, conditions = nil, options = {}, &block)
|
256
|
+
sql = ActiveRecordWhereAssoc::CoreLogic.assoc_exists_sql(self.klass, association_name, conditions, options, &block)
|
257
|
+
where(sql)
|
258
|
+
end
|
259
|
+
|
260
|
+
# Returns a new relation with a condition added (a +#where+) that checks if an association
|
261
|
+
# of the model does not exist. Extra conditions the associated model that exists must not match
|
262
|
+
# can also be specified.
|
263
|
+
#
|
264
|
+
# This the exact opposite of what #where_assoc_exists does, so a #where_assoc_not_exists with
|
265
|
+
# the same arguments will keep every records that were rejected by the #where_assoc_exists.
|
266
|
+
#
|
267
|
+
# You could say this is a way of doing a +#reject+ that uses associations of your model
|
268
|
+
# on the SQL side, but faster and more concise.
|
269
|
+
#
|
270
|
+
# Examples (with an equivalent ruby +#reject+)
|
271
|
+
#
|
272
|
+
# # Posts that have no comments
|
273
|
+
# Post.where_assoc_not_exists(:comments)
|
274
|
+
# Post.all.reject { |post| post.comments.exists? }
|
275
|
+
#
|
276
|
+
# # Posts that don't have comments marked as spam (but might have unmarked comments)
|
277
|
+
# Post.where_assoc_not_exists(:comments, is_spam: true)
|
278
|
+
# Post.reject { |post| post.comments.any? {|comment| comment.is_spam } }
|
279
|
+
#
|
280
|
+
# # Posts that don't have comments that have replies (but can have comments that have no replies)
|
281
|
+
# Post.where_assoc_exists([:comments, :replies])
|
282
|
+
# Post.reject { |post| post.comments.any? {|comment| comment.replies.exists? } }
|
283
|
+
#
|
284
|
+
# [association_name]
|
285
|
+
# The association that must exist <br>
|
286
|
+
# See RelationReturningMethods@Association
|
287
|
+
#
|
288
|
+
# [condition]
|
289
|
+
# Extra conditions the association must not match <br>
|
290
|
+
# See RelationReturningMethods@Condition
|
291
|
+
#
|
292
|
+
# [options]
|
293
|
+
# Options to alter the generated query <br>
|
294
|
+
# See RelationReturningMethods@Options
|
295
|
+
#
|
296
|
+
# [&block]
|
297
|
+
# More complex conditions the associated record must match (can also use scopes of the association's model) <br>
|
298
|
+
# See RelationReturningMethods@Block
|
299
|
+
#
|
300
|
+
# You can get the SQL string of the condition using SqlReturningMethods#assoc_not_exists_sql.
|
301
|
+
def where_assoc_not_exists(association_name, conditions = nil, options = {}, &block)
|
302
|
+
sql = ActiveRecordWhereAssoc::CoreLogic.assoc_not_exists_sql(self.klass, association_name, conditions, options, &block)
|
303
|
+
where(sql)
|
304
|
+
end
|
305
|
+
|
306
|
+
# :section: Complex method
|
307
|
+
|
308
|
+
# Returns a new relation with a condition added (a +#where+) that checks how many records an association
|
309
|
+
# of the model has. Extra conditions the associated model must match can also be specified.
|
310
|
+
#
|
311
|
+
# This method is a generalization of #where_assoc_exists and #where_assoc_not_exists. It does the same
|
312
|
+
# thing, but can be more precise over how many records should exist (and match the extra conditions)
|
313
|
+
# To clarify, here are equivalent examples:
|
314
|
+
#
|
315
|
+
# Post.where_assoc_exists(:comments)
|
316
|
+
# Post.where_assoc_count(1, :<=, :comments)
|
317
|
+
#
|
318
|
+
# Post.where_assoc_not_exists(:comments)
|
319
|
+
# Post.where_assoc_count(0, :==, :comments)
|
320
|
+
#
|
321
|
+
# But these have no equivalent:
|
322
|
+
#
|
323
|
+
# # Posts with at least 5 comments
|
324
|
+
# Post.where_assoc_count(5, :<=, :comments)
|
325
|
+
#
|
326
|
+
# # Posts with less than 5 comments
|
327
|
+
# Post.where_assoc_count(5, :>, :comments)
|
328
|
+
#
|
329
|
+
# You could say this is a way of doing a +#select+ that +#count+ the associations of your model
|
330
|
+
# on the SQL side, but faster and more concise.
|
331
|
+
#
|
332
|
+
# Examples (with an equivalent ruby +#select+ and +#count+)
|
333
|
+
#
|
334
|
+
# # Posts with at least 5 comments
|
335
|
+
# Post.where_assoc_count(5, :<=, :comments)
|
336
|
+
# Post.all.select { |post| post.comments.count >= 5 }
|
337
|
+
#
|
338
|
+
# # Posts that have at least 5 comments marked as spam
|
339
|
+
# Post.where_assoc_count(5, :<=, :comments, is_spam: true)
|
340
|
+
# Post.all.select { |post| post.comments.where(is_spam: true).count >= 5 }
|
341
|
+
#
|
342
|
+
# # Posts that have at least 10 replies spread over their comments
|
343
|
+
# Post.where_assoc_count(10, :<=, [:comments, :replies])
|
344
|
+
# Post.select { |post| post.comments.sum { |comment| comment.replies.count } >= 5 }
|
345
|
+
#
|
346
|
+
# [left_operand]
|
347
|
+
# 1st argument, the left side of the comparison. <br>
|
348
|
+
# One of:
|
349
|
+
# * a number
|
350
|
+
# * a string of SQL to embed in the query
|
351
|
+
# * a range (operator must be :== or :!=), will use BETWEEN or NOT BETWEEN<br>
|
352
|
+
# supports infinite ranges and exclusive end
|
353
|
+
#
|
354
|
+
# # Posts with 5 to 10 comments
|
355
|
+
# Post.where_assoc_count(5..10, :==, :comments)
|
356
|
+
#
|
357
|
+
# # Posts with less than 5 or more than 10 comments
|
358
|
+
# Post.where_assoc_count(5..10, :!=, :comments)
|
359
|
+
#
|
360
|
+
# [operator]
|
361
|
+
# The operator to use, one of these symbols: <code> :< :<= :== :!= :>= :> </code>
|
362
|
+
#
|
363
|
+
# [association_name]
|
364
|
+
# The association that must have a certain number of occurrences <br>
|
365
|
+
# Note that if you use an array of association names, the number of the last association
|
366
|
+
# is what is counted.
|
367
|
+
#
|
368
|
+
# # Users which have received at least 5 comments total (can be spread on all of their posts)
|
369
|
+
# User.where_assoc_count(5, :<=, [:posts, :comments])
|
370
|
+
#
|
371
|
+
# See RelationReturningMethods@Association
|
372
|
+
#
|
373
|
+
# [condition]
|
374
|
+
# Extra conditions the association must match to count <br>
|
375
|
+
# See RelationReturningMethods@Condition
|
376
|
+
#
|
377
|
+
# [options]
|
378
|
+
# Options to alter the generated query <br>
|
379
|
+
# See RelationReturningMethods@Options
|
380
|
+
#
|
381
|
+
# [&block]
|
382
|
+
# More complex conditions the associated record must match (can also use scopes of the association's model) <br>
|
383
|
+
# See RelationReturningMethods@Block
|
384
|
+
#
|
385
|
+
# The order of the parameters may seem confusing. But you will get used to it. It helps
|
386
|
+
# to remember that the goal is to do:
|
387
|
+
# 5 < (SELECT COUNT(*) FROM ...)
|
388
|
+
# So the parameters are in the same order as in that query: number, operator, association.
|
389
|
+
#
|
390
|
+
# To be clear, when you use multiple associations in an array, the count you will be
|
391
|
+
# comparing against is the total number of records of that last association.
|
392
|
+
#
|
393
|
+
# # The users that have received at least 5 comments total on all of their posts
|
394
|
+
# # So this can be from one post that has 5 comments of from 5 posts with 1 comments
|
395
|
+
# User.where_assoc_count(5, :<=, [:posts, :comments])
|
396
|
+
#
|
397
|
+
# # The users that have at least 5 posts with at least one comments
|
398
|
+
# User.where_assoc_count(5, :<=, :posts) { where_assoc_exists(:comments) }
|
399
|
+
#
|
400
|
+
# You can get the SQL string of the condition using SqlReturningMethods#compare_assoc_count_sql.
|
401
|
+
# You can get the SQL string for only the counting using SqlReturningMethods#only_assoc_count_sql.
|
402
|
+
def where_assoc_count(left_operand, operator, association_name, conditions = nil, options = {}, &block)
|
403
|
+
sql = ActiveRecordWhereAssoc::CoreLogic.compare_assoc_count_sql(self.klass, left_operand, operator,
|
404
|
+
association_name, conditions, options, &block)
|
405
|
+
where(sql)
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordWhereAssoc
|
4
|
+
# The methods in this module return partial SQL queries. These are used by the main methods of
|
5
|
+
# this gem: the #where_assoc_* methods located in RelationReturningMethods. But in some situation, the SQL strings can be useful to
|
6
|
+
# do complex manual queries by embedding them in your own SQL code.
|
7
|
+
#
|
8
|
+
# Those methods should be used directly on your model's class. You can use them from a relation, but the result will be
|
9
|
+
# the same, so your intent will be clearer by doing it on the class directly.
|
10
|
+
#
|
11
|
+
# # This is the recommended way:
|
12
|
+
# sql = User.assoc_exists_sql(:posts)
|
13
|
+
#
|
14
|
+
# # While this also works, it may be confusing when reading the code:
|
15
|
+
# sql = my_filtered_users.assoc_exists_sql(:posts)
|
16
|
+
# # the sql variable is not affected by my_filtered_users.
|
17
|
+
module SqlReturningMethods
|
18
|
+
# This method returns a string containing the SQL condition used by RelationReturningMethods#where_assoc_exists.
|
19
|
+
# You can pass that SQL string directly to #where to get the same result as RelationReturningMethods#where_assoc_exists.
|
20
|
+
# This can be useful to get the SQL of an EXISTS query for use in your own SQL code.
|
21
|
+
#
|
22
|
+
# For example:
|
23
|
+
# # Users with a post or a comment
|
24
|
+
# User.where("#{User.assoc_exists_sql(:posts)} OR #{User.assoc_exists_sql(:comments)}")
|
25
|
+
# my_users.where("#{User.assoc_exists_sql(:posts)} OR #{User.assoc_exists_sql(:comments)}")
|
26
|
+
#
|
27
|
+
# The parameters are the same as RelationReturningMethods#where_assoc_exists, including the
|
28
|
+
# possibility of specifying a list of association_name.
|
29
|
+
def assoc_exists_sql(association_name, conditions = nil, options = {}, &block)
|
30
|
+
ActiveRecordWhereAssoc::CoreLogic.assoc_exists_sql(self, association_name, conditions, options, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
# This method generates the SQL query used by RelationReturningMethods#where_assoc_not_exists.
|
34
|
+
# This method is the same as #assoc_exists_sql, but for RelationReturningMethods#where_assoc_not_exists.
|
35
|
+
#
|
36
|
+
# The parameters are the same as RelationReturningMethods#where_assoc_not_exists, including the
|
37
|
+
# possibility of specifying a list of association_name.
|
38
|
+
def assoc_not_exists_sql(association_name, conditions = nil, options = {}, &block)
|
39
|
+
ActiveRecordWhereAssoc::CoreLogic.assoc_not_exists_sql(self, association_name, conditions, options, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
# This method returns a string containing the SQL condition used by RelationReturningMethods#where_assoc_count.
|
43
|
+
# You can pass that SQL string directly to #where to get the same result as RelationReturningMethods#where_assoc_count.
|
44
|
+
# This can be useful to get the SQL query to compare the count of an association for use in your own SQL code.
|
45
|
+
#
|
46
|
+
# For example:
|
47
|
+
# # Users with at least 10 posts or at least 10 comment
|
48
|
+
# User.where("#{User.compare_assoc_count_sql(10, :<=, :posts)} OR #{User.compare_assoc_count_sql(10, :<=, :comments)}")
|
49
|
+
# my_users.where("#{User.compare_assoc_count_sql(10, :<=, :posts)} OR #{User.compare_assoc_count_sql(10, :<=, :comments)}")
|
50
|
+
#
|
51
|
+
# The parameters are the same as RelationReturningMethods#where_assoc_count, including the
|
52
|
+
# possibility of specifying a list of association_name.
|
53
|
+
def compare_assoc_count_sql(left_operand, operator, association_name, conditions = nil, options = {}, &block)
|
54
|
+
ActiveRecordWhereAssoc::CoreLogic.compare_assoc_count_sql(self, left_operand, operator, association_name, conditions, options, &block)
|
55
|
+
end
|
56
|
+
|
57
|
+
# This method returns a string containing the SQL to count an association used by RelationReturningMethods#where_assoc_count.
|
58
|
+
# The returned SQL does not do a comparison, only the counting part. So you can do the comparison yourself.
|
59
|
+
# This can be useful to get the SQL to count the an association query for use in your own SQL code.
|
60
|
+
#
|
61
|
+
# For example:
|
62
|
+
# # Users with more posts than comments
|
63
|
+
# User.where("#{User.only_assoc_count_sql(:posts)} > #{User.only_assoc_count_sql(:comments)}")
|
64
|
+
# my_users.where("#{User.only_assoc_count_sql(:posts)} > #{User.only_assoc_count_sql(:comments)}")
|
65
|
+
#
|
66
|
+
# Since the comparison is not made by this method, the first 2 parameters (left_operand and operator)
|
67
|
+
# of RelationReturningMethods#where_assoc_count are not accepted by this method. The remaining
|
68
|
+
# parameters of RelationReturningMethods#where_assoc_count are accepted, which are the same
|
69
|
+
# the same as those of RelationReturningMethods#where_assoc_exists.
|
70
|
+
def only_assoc_count_sql(association_name, conditions = nil, options = {}, &block)
|
71
|
+
ActiveRecordWhereAssoc::CoreLogic.only_assoc_count_sql(self, association_name, conditions, options, &block)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|