activerecord_where_assoc 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/ALTERNATIVES_PROBLEMS.md +222 -0
- data/EXAMPLES.md +222 -0
- data/README.md +17 -43
- data/lib/active_record_where_assoc.rb +4 -4
- data/lib/active_record_where_assoc/query_methods.rb +5 -5
- data/lib/active_record_where_assoc/version.rb +1 -1
- data/lib/activerecord_where_assoc.rb +0 -2
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e248bec95c932b0bd11743e33d38915ebc13c398
|
4
|
+
data.tar.gz: bea0b06ff2f9e3f61a92e4e2f0cab78daaf63b9b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9551db7bfb1f1219dd1c23fe9255e4ffd495a4bf6777c53619d7037b432e1b13ecafeabee6be8083d8556bca49cf6473f3fbd4ff601723a1380f953a9dee764b
|
7
|
+
data.tar.gz: 5b1d53119ce95d0ab40665330f63c58512818f4af16352e5eb6a7245853ed15c40c2391152aa08bb894703ef853b775c94d3302c5641ae4372d75dfbda2e91fa
|
@@ -0,0 +1,222 @@
|
|
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
|
+
* Allows powerful scopes without traps.
|
14
|
+
* Handles recursive associations correctly.
|
15
|
+
* Handles has_one correctly.
|
16
|
+
|
17
|
+
## Short version
|
18
|
+
|
19
|
+
Summary of the problems of the alternatives that `activerecord_where_assoc` solves. The following sections go in more details.
|
20
|
+
|
21
|
+
* every alternatives (except raw SQL):
|
22
|
+
* treat `has_one` like a `has_many`.
|
23
|
+
* can't handle recursive associations. (ex: parent/children)
|
24
|
+
* no simple way of checking for more complex counts. (such as less than 5)
|
25
|
+
* `joins` / `includes`:
|
26
|
+
* doing `not exists` with conditions requires a `LEFT JOIN` with the conditions as part of the `ON`, which requires raw SQL.
|
27
|
+
* checking for 2 sets of conditions on different records of the same association won't work. (so your scopes becomes incompatible)
|
28
|
+
* can't be used with Rails 5's `or` unless both sides do the same `joins` / `includes` / `eager_load`.
|
29
|
+
* `joins`:
|
30
|
+
* `has_many` may return duplicate rows per record.
|
31
|
+
* using `uniq` / `distinct` to solve duplicate rows is an unexpected side-effect when this is in a scope.
|
32
|
+
* `includes`:
|
33
|
+
* triggers eagerloading, which makes your `scope` have unexpected bad performances if it's not necessary.
|
34
|
+
* when using a condition, the eagerloaded records are also filtered, which is very bug-prone when in a scope.
|
35
|
+
* can't do `not exists` with conditions.
|
36
|
+
* raw SQL:
|
37
|
+
* verbose, less clear on the goal of the queries (you don't even name the association the query is about).
|
38
|
+
* need to repeat conditions from the association / default_scope.
|
39
|
+
* `where_exists` gem:
|
40
|
+
* can't use scopes of the association's model.
|
41
|
+
* can't go deeper than one level of association.
|
42
|
+
|
43
|
+
## Common problems to most alternatives
|
44
|
+
|
45
|
+
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.
|
46
|
+
|
47
|
+
### Treating has_one like has_many
|
48
|
+
|
49
|
+
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.
|
50
|
+
|
51
|
+
And example to clarify:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
class Person < ActiveRecord::Base
|
55
|
+
has_many :addresses
|
56
|
+
has_one :current_address, -> { order("effective_date DESC") }, class_name: 'Address'
|
57
|
+
end
|
58
|
+
|
59
|
+
# This correctly matches only those whose current_address is in Montreal
|
60
|
+
Person.where_assoc_exists(:current_address, city: 'Montreal')
|
61
|
+
|
62
|
+
# Every alternatives (except raw SQL):
|
63
|
+
# Matches those that have had an address in Montreal, no matter when
|
64
|
+
Person.where_assoc_exists(:addresses, city: 'Montreal')
|
65
|
+
```
|
66
|
+
|
67
|
+
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.
|
68
|
+
|
69
|
+
### Raw SQL joins or sub-selects
|
70
|
+
|
71
|
+
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.
|
72
|
+
|
73
|
+
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.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
class Post < ActiveRecord::Base
|
77
|
+
# Any raw SQL doing a join or sub-select on public_comments, if it want to be representative,
|
78
|
+
# must repeat "public = true".
|
79
|
+
has_many :public_comments, -> { where(public: true) }, class_name: 'Comment'
|
80
|
+
end
|
81
|
+
|
82
|
+
class Comment < ActiveRecord::Base
|
83
|
+
# Any raw SQL doing a join or sub-select to this model, if it want to be representative,
|
84
|
+
# must repeat "deleted_at IS NULL".
|
85
|
+
default_scope -> { where(deleted_at: nil) }
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
All of this is avoided by where_assoc_* methods.
|
90
|
+
|
91
|
+
### Unable to handle recursive associations
|
92
|
+
|
93
|
+
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.
|
94
|
+
|
95
|
+
This brings us back to the [raw SQL joins](#raw-sql-joins-or-sub-selects) problem.
|
96
|
+
|
97
|
+
`where_assoc_*` methods handle this seemlessly.
|
98
|
+
|
99
|
+
## ActiveRecord only
|
100
|
+
|
101
|
+
Those are the common ways given in stack overflow answers.
|
102
|
+
|
103
|
+
### Using `joins` and `where`
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
Post.where_assoc_exists(:comments, is_spam: true)
|
107
|
+
Post.joins(:comments).where(comments: {is_spam: true})
|
108
|
+
```
|
109
|
+
|
110
|
+
* 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.
|
111
|
+
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.
|
112
|
+
|
113
|
+
* 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.
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
Post.where_assoc_not_exists(:comments, is_spam: true)
|
117
|
+
Post.joins("LEFT JOIN comments ON posts.id = comments.post_id AND comments.is_spam = true").where(comments: {id: nil})
|
118
|
+
```
|
119
|
+
|
120
|
+
Writing a raw join like that has yet more problems: [raw SQL joins](#raw-sql-joins-or-sub-selects)
|
121
|
+
|
122
|
+
* 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.
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
# We want to be able to match either different or the same records
|
126
|
+
Post.where_assoc_exists(:comments, is_spam: true)
|
127
|
+
.where_assoc_exists(:comments, is_reported: true)
|
128
|
+
|
129
|
+
# Please don't ever do this, this just shows how painful it would be
|
130
|
+
# If you reach the need to do this but won't use where_assoc_exists,
|
131
|
+
# go for a regular #where("EXISTS( SELECT ...)")
|
132
|
+
Post.joins(:comments).where(comments: {is_spam: true})
|
133
|
+
.joins("JOIN comments comments_for_reported ON posts.id = comments_for_reported.post_id")
|
134
|
+
.where(comments_for_reported: {is_reported: true})
|
135
|
+
```
|
136
|
+
|
137
|
+
* Cannot be used with Rails 5's `or` unless both side do the same `joins`.
|
138
|
+
* [Treats has_one like a has_many](#treating-has_one-like-has_many)
|
139
|
+
* [Can't handle recursive associations](#unable-to-handle-recursive-associations)
|
140
|
+
|
141
|
+
### Using `includes` (or `eager_load`) and `where`
|
142
|
+
|
143
|
+
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.
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
Post.where_assoc_exists(:comments, is_spam: true)
|
147
|
+
Post.eager_load(:comments).where(comments: {is_spam: true})
|
148
|
+
```
|
149
|
+
|
150
|
+
* 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.
|
151
|
+
|
152
|
+
* 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.
|
153
|
+
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.
|
154
|
+
|
155
|
+
* Cannot be used with Rails 5's `or` unless both side do the same `includes` or `eager_load`.
|
156
|
+
|
157
|
+
* [Treats has_one like a has_many](#treating-has_one-like-has_many)
|
158
|
+
* [Can't handle recursive associations](#unable-to-handle-recursive-associations)
|
159
|
+
|
160
|
+
* Simply cannot be used for complex cases.
|
161
|
+
|
162
|
+
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):
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
Post.where_assoc_exists(:comments)
|
166
|
+
Post.eager_load(:comments).where(comments: {id: nil})
|
167
|
+
```
|
168
|
+
|
169
|
+
### Using `where("EXISTS( SELECT... )")`
|
170
|
+
|
171
|
+
This is what is gem does behind the scene, but doing it manually can lead to troubles:
|
172
|
+
|
173
|
+
* Problems with writing [raw SQL sub-selects](#raw-sql-joins-or-sub-selects)
|
174
|
+
|
175
|
+
* Unless you do a quite complex nested sub-selects, you will [treat has_one like a has_many](#treating-has_one-like-has_many)
|
176
|
+
|
177
|
+
|
178
|
+
## Gems
|
179
|
+
|
180
|
+
### where_exists
|
181
|
+
|
182
|
+
https://github.com/EugZol/where_exists
|
183
|
+
|
184
|
+
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.
|
185
|
+
|
186
|
+
* where_exists supports polymorphic belongs_to. This is something that where_assoc doesn't do at the moment.
|
187
|
+
However, the way it does this is by doing a pluck on the type column, which in some situation could be a slow query if there is a lots of rows to scan.
|
188
|
+
|
189
|
+
* Unable to use scopes of the association's model.
|
190
|
+
```ruby
|
191
|
+
# There is no equivalent for this (admins is a scope on User)
|
192
|
+
Comment.where_assoc_exists(:author, &:admins)
|
193
|
+
```
|
194
|
+
|
195
|
+
* Cannot use a block for more complex conditions
|
196
|
+
```ruby
|
197
|
+
# There is no equivalent for this
|
198
|
+
Comment.where_assoc_exists(:author) { admins.where("created_at <= ?", 1.month.ago) }
|
199
|
+
```
|
200
|
+
|
201
|
+
* Unable to dig deeper in the associations
|
202
|
+
Note: it does follow :through associations so doing a custom associations for your need can be a workaround.
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
# There is no equivalent for this (Users that have posts with at least a comments)
|
206
|
+
User.where_assoc_exists([:posts, :comments])
|
207
|
+
```
|
208
|
+
|
209
|
+
* Has no equivalent to `where_assoc_count`
|
210
|
+
```ruby
|
211
|
+
# There is no equivalent for this (posts with more than 5 comments)
|
212
|
+
Post.where_assoc_count(:comments, :>, 5)
|
213
|
+
```
|
214
|
+
|
215
|
+
* [Treats has_one like a has_many](#treating-has_one-like-has_many)
|
216
|
+
|
217
|
+
* [Can't handle recursive associations](#unable-to-handle-recursive-associations)
|
218
|
+
|
219
|
+
* `where_exists` is shorter than `where_assoc_exists`, but it is also less obvious about what it does.
|
220
|
+
In any case, it is trivial to alias one name to the other one.
|
221
|
+
|
222
|
+
* where_exists supports Rails 4.2 and up, while where_assoc supports Rails 4.1 and up.
|
data/EXAMPLES.md
ADDED
@@ -0,0 +1,222 @@
|
|
1
|
+
Here are some example usages of the gem, along with the generated SQL. Each of those can be chained with scoping methods.
|
2
|
+
|
3
|
+
Models can be found in [examples/models.md](examples/models.md). Explanation is provided in that file to be able to run these queries.
|
4
|
+
|
5
|
+
The content below is generated from running `ruby examples/examples.rb`
|
6
|
+
|
7
|
+
-------
|
8
|
+
|
9
|
+
## Simple examples
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
# Posts that have a least one comment
|
13
|
+
Post.where_assoc_exists(:comments)
|
14
|
+
```
|
15
|
+
```sql
|
16
|
+
SELECT "posts".* FROM "posts"
|
17
|
+
WHERE (EXISTS (
|
18
|
+
SELECT 0 FROM "comments"
|
19
|
+
WHERE "comments"."post_id" = "posts"."id"
|
20
|
+
))
|
21
|
+
```
|
22
|
+
|
23
|
+
---
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# Posts that have no comments
|
27
|
+
Post.where_assoc_not_exists(:comments)
|
28
|
+
```
|
29
|
+
```sql
|
30
|
+
SELECT "posts".* FROM "posts"
|
31
|
+
WHERE (NOT EXISTS (
|
32
|
+
SELECT 0 FROM "comments"
|
33
|
+
WHERE "comments"."post_id" = "posts"."id"
|
34
|
+
))
|
35
|
+
```
|
36
|
+
|
37
|
+
---
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
# Posts that have a least 50 comment
|
41
|
+
Post.where_assoc_count(50, :<=, :comments)
|
42
|
+
```
|
43
|
+
```sql
|
44
|
+
SELECT "posts".* FROM "posts"
|
45
|
+
WHERE ((50) <= COALESCE((
|
46
|
+
SELECT COUNT(*) FROM "comments"
|
47
|
+
WHERE "comments"."post_id" = "posts"."id"
|
48
|
+
), 0))
|
49
|
+
```
|
50
|
+
|
51
|
+
---
|
52
|
+
|
53
|
+
## Examples with condition / scope
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
# comments of `my_post` that were made by an admin (Using a hash)
|
57
|
+
my_post.comments.where_assoc_exists(:author, is_admin: true)
|
58
|
+
```
|
59
|
+
```sql
|
60
|
+
SELECT "comments".* FROM "comments"
|
61
|
+
WHERE "comments"."post_id" = 1 AND (EXISTS (
|
62
|
+
SELECT 0 FROM "users"
|
63
|
+
WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
|
64
|
+
))
|
65
|
+
```
|
66
|
+
|
67
|
+
---
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
# comments of `my_post` that were not made by an admin (Using scope)
|
71
|
+
my_post.comments.where_assoc_not_exists(:author, &:admins)
|
72
|
+
```
|
73
|
+
```sql
|
74
|
+
SELECT "comments".* FROM "comments"
|
75
|
+
WHERE "comments"."post_id" = 1 AND (NOT EXISTS (
|
76
|
+
SELECT 0 FROM "users"
|
77
|
+
WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
|
78
|
+
))
|
79
|
+
```
|
80
|
+
|
81
|
+
---
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
# Posts that have at least 5 reported comments (Using array condition)
|
85
|
+
Post.where_assoc_count(5, :<=, :comments, ["is_reported = ?", true])
|
86
|
+
```
|
87
|
+
```sql
|
88
|
+
SELECT "posts".* FROM "posts"
|
89
|
+
WHERE ((5) <= COALESCE((
|
90
|
+
SELECT COUNT(*) FROM "comments"
|
91
|
+
WHERE "comments"."post_id" = "posts"."id" AND (is_reported = 't')
|
92
|
+
), 0))
|
93
|
+
```
|
94
|
+
|
95
|
+
---
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
# Posts made by an admin (Using a string)
|
99
|
+
Post.where_assoc_exists(:author, "is_admin = 't'")
|
100
|
+
```
|
101
|
+
```sql
|
102
|
+
SELECT "posts".* FROM "posts"
|
103
|
+
WHERE (EXISTS (
|
104
|
+
SELECT 0 FROM "users"
|
105
|
+
WHERE "users"."id" = "posts"."author_id" AND (is_admin = 't')
|
106
|
+
))
|
107
|
+
```
|
108
|
+
|
109
|
+
---
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
# comments of `my_post` that were not made by an admin (Using block and a scope)
|
113
|
+
my_post.comments.where_assoc_not_exists(:author) { admins }
|
114
|
+
```
|
115
|
+
```sql
|
116
|
+
SELECT "comments".* FROM "comments"
|
117
|
+
WHERE "comments"."post_id" = 1 AND (NOT EXISTS (
|
118
|
+
SELECT 0 FROM "users"
|
119
|
+
WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
|
120
|
+
))
|
121
|
+
```
|
122
|
+
|
123
|
+
---
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
# Posts that have at least 5 reported comments (Using block with #where)
|
127
|
+
Post.where_assoc_count(5, :<=, :comments) { where(is_reported: true) }
|
128
|
+
```
|
129
|
+
```sql
|
130
|
+
SELECT "posts".* FROM "posts"
|
131
|
+
WHERE ((5) <= COALESCE((
|
132
|
+
SELECT COUNT(*) FROM "comments"
|
133
|
+
WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 't'
|
134
|
+
), 0))
|
135
|
+
```
|
136
|
+
|
137
|
+
---
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
# comments made in replies to my_user's post
|
141
|
+
Comment.where_assoc_exists(:post, author_id: my_user.id)
|
142
|
+
```
|
143
|
+
```sql
|
144
|
+
SELECT "comments".* FROM "comments"
|
145
|
+
WHERE (EXISTS (
|
146
|
+
SELECT 0 FROM "posts"
|
147
|
+
WHERE "posts"."id" = "comments"."post_id" AND "posts"."author_id" = 1
|
148
|
+
))
|
149
|
+
```
|
150
|
+
|
151
|
+
---
|
152
|
+
|
153
|
+
## Complex / powerful examples
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
# posts with a comment by an admin (uses array to go through multiple associations)
|
157
|
+
Post.where_assoc_exists([:comments, :author], is_admin: true)
|
158
|
+
```
|
159
|
+
```sql
|
160
|
+
SELECT "posts".* FROM "posts"
|
161
|
+
WHERE (EXISTS (
|
162
|
+
SELECT 0 FROM "comments"
|
163
|
+
WHERE "comments"."post_id" = "posts"."id" AND (EXISTS (
|
164
|
+
SELECT 0 FROM "users"
|
165
|
+
WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
|
166
|
+
))
|
167
|
+
))
|
168
|
+
```
|
169
|
+
|
170
|
+
---
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
# posts where the author also commented on the post (use conditions between posts)
|
174
|
+
Post.where_assoc_exists(:comments, "posts.author_id = comments.author_id")
|
175
|
+
```
|
176
|
+
```sql
|
177
|
+
SELECT "posts".* FROM "posts"
|
178
|
+
WHERE (EXISTS (
|
179
|
+
SELECT 0 FROM "comments"
|
180
|
+
WHERE "comments"."post_id" = "posts"."id" AND (posts.author_id = comments.author_id)
|
181
|
+
))
|
182
|
+
```
|
183
|
+
|
184
|
+
---
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
# posts with a reported comment made by an admin (must be the same comments)
|
188
|
+
Post.where_assoc_exists(:comments, is_reported: true) {
|
189
|
+
where_assoc_exists(:author, is_admin: true)
|
190
|
+
}
|
191
|
+
```
|
192
|
+
```sql
|
193
|
+
SELECT "posts".* FROM "posts"
|
194
|
+
WHERE (EXISTS (
|
195
|
+
SELECT 0 FROM "comments"
|
196
|
+
WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 't' AND (EXISTS (
|
197
|
+
SELECT 0 FROM "users"
|
198
|
+
WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
|
199
|
+
))
|
200
|
+
))
|
201
|
+
```
|
202
|
+
|
203
|
+
---
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
# posts with a reported comment and a comment by an admin (can be different or same comments)
|
207
|
+
my_user.posts.where_assoc_exists(:comments, is_reported: true)
|
208
|
+
.where_assoc_exists([:comments, :author], is_admin: true)
|
209
|
+
```
|
210
|
+
```sql
|
211
|
+
SELECT "posts".* FROM "posts"
|
212
|
+
WHERE "posts"."author_id" = 1 AND (EXISTS (
|
213
|
+
SELECT 0 FROM "comments"
|
214
|
+
WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 't'
|
215
|
+
)) AND (EXISTS (
|
216
|
+
SELECT 0 FROM "comments"
|
217
|
+
WHERE "comments"."post_id" = "posts"."id" AND (EXISTS (
|
218
|
+
SELECT 0 FROM "users"
|
219
|
+
WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
|
220
|
+
))
|
221
|
+
))
|
222
|
+
```
|
data/README.md
CHANGED
@@ -15,14 +15,16 @@ my_post.comments.where_assoc_not_exists(:author, is_admin: true).where(...)
|
|
15
15
|
Post.where_assoc_exists([:comments, :author], &:admins).where(...)
|
16
16
|
|
17
17
|
# Find my_user's posts that have at least 5 non-spam comments
|
18
|
-
my_user.posts.where_assoc_count(5, :>=, :comments) { |comments| comments.where(
|
18
|
+
my_user.posts.where_assoc_count(5, :>=, :comments) { |comments| comments.where(is_spam: false) }.where(...)
|
19
19
|
```
|
20
20
|
|
21
21
|
These allow for powerful, chainable, clear and easy to reuse queries. (Great for scopes)
|
22
22
|
|
23
23
|
You also avoid many [problems with the alternative options](ALTERNATIVES_PROBLEMS.md).
|
24
24
|
|
25
|
-
Works with SQLite3, PostgreSQL and MySQL. [MySQL has one limitation](#mysql-doesnt-support-sub-limit). Untested with other
|
25
|
+
Works with SQLite3, PostgreSQL and MySQL. [MySQL has one limitation](#mysql-doesnt-support-sub-limit). Untested with other RDBMS.
|
26
|
+
|
27
|
+
Here are [many examples](EXAMPLES.md), including the generated SQL queries.
|
26
28
|
|
27
29
|
## Feedback
|
28
30
|
|
@@ -60,13 +62,13 @@ Or install it yourself as:
|
|
60
62
|
Returns a new relation, which is the result of filtering the current relation based on if a record for the specified association of the model exists (or not). Conditions that the associated model must match to count as existing can also be specified.
|
61
63
|
|
62
64
|
```ruby
|
63
|
-
Post.where_assoc_exists(:comments,
|
64
|
-
Post.where_assoc_not_exists(:comments,
|
65
|
+
Post.where_assoc_exists(:comments, is_spam: true)
|
66
|
+
Post.where_assoc_not_exists(:comments, is_spam: true)
|
65
67
|
```
|
66
68
|
|
67
69
|
* 1st parameter: the association we are doing the condition on.
|
68
70
|
* 2nd parameter: (optional) the condition to apply on the association. It can be anything that `#where` can receive, so: Hash, String and Array (string with binds).
|
69
|
-
* 3rd parameter: [options (listed below)](#options) to alter some behaviors.
|
71
|
+
* 3rd parameter: [options (listed below)](#options) to alter some behaviors. (rarely necessary)
|
70
72
|
* block: adds more complex conditions by receiving a relation on the association. Can apply `#where`, `#where_assoc_*`, scopes, and other scoping methods.
|
71
73
|
The block either:
|
72
74
|
|
@@ -81,11 +83,11 @@ Post.where_assoc_not_exists(:comments, spam: true)
|
|
81
83
|
This is a generalization of `#where_assoc_exists` and `#where_assoc_not_exists`. It behave behaves the same way as them, but is more flexible as it allows you to be specific about how many matches there should be. To clarify, here are equivalent examples:
|
82
84
|
|
83
85
|
```ruby
|
84
|
-
Post.where_assoc_exists(:comments,
|
85
|
-
Post.where_assoc_count(1, :<=, :comments,
|
86
|
+
Post.where_assoc_exists(:comments, is_spam: true)
|
87
|
+
Post.where_assoc_count(1, :<=, :comments, is_spam: true)
|
86
88
|
|
87
|
-
Post.where_assoc_not_exists(:comments,
|
88
|
-
Post.where_assoc_count(0, :==, :comments,
|
89
|
+
Post.where_assoc_not_exists(:comments, is_spam: true)
|
90
|
+
Post.where_assoc_count(0, :==, :comments, is_spam: true)
|
89
91
|
```
|
90
92
|
|
91
93
|
* 1st parameter: a number or any string of SQL to embed in the query used for the leftoperand of the comparison.
|
@@ -99,6 +101,10 @@ The order of the parameters may seem confusing, but you will get used to it. To
|
|
99
101
|
|
100
102
|
The parameters are in the same order as in that query: number, operator, association.
|
101
103
|
|
104
|
+
### More examples
|
105
|
+
|
106
|
+
You can view [more usage examples](EXAMPLES.md).
|
107
|
+
|
102
108
|
### Options
|
103
109
|
|
104
110
|
Each of the methods above can take an options argument. It is also possible to change the default value for the options.
|
@@ -153,38 +159,6 @@ These methods have many advantages over the alternative ways of achieving the si
|
|
153
159
|
* Handles recursive associations (such as parent/children) seemlessly.
|
154
160
|
* Can be used to quickly generate a SQL query that you can edit/use manually.
|
155
161
|
|
156
|
-
## More examples
|
157
|
-
|
158
|
-
High level explanation of various ways of using the methods. Also take a look at [usage tips](#usage-tips)
|
159
|
-
|
160
|
-
```ruby
|
161
|
-
# Find my_post's comments that were not made by an admin
|
162
|
-
# Uses a Hash for the condition
|
163
|
-
my_post.comments.where_assoc_not_exists(:author, is_admin: true)
|
164
|
-
|
165
|
-
# Find my_user's posts that have comments by an admin
|
166
|
-
# Uses an array as shortcut to go to a nested related
|
167
|
-
# Uses the block shortcut to use a scope that exists on Author
|
168
|
-
my_user.posts.where_assoc_exists([:comments, :author], &:admins).where(...)
|
169
|
-
|
170
|
-
# Find my_user's posts that have at least 5 non-spam comments
|
171
|
-
# Uses a block with a parameter to do a condition
|
172
|
-
my_user.posts.where_assoc_count(5, :>=, :comments) { |s| s.where(spam: false) }
|
173
|
-
|
174
|
-
# Find my_user's posts that have at least 5 non-spam comments
|
175
|
-
# Uses a block without parameters to do a condition
|
176
|
-
my_user.posts.where_assoc_count(5, :>=, :comments) { where(spam: false) }
|
177
|
-
|
178
|
-
# Find my_user's posts that have comments by an honest admin
|
179
|
-
# Uses multiple associations.
|
180
|
-
# Uses a hash as 2nd parameter to do the conditions
|
181
|
-
my_user.posts.where_assoc_exists([:comments, :author], honest: true, is_admin: true)
|
182
|
-
|
183
|
-
# Find any post that has reached its maximum number of allowed comments
|
184
|
-
# Uses a string on the left side (first parameter) to refer to a column in the previous table.
|
185
|
-
Post.where_assoc_count("posts.max_comments_allowed", :==, :comments)
|
186
|
-
```
|
187
|
-
|
188
162
|
## Usage tips
|
189
163
|
|
190
164
|
### Nested associations
|
@@ -216,10 +190,10 @@ This shortcut can be used for every `where_assoc_*` methods. The conditions and
|
|
216
190
|
The following have different meanings:
|
217
191
|
|
218
192
|
```ruby
|
219
|
-
my_user.posts.where_assoc_exists(:comments_authors, is_admin: true,
|
193
|
+
my_user.posts.where_assoc_exists(:comments_authors, is_admin: true, is_honest: true)
|
220
194
|
|
221
195
|
my_user.posts.where_assoc_exists(:comments_authors, is_admin: true)
|
222
|
-
.where_assoc_exists(:comments_authors,
|
196
|
+
.where_assoc_exists(:comments_authors, is_honest: true)
|
223
197
|
```
|
224
198
|
|
225
199
|
The first is the posts of `my_user` that have a comment made by an honest admin. It requires a single comment to match every conditions.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative "active_record_where_assoc/version"
|
4
4
|
require "active_record"
|
5
5
|
|
6
6
|
module ActiveRecordWhereAssoc
|
@@ -13,9 +13,9 @@ module ActiveRecordWhereAssoc
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
require_relative "active_record_where_assoc/core_logic"
|
17
|
+
require_relative "active_record_where_assoc/query_methods"
|
18
|
+
require_relative "active_record_where_assoc/querying"
|
19
19
|
|
20
20
|
ActiveSupport.on_load(:active_record) do
|
21
21
|
ActiveRecord.eager_load!
|
@@ -55,13 +55,13 @@ module ActiveRecordWhereAssoc
|
|
55
55
|
#
|
56
56
|
# # Posts that have at least one comment considered as spam
|
57
57
|
# # Using a Hash
|
58
|
-
# Post.where_assoc_exists(:comments,
|
58
|
+
# Post.where_assoc_exists(:comments, is_spam: true)
|
59
59
|
#
|
60
60
|
# # Using a String
|
61
|
-
# Post.where_assoc_exists(:comments, "
|
61
|
+
# Post.where_assoc_exists(:comments, "is_spam = true")
|
62
62
|
#
|
63
63
|
# # Using an Array (a string and its binds)
|
64
|
-
# Post.where_assoc_exists(:comments, ["
|
64
|
+
# Post.where_assoc_exists(:comments, ["is_spam = ?", true])
|
65
65
|
#
|
66
66
|
# If the condition argument is blank, it is ignored (just like #where does).
|
67
67
|
#
|
@@ -94,7 +94,7 @@ module ActiveRecordWhereAssoc
|
|
94
94
|
# filters or may return nil to do nothing.
|
95
95
|
#
|
96
96
|
# # Using a where for the added condition
|
97
|
-
# Post.where_assoc_exists(:comments) { |comments| comments.where(
|
97
|
+
# Post.where_assoc_exists(:comments) { |comments| comments.where(is_spam: true) }
|
98
98
|
#
|
99
99
|
# # Applying a scope of the relation
|
100
100
|
# Post.where_assoc_exists(:comments) { |comments| comments.spam_flagged }
|
@@ -108,7 +108,7 @@ module ActiveRecordWhereAssoc
|
|
108
108
|
# the block. Everything else is identical to the block with one argument.
|
109
109
|
#
|
110
110
|
# # Using a where for the added condition
|
111
|
-
# Post.where_assoc_exists(:comments) { where(
|
111
|
+
# Post.where_assoc_exists(:comments) { where(is_spam: true) }
|
112
112
|
#
|
113
113
|
# # Applying a scope of the relation
|
114
114
|
# Post.where_assoc_exists(:comments) { spam_flagged }
|
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: 0.1.
|
4
|
+
version: 0.1.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: 2018-04-
|
11
|
+
date: 2018-04-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: niceql
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - '='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.1.14
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - '='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.1.14
|
125
139
|
- !ruby/object:Gem::Dependency
|
126
140
|
name: sqlite3
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -144,6 +158,8 @@ executables: []
|
|
144
158
|
extensions: []
|
145
159
|
extra_rdoc_files: []
|
146
160
|
files:
|
161
|
+
- ALTERNATIVES_PROBLEMS.md
|
162
|
+
- EXAMPLES.md
|
147
163
|
- LICENSE.txt
|
148
164
|
- README.md
|
149
165
|
- lib/active_record_where_assoc.rb
|