Skip to content

Commit 74a050d

Browse files
Merge pull request #234 from sqlkata/includes
add IncludeMany the reverse relation
2 parents 490b5d0 + d9d1df0 commit 74a050d

File tree

6 files changed

+219
-82
lines changed

6 files changed

+219
-82
lines changed

Program/Program.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ static void Main(string[] args)
4848
Console.WriteLine(result.ToString());
4949
};
5050

51+
/*
5152
var accounts = db.Query("Accounts")
5253
.WhereNotNull("BankId")
5354
.Include("bank",
@@ -56,8 +57,19 @@ static void Main(string[] args)
5657
)
5758
.Select("Id", "Name", "BankId")
5859
.OrderByDesc("Id").Limit(10).Get();
60+
*/
5961

60-
Console.WriteLine(JsonConvert.SerializeObject(accounts, Formatting.Indented));
62+
var includedAccountsQuery = db.Query("Accounts").Limit(2)
63+
.IncludeMany("Transactions", db.Query("Transactions"))
64+
.Include("Company", db.Query("Companies"));
65+
66+
var bank = db.Query("Banks as Icon")
67+
.IncludeMany("Accounts", includedAccountsQuery, "BankId")
68+
.WhereExists(q => q.From("Accounts").WhereColumns("Accounts.BankId", "=", "Icon.Id"))
69+
.Limit(1)
70+
.Get();
71+
72+
Console.WriteLine(JsonConvert.SerializeObject(bank, Formatting.Indented));
6173

6274
}
6375

QueryBuilder/Include.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ public class Include
66
public Query Query { get; set; }
77
public string ForeignKey { get; set; }
88
public string LocalKey { get; set; }
9+
public bool IsMany { get; set; }
910
}
1011
}

QueryBuilder/Query.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -310,23 +310,25 @@ public override Query NewQuery()
310310
return new Query();
311311
}
312312

313-
public Query Include(string relationName, Query query, string foreignKey = null, string localKey = "Id")
313+
public Query Include(string relationName, Query query, string foreignKey = null, string localKey = "Id", bool isMany = false)
314314
{
315-
if (foreignKey == null)
316-
{
317-
foreignKey = relationName + "Id";
318-
}
319315

320316
Includes.Add(new Include
321317
{
322318
Name = relationName,
323319
LocalKey = localKey,
324320
ForeignKey = foreignKey,
325321
Query = query,
322+
IsMany = isMany,
326323
});
327324

328325
return this;
329326
}
330327

328+
public Query IncludeMany(string relationName, Query query, string foreignKey = null, string localKey = "Id")
329+
{
330+
return Include(relationName, query, foreignKey, localKey, isMany: true);
331+
}
332+
331333
}
332334
}

SqlKata.Execution/QueryFactory.Extensions.Async.cs

Lines changed: 99 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Dapper;
66
using System.Threading.Tasks;
77
using System.Dynamic;
8+
using Humanizer;
89

910
namespace SqlKata.Execution
1011
{
@@ -22,44 +23,7 @@ public static async Task<IEnumerable<T>> GetAsync<T>(this QueryFactory db, Query
2223
commandTimeout: db.QueryTimeout
2324
)).ToList();
2425

25-
if (!result.Any())
26-
{
27-
return result;
28-
}
29-
30-
if (result[0] is IDynamicMetaObjectProvider)
31-
{
32-
var dynamicResult = result
33-
.Cast<IDictionary<string, object>>()
34-
.Select(x => new Dictionary<string, object>(x, StringComparer.OrdinalIgnoreCase))
35-
.ToList();
36-
37-
foreach (var include in query.Includes)
38-
{
39-
var ids = dynamicResult.Where(x => x[include.ForeignKey] != null)
40-
.Select(x => x[include.ForeignKey].ToString())
41-
.ToList();
42-
43-
if (!ids.Any())
44-
{
45-
continue;
46-
}
47-
48-
var related = (await include.Query.WhereIn(include.LocalKey, ids).GetAsync())
49-
.Cast<IDictionary<string, object>>()
50-
.Select(x => new Dictionary<string, object>(x, StringComparer.OrdinalIgnoreCase))
51-
.ToDictionary(x => x[include.LocalKey].ToString());
52-
53-
foreach (var item in dynamicResult)
54-
{
55-
var foreignValue = item[include.ForeignKey].ToString();
56-
item[include.Name] = related.ContainsKey(foreignValue) ? related[foreignValue] : null;
57-
}
58-
}
59-
60-
return dynamicResult.Cast<T>();
61-
62-
}
26+
result = (await handleIncludesAsync(query, result)).ToList();
6327

