Skip to content

Commit 52e8020

Browse files
authored
MONGOID-4716 Fix eager loading of nested referenced associations (#5194)
* MONGOID-4716 failing test * MONGOID-4716 fix nested eager loading * MONGOID-4716 fix bugs and clean tests * MONGOID-4716 add has_many tests
1 parent 38bb021 commit 52e8020

File tree

2 files changed

+341
-7
lines changed

2 files changed

+341
-7
lines changed

lib/mongoid/association/eager_loadable.rb

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,31 @@ def eager_load(docs)
2020
end
2121
end
2222

23-
def preload(relations, docs)
24-
relations.group_by(&:inverse_class_name)
25-
.values
26-
.each do |associations|
27-
associations.group_by(&:relation)
28-
.each do |relation, association|
29-
relation.eager_loader(association, docs).run
23+
# Load the associations for the given documents. This will be done
24+
# recursively to load the associations of the given documents'
25+
# subdocuments.
26+
#
27+
# @param [ Array<Association> ] association The associations to load.
28+
# @param [ Array<Document> ] document The documents.
29+
def preload(associations, docs)
30+
assoc_map = associations.group_by(&:inverse_class_name)
31+
docs_map = { klass.to_s => docs.to_set }
32+
queue = [ klass.to_s ]
33+
34+
while klass = queue.shift
35+
if as = assoc_map.delete(klass)
36+
as.group_by(&:relation)
37+
.each do |relation, assocs|
38+
assocs.each { |a| queue << a.class_name }
39+
40+
docs = docs_map[klass] || []
41+
res = relation.eager_loader(assocs, docs.to_a).run
42+
43+
res.group_by(&:class).each do |k, vs|
44+
docs_map[k.to_s] ||= [].to_set
45+
docs_map[k.to_s].merge(vs)
46+
end
47+
end
3048
end
3149
end
3250
end

spec/mongoid/criteria/includable_spec.rb

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,6 +1076,322 @@ class D
10761076
expect(criteria).to eq([ person ])
10771077
end
10781078
end
1079+
1080+
context "when including nested referenced associations" do
1081+
1082+
context "when using a has_one association" do
1083+
before(:all) do
1084+
class A
1085+
include Mongoid::Document
1086+
has_one :b
1087+
end
1088+
1089+
class B
1090+
include Mongoid::Document
1091+
belongs_to :a
1092+
has_one :c
1093+
end
1094+
1095+
class C
1096+
include Mongoid::Document
1097+
belongs_to :b
1098+
has_one :d
1099+
end
1100+
1101+
class D
1102+
include Mongoid::Document
1103+
belongs_to :c
1104+
end
1105+
end
1106+
1107+
after(:all) do
1108+
Object.send(:remove_const, :A)
1109+
Object.send(:remove_const, :B)
1110+
Object.send(:remove_const, :C)
1111+
Object.send(:remove_const, :D)
1112+
end
1113+
1114+
let!(:a) do
1115+
A.create!
1116+
end
1117+
1118+
let!(:b) do
1119+
B.create!
1120+
end
1121+
1122+
let!(:c) do
1123+
C.create!
1124+
end
1125+
1126+
let!(:d) do
1127+
D.create!
1128+
end
1129+
1130+
before do
1131+
c.d = d
1132+
b.c = c
1133+
a.b = b
1134+
end
1135+
1136+
context "when including the belongs_to assocation" do
1137+
let!(:result) do
1138+
C.includes(b: :a).first
1139+
end
1140+
1141+
it "finds the right document" do
1142+
expect(result).to eq(c)
1143+
expect(result.b).to eq(c.b)
1144+
expect(result.b.a).to eq(c.b.a)
1145+
end
1146+
1147+
it "does not execute a query" do
1148+
expect_no_queries do
1149+
result.b.a
1150+
end
1151+
end
1152+
end
1153+
1154+
context "when including a doubly-nested belongs_to assocation" do
1155+
let!(:result) do
1156+
D.includes(c: { b: :a }).first
1157+
end
1158+
1159+
it "finds the right document" do
1160+
expect(result).to eq(d)
1161+
expect(result.c).to eq(d.c)
1162+
expect(result.c.b).to eq(d.c.b)
1163+
expect(result.c.b.a).to eq(d.c.b.a)
1164+
end
1165+
1166+
it "does not execute a query" do
1167+
expect_no_queries do
1168+
result.c.b.a
1169+
end
1170+
end
1171+
end
1172+
1173+
context "when including the has_many assocation" do
1174+
let!(:result) do
1175+
A.includes(b: :c).first
1176+
end
1177+
1178+
it "finds the right document" do
1179+
expect(result).to eq(a)
1180+
expect(result.b).to eq(a.b)
1181+
expect(result.b.c).to eq(a.b.c)
1182+
end
1183+
1184+
it "does not executes a query" do
1185+
expect_no_queries do
1186+
result.b.c
1187+
end
1188+
end
1189+
end
1190+
1191+
context "when including a doubly-nested has_many assocation" do
1192+
let!(:result) do
1193+
A.includes(b: { c: :d }).first
1194+
end
1195+
1196+
it "finds the right document" do
1197+
expect(result).to eq(a)
1198+
expect(result.b).to eq(a.b)
1199+
expect(result.b.c).to eq(a.b.c)
1200+
expect(result.b.c.d).to eq(a.b.c.d)
1201+
end
1202+
1203+
it "does not execute a query" do
1204+
expect_no_queries do
1205+
result.b.c.d
1206+
end
1207+
end
1208+
end
1209+
1210+
context "when there are multiple documents" do
1211+
let!(:as) do
1212+
res = 9.times.map do |i|
1213+
A.create!.tap do |a|
1214+
a.b = B.create!.tap do |b|
1215+
b.c = C.create!
1216+
end
1217+
end
1218+
end
1219+
[a, *res]
1220+
end
1221+
1222+
let!(:results) do
1223+
A.includes(b: :c).entries.sort
1224+
end
1225+
1226+
it "finds the right document" do
1227+
as.length.times do |i|
1228+
expect(as[i]).to eq(results[i])
1229+
expect(as[i].b).to eq(results[i].b)
1230+
expect(as[i].b.c).to eq(results[i].b.c)
1231+
end
1232+
end
1233+
1234+
it "does not execute a query" do
1235+
expect_no_queries do
1236+
results.each do |a|
1237+
a.b.c
1238+
end
1239+
end
1240+
end
1241+
end
1242+
1243+
context "when there are multiple associations" do
1244+
before(:all) do
1245+
class A
1246+
has_one :c
1247+
end
1248+
1249+
class C
1250+
belongs_to :a
1251+
end
1252+
end
1253+
1254+
let(:c2) { C.create! }
1255+
let(:d2) { D.create! }
1256+
1257+
before do
1258+
a.c = c2
1259+
a.c.d = d2
1260+
end
1261+
1262+
let!(:results) do
1263+
A.includes(b: { c: :d }, c: :d).first
1264+
end
1265+
1266+
it "finds the right document" do
1267+
expect(results).to eq(a)
1268+
expect(results.b).to eq(a.b)
1269+
expect(results.b.c).to eq(a.b.c)
1270+
expect(results.b.c.d).to eq(a.b.c.d)
1271+
expect(results.c).to eq(a.c)
1272+
expect(results.c.d).to eq(a.c.d)
1273+
end
1274+
1275+
it "does not execute a query" do
1276+
expect_no_queries do
1277+
results.c.d
1278+
results.b.c.d
1279+
end
1280+
end
1281+
end
1282+
end
1283+
end
1284+
1285+
context "when using a has_many association" do
1286+
before(:all) do
1287+
class IncUser
1288+
include Mongoid::Document
1289+
has_many :posts, class_name: 'IncPost'
1290+
has_many :comments, class_name: 'IncComment'
1291+
end
1292+
1293+
class IncPost
1294+
include Mongoid::Document
1295+
belongs_to :user, class_name: 'IncUser'
1296+
has_many :comments, class_name: 'IncComment'
1297+
end
1298+
1299+
class IncComment
1300+
include Mongoid::Document
1301+
belongs_to :posts, class_name: 'IncPost'
1302+
belongs_to :user, class_name: 'IncUser'
1303+
belongs_to :thread, class_name: 'IncThread'
1304+
end
1305+
1306+
class IncThread
1307+
include Mongoid::Document
1308+
has_many :comments, class_name: 'IncComment'
1309+
end
1310+
end
1311+
1312+
after(:all) do
1313+
Object.send(:remove_const, :IncUser)
1314+
Object.send(:remove_const, :IncPost)
1315+
Object.send(:remove_const, :IncComment)
1316+
Object.send(:remove_const, :IncThread)
1317+
end
1318+
1319+
let!(:user) do
1320+
IncUser.create!(posts: posts, comments: user_comments)
1321+
end
1322+
1323+
let!(:posts) do
1324+
[ IncPost.create!(comments: post_comments) ]
1325+
end
1326+
1327+
let!(:user_comments) do
1328+
2.times.map{ IncComment.create! }
1329+
end
1330+
1331+
let!(:post_comments) do
1332+
2.times.map{ IncComment.create! }
1333+
end
1334+
1335+
context "when including the same class twice" do
1336+
let!(:results) do
1337+
IncPost.includes({ user: :comments }, :comments).entries.sort
1338+
end
1339+
1340+
it "finds the right documents" do
1341+
posts.length.times do |i|
1342+
expect(posts[i]).to eq(results[i])
1343+
expect(posts[i].user).to eq(results[i].user)
1344+
expect(posts[i].user.comments).to eq(results[i].user.comments)
1345+
expect(posts[i].comments).to eq(results[i].comments)
1346+
end
1347+
end
1348+
1349+
it "does not execute a query" do
1350+
expect_no_queries do
1351+
results.each do |res|
1352+
res.user
1353+
res.user.comments.to_a
1354+
res.comments.to_a
1355+
end
1356+
end
1357+
end
1358+
end
1359+
1360+
context "when the association chain has a class name twice" do
1361+
let!(:thread) { IncThread.create!(comments: user_comments) }
1362+
1363+
let!(:result) do
1364+
IncThread.includes(comments: { user: { posts: :comments } }).first
1365+
end
1366+
1367+
it "finds the right document" do
1368+
expect(result).to eq(thread)
1369+
result.comments.length.times do |i|
1370+
c1 = result.comments[i]
1371+
c2 = thread.comments[i]
1372+
expect(c1).to eq(c2)
1373+
expect(c1.user).to eq(c2.user)
1374+
c1.user.posts.length.times do |i|
1375+
p1 = c1.user.posts[i]
1376+
p2 = c2.user.posts[i]
1377+
1378+
expect(p1).to eq(p2)
1379+
expect(p1.comments).to eq(p2.comments)
1380+
end
1381+
end
1382+
end
1383+
1384+
it "does not execute a query" do
1385+
expect_no_queries do
1386+
result.comments.each do |comment|
1387+
comment.user.posts.each do |post|
1388+
post.comments.to_a
1389+
end
1390+
end
1391+
end
1392+
end
1393+
end
1394+
end
10791395
end
10801396

10811397
describe "#inclusions" do

0 commit comments

Comments
 (0)