activerecord_where_assoc 0.1.3 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|