6428
return result;
6529
}
@@ -332,5 +296,102 @@ public static async Task<int> StatementAsync(this QueryFactory db, string sql, o
332296
return await db.Connection.ExecuteAsync(sql, param, commandTimeout: db.QueryTimeout);
333297
}
334298
#endregion
299+
300+
// TODO: currently am copying this from the handleInclude (sync) method, refactor this and reuse the common part.
301+
private static async Task<IEnumerable<T>> handleIncludesAsync<T>(Query query, IEnumerable<T> result)
302+
{
303+
if (!result.Any())
304+
{
305+
return result;
306+
}
307+
308+
var canBeProcessed = query.Includes.Any() && result.ElementAt(0) is IDynamicMetaObjectProvider;
309+
310+
if (!canBeProcessed)
311+
{
312+
return result;
313+
}
314+
315+
var dynamicResult = result
316+
.Cast<IDictionary<string, object>>()
317+
.Select(x => new Dictionary<string, object>(x, StringComparer.OrdinalIgnoreCase))
318+
.ToList();
319+
320+
foreach (var include in query.Includes)
321+
{
322+
323+
if (include.IsMany)
324+
{
325+
if (include.ForeignKey == null)
326+
{
327+
// try to guess the default key
328+
// I will try to fetch the table name if provided and appending the Id as a convention
329+
// Here am using Humanizer package to help getting the singular form of the table
330+
331+
var fromTable = query.GetOneComponent("from") as FromClause;
332+
333+
if (fromTable == null)
334+
{
335+
throw new InvalidOperationException($"Cannot guess the foreign key for the included relation '{include.Name}'");
336+
}
337+
338+
var table = fromTable.Alias ?? fromTable.Table;
339+
340+
include.ForeignKey = table.Singularize(false) + "Id";
341+
}
342+
343+
var localIds = dynamicResult.Where(x => x[include.LocalKey] != null)
344+
.Select(x => x[include.LocalKey].ToString())
345+
.ToList();
346+
347+
if (!localIds.Any())
348+
{
349+
continue;
350+
}
351+
352+
var children = (await include.Query.WhereIn(include.ForeignKey, localIds).GetAsync())
353+
.Cast<IDictionary<string, object>>()
354+
.Select(x => new Dictionary<string, object>(x, StringComparer.OrdinalIgnoreCase))
355+
.GroupBy(x => x[include.ForeignKey].ToString())
356+
.ToDictionary(x => x.Key, x => x.ToList());
357+
358+
foreach (var item in dynamicResult)
359+
{
360+
var localValue = item[include.LocalKey].ToString();
361+
item[include.Name] = children.ContainsKey(localValue) ? children[localValue] : new List<Dictionary<string, object>>();
362+
}
363+
364+
continue;
365+
}
366+
367+
if (include.ForeignKey == null)
368+
{
369+
include.ForeignKey = include.Name + "Id";
370+
}
371+
372+
var foreignIds = dynamicResult.Where(x => x[include.ForeignKey] != null)
373+
.Select(x => x[include.ForeignKey].ToString())
374+
.ToList();
375+
376+
if (!foreignIds.Any())
377+
{
378+
continue;
379+
}
380+
381+
var related = (await include.Query.WhereIn(include.LocalKey, foreignIds).GetAsync())
382+
.Cast<IDictionary<string, object>>()
383+
.Select(x => new Dictionary<string, object>(x, StringComparer.OrdinalIgnoreCase))
384+
.ToDictionary(x => x[include.LocalKey].ToString());
385+
386+
foreach (var item in dynamicResult)
387+
{
388+
var foreignValue = item[include.ForeignKey].ToString();
389+
item[include.Name] = related.ContainsKey(foreignValue) ? related[foreignValue] : null;
390+
}
391+
}
392+
393+
return dynamicResult.Cast<T>();
394+
395+
}
335396
}
336397
}

SqlKata.Execution/QueryFactory.Extensions.cs

Lines changed: 98 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Dynamic;
55
using System.Linq;
66
using Dapper;
7+
using Humanizer;
78

89
namespace SqlKata.Execution
910
{
@@ -21,44 +22,7 @@ public static IEnumerable<T> Get<T>(this QueryFactory db, Query query)
2122
commandTimeout: db.QueryTimeout
2223
).ToList();
2324

24-
if (!result.Any())
25-
{
26-
return result;
27-
}
28-
29-
if (result[0] is IDynamicMetaObjectProvider)
30-
{
31-
var dynamicResult = result
32-
.Cast<IDictionary<string, object>>()
33-
.Select(x => new Dictionary<string, object>(x, StringComparer.OrdinalIgnoreCase))
34-
.ToList();
35-
36-
foreach (var include in query.Includes)
37-
{
38-
var ids = dynamicResult.Where(x => x[include.ForeignKey] != null)
39-
.Select(x => x[include.ForeignKey].ToString())
40-
.ToList();
41-
42-
if (!ids.Any())
43-
{
44-
continue;
45-
}
46-
47-
var related = include.Query.WhereIn(include.LocalKey, ids).Get()
48-
.Cast<IDictionary<string, object>>()
49-
.Select(x => new Dictionary<string, object>(x, StringComparer.OrdinalIgnoreCase))
50-
.ToDictionary(x => x[include.LocalKey].ToString());
51-
52-
foreach (var item in dynamicResult)
53-
{
54-
var foreignValue = item[include.ForeignKey].ToString();
55-
item[include.Name] = related.ContainsKey(foreignValue) ? related[foreignValue] : null;
56-
}
57-
}
58-
59-
return dynamicResult.Cast<T>();
60-
61-
}
25+
result = handleIncludes<T>(query, result).ToList();
6226

