activerecord-has_some_of_many 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f35b023998c92a01d9614640c84f296ac53c15ba1459daec41da6fc042b7ff8c
|
4
|
+
data.tar.gz: d004c4ce3f54eea253f86a0347d5690693b220e9f364e9c36037df91502c021e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 52201a2fb0479d35037a324178434820f87eb3b527733e9d7ce797153b50bc8160fe01a38c8fbe698f86bb9728054b8a1a73030f500436586236d10c6d3a3841
|
7
|
+
data.tar.gz: b827a9092bd4d422c367b61afca9cd0d3edbe70738c9b7a49db1de6848e3c0c9a55d4c187b817389ea03d332a91f9536b1d1de78b7ba96ec327c86fdd6a891b7
|
data/README.md
CHANGED
@@ -1,13 +1,23 @@
|
|
1
1
|
# `activerecord-has_some_of_many`
|
2
2
|
|
3
|
-
This gem adds new optimized Active Record association methods (`has_one_of_many`, `has_some_of_many`) for "top N" queries to ActiveRecord using `JOIN LATERAL` that are eager-loadable (`includes(:association)`, `preloads(:association)`) to avoid N+1 queries, and is compatible with typical queries and batch methods (`find_each`, `in_batches`, find_in_batches`). For example, you might have these types of queries in your application:
|
3
|
+
This gem adds new optimized Active Record association methods (`has_one_of_many`, `has_some_of_many`) for "top N" queries to ActiveRecord using `JOIN LATERAL` that are eager-loadable (`includes(:association)`, `preloads(:association)`) to avoid N+1 queries, and is compatible with typical queries and batch methods (`find_each`, `in_batches`, `find_in_batches`). For example, you might have these types of queries in your application:
|
4
4
|
|
5
|
-
- Users have many posts, and you want to query the most recent post for each user
|
5
|
+
- Users have many posts, and you want to query the most recent post for each user
|
6
6
|
- Posts have many comments, and you want to query the 5 most recent visible comments for each post
|
7
7
|
- Posts have many comments, and you want to query the one comment with the largest `votes_count` for each post
|
8
8
|
|
9
9
|
You can read more about these types of queries on [Benito Serna's "Fetching the top n per group with a lateral join with rails"](https://bhserna.com/fetching-the-top-n-per-group-with-a-lateral-join-with-rails.html).
|
10
10
|
|
11
|
+
## Compatibility
|
12
|
+
|
13
|
+
This gem is only compatible with databases that offer `LATERAL` joins within Active Record. As far as I'm aware, that is **only Postgres**.
|
14
|
+
|
15
|
+
This gem is not necessary on SQLite, as SQLite will perform lateral-like behavior on join queries by default. MySQL has support for lateral queries, but they are not yet implemented in Active Record.
|
16
|
+
|
17
|
+
**Really complex queries may not work; please open an issue!** This library works by rewriting Active Record queries; it's possible to create some very complex queries with Active Record. Some things that are specifically known to work:
|
18
|
+
- Model associations to the same model. It works.
|
19
|
+
- _Please help expand this list by opening an issue if you find something that doesn't work!_
|
20
|
+
|
11
21
|
## Usage
|
12
22
|
|
13
23
|
Add to your gemfile, and run `bundle install`:
|
@@ -25,7 +35,7 @@ class User < ActiveRecord::Base
|
|
25
35
|
# You can also use `has_some_of_many` to get the top N records. Be sure to add a limit to the scope.
|
26
36
|
has_some_of_many :last_five_posts, -> { order("created_at DESC").limit(5) }, class_name: "Post"
|
27
37
|
|
28
|
-
# More complex
|
38
|
+
# More complex scopes are possible, for example:
|
29
39
|
has_one_of_many :top_comment, -> { where(published: true).order("votes_count DESC") }, class_name: "Comment"
|
30
40
|
has_some_of_many :top_ten_comments, -> { where(published: true).order("votes_count DESC").limit(10) }, class_name: "Comment"
|
31
41
|
end
|
@@ -37,17 +47,17 @@ User.where(active: true).includes(:last_post, :last_five_posts, :top_comment).ea
|
|
37
47
|
user.top_comment
|
38
48
|
end
|
39
49
|
|
40
|
-
#
|
50
|
+
# Add compound indexes to your database to make these queries fast!
|
41
51
|
add_index :comments, [:post_id, :created_at]
|
42
52
|
add_index :comments, [:post_id, :votes_count]
|
43
53
|
```
|
44
54
|
|
45
55
|
## Why?
|
46
56
|
|
47
|
-
Finding the "Top N" is a common problem, that can be easily solved with a `JOIN LATERAL` when writing raw SQL queries. Lateral Joins were introduced in Postgres 9.3:
|
57
|
+
Finding the "Top N" is a common problem, that can be easily solved with a `JOIN LATERAL` when writing raw SQL queries. Lateral Joins were introduced in Postgres 9.3:
|
48
58
|
|
49
59
|
> a LATERAL join is like a SQL foreach loop, in which Postgres will iterate over each row in a result set and evaluate a subquery using that row as a parameter. ([source](https://www.heap.io/blog/postgresqls-powerful-new-join-type-lateral))
|
50
|
-
|
60
|
+
|
51
61
|
For example, to find only the one most recent comments for a collection of posts, we might write:
|
52
62
|
|
53
63
|
```sql
|
@@ -33,7 +33,8 @@ module ActiveRecord
|
|
33
33
|
.select(klass.arel_table[primary_key].as(foreign_key_alias), lateral_table[Arel.star])
|
34
34
|
.arel.join(
|
35
35
|
relation
|
36
|
-
.
|
36
|
+
.then { |query| relation.table.name == klass.arel_table.name ? ActiveRecord::HasSomeOfMany::RelationRewriter.new(query).alias_table("#{klass.table_name}__alias") : query }
|
37
|
+
.then { |query| query.where(query.table[foreign_key].eq(klass.arel_table[primary_key])) }
|
37
38
|
.then { |query| limit ? query.limit(limit) : query }
|
38
39
|
.arel.lateral(lateral_table.name)
|
39
40
|
).on("TRUE")
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module HasSomeOfMany
|
5
|
+
class RelationRewriter
|
6
|
+
def initialize(relation)
|
7
|
+
@relation = relation
|
8
|
+
end
|
9
|
+
|
10
|
+
def alias_table(alias_name)
|
11
|
+
relation_class = @relation.klass
|
12
|
+
original_table = relation_class.arel_table
|
13
|
+
alias_table = original_table.alias(alias_name)
|
14
|
+
|
15
|
+
where_clause = recursively_alias_table(@relation.where_clause.ast, original_table, alias_table)
|
16
|
+
order_clause = @relation.order_values.map do |order|
|
17
|
+
if order.respond_to?(:expr)
|
18
|
+
order.expr = alias_table[order.expr.name] if order.expr.relation == original_table
|
19
|
+
end
|
20
|
+
order
|
21
|
+
end
|
22
|
+
|
23
|
+
new_relation = @relation.dup.unscope(:select, :where, :order)
|
24
|
+
new_relation.instance_variable_set(:@table, alias_table) # Is there a better way to modify the original relation?
|
25
|
+
new_relation.where(where_clause).order(order_clause)
|
26
|
+
end
|
27
|
+
|
28
|
+
def recursively_alias_table(node, original_table, alias_table)
|
29
|
+
case node
|
30
|
+
when Arel::Nodes::And, Arel::Nodes::Or
|
31
|
+
return nil if node.children.empty?
|
32
|
+
|
33
|
+
# Recurse for left and right nodes
|
34
|
+
node.children.each { |child| recursively_alias_table(child, original_table, alias_table) }
|
35
|
+
node
|
36
|
+
when Arel::Nodes::Grouping
|
37
|
+
# Recurse for the expression inside the grouping
|
38
|
+
node.expr = recursively_alias_table(node.expr, original_table, alias_table)
|
39
|
+
node
|
40
|
+
when Arel::Nodes::Equality, Arel::Nodes::GreaterThan, Arel::Nodes::LessThan, Arel::Nodes::HomogeneousIn
|
41
|
+
# For conditions, modify left-hand side (the column)
|
42
|
+
if node.left.relation == original_table
|
43
|
+
node.left.relation = alias_table
|
44
|
+
end
|
45
|
+
node
|
46
|
+
else
|
47
|
+
node
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'active_support/lazy_load_hooks'
|
4
|
-
require_relative "version"
|
4
|
+
require_relative "activerecord/has_some_of_many/version"
|
5
5
|
|
6
6
|
ActiveSupport.on_load(:active_record) do
|
7
7
|
require_relative "activerecord/has_some_of_many/associations"
|
8
|
+
require_relative "activerecord/has_some_of_many/relation_rewriter"
|
9
|
+
|
8
10
|
include ActiveRecord::HasSomeOfMany::Associations
|
9
11
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-has_some_of_many
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Sheldon [he/him]
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- README.md
|
37
37
|
- lib/activerecord-has_some_of_many.rb
|
38
38
|
- lib/activerecord/has_some_of_many/associations.rb
|
39
|
+
- lib/activerecord/has_some_of_many/relation_rewriter.rb
|
39
40
|
- lib/activerecord/has_some_of_many/version.rb
|
40
41
|
homepage: https://github.com/bensheldon/activerecord-has_some_of_many
|
41
42
|
licenses:
|