6327
return result;
6428
}
@@ -305,5 +269,101 @@ public static int Statement(this QueryFactory db, string sql, object param = nul
305269
return db.Connection.Execute(sql, param, commandTimeout: db.QueryTimeout);
306270
}
307271
#endregion
272+
273+
private static IEnumerable<T> handleIncludes<T>(Query query, IEnumerable<T> result)
274+
{
275+
if (!result.Any())
276+
{
277+
return result;
278+
}
279+
280+
var canBeProcessed = query.Includes.Any() && result.ElementAt(0) is IDynamicMetaObjectProvider;
281+
282+
if (!canBeProcessed)
283+
{
284+
return result;
285+
}
286+
287+
var dynamicResult = result
288+
.Cast<IDictionary<string, object>>()
289+
.Select(x => new Dictionary<string, object>(x, StringComparer.OrdinalIgnoreCase))
290+
.ToList();
291+
292+
foreach (var include in query.Includes)
293+
{
294+
295+
if (include.IsMany)
296+
{
297+
if (include.ForeignKey == null)
298+
{
299+
// try to guess the default key
300+
// I will try to fetch the table name if provided and appending the Id as a convention
301+
// Here am using Humanizer package to help getting the singular form of the table
302+
303+
var fromTable = query.GetOneComponent("from") as FromClause;
304+
305+
if (fromTable == null)
306+
{
307+
throw new InvalidOperationException($"Cannot guess the foreign key for the included relation '{include.Name}'");
308+
}
309+
310+
var table = fromTable.Alias ?? fromTable.Table;
311+
312+
include.ForeignKey = table.Singularize(false) + "Id";
313+
}
314+
315+
var localIds = dynamicResult.Where(x => x[include.LocalKey] != null)
316+
.Select(x => x[include.LocalKey].ToString())
317+
.ToList();
318+
319+
if (!localIds.Any())
320+
{
321+
continue;
322+
}
323+
324+
var children = include.Query.WhereIn(include.ForeignKey, localIds).Get()
325+
.Cast<IDictionary<string, object>>()
326+
.Select(x => new Dictionary<string, object>(x, StringComparer.OrdinalIgnoreCase))
327+
.GroupBy(x => x[include.ForeignKey].ToString())
328+
.ToDictionary(x => x.Key, x => x.ToList());
329+
330+
foreach (var item in dynamicResult)
331+
{
332+
var localValue = item[include.LocalKey].ToString();
333+
item[include.Name] = children.ContainsKey(localValue) ? children[localValue] : new List<Dictionary<string, object>>();
334+
}
335+
336+
continue;
337+
}
338+
339+
if (include.ForeignKey == null)
340+
{
341+
include.ForeignKey = include.Name + "Id";
342+
}
343+
344+
var foreignIds = dynamicResult.Where(x => x[include.ForeignKey] != null)
345+
.Select(x => x[include.ForeignKey].ToString())
346+
.ToList();
347+
348+
if (!foreignIds.Any())
349+
{
350+
continue;
351+
}
352+
353+
var related = include.Query.WhereIn(include.LocalKey, foreignIds).Get()
354+
.Cast<IDictionary<string, object>>()
355+
.Select(x => new Dictionary<string, object>(x, StringComparer.OrdinalIgnoreCase))
356+
.ToDictionary(x => x[include.LocalKey].ToString());
357+
358+
foreach (var item in dynamicResult)
359+
{
360+
var foreignValue = item[include.ForeignKey].ToString();
361+
item[include.Name] = related.ContainsKey(foreignValue) ? related[foreignValue] : null;
362+
}
363+
}
364+
365+
return dynamicResult.Cast<T>();
366+
367+
}
308368
}
309369
}

SqlKata.Execution/SqlKata.Execution.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
</ItemGroup>
55
<ItemGroup>
66
<PackageReference Include="dapper" Version="1.50.5" />
7+
<PackageReference Include="Humanizer.Core" Version="2.6.2" />
78
</ItemGroup>
89
<PropertyGroup>
910
<TargetFrameworks>netstandard1.3;net451</TargetFrameworks>

0 commit comments

Comments
 (0)