From 52c5ca7da32283f80802fc865b20b798e8aebf10 Mon Sep 17 00:00:00 2001 From: 2881099 <2881099@qq.com> Date: Sat, 30 Apr 2022 20:33:32 +0800 Subject: [PATCH] =?UTF-8?q?-=20=E5=A2=9E=E5=8A=A0=20IncludeMany=20?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E6=96=B9=E6=B3=95=E9=87=8D=E8=BD=BD=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=AD=97=E7=AC=A6=E4=B8=B2=E5=8F=82=E6=95=B0?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sqlite/Curd/SqliteSelectTest.cs | 5 + FreeSql/Extensions/FreeSqlGlobalExtensions.cs | 108 +++++++++++++++++- FreeSql/FreeSql.xml | 23 +++- FreeSql/Interface/Curd/ISelect/ISelect1.cs | 2 +- .../SelectProvider/Select0Provider.cs | 19 ++- 5 files changed, 144 insertions(+), 13 deletions(-) diff --git a/FreeSql.Tests/FreeSql.Tests/Sqlite/Curd/SqliteSelectTest.cs b/FreeSql.Tests/FreeSql.Tests/Sqlite/Curd/SqliteSelectTest.cs index 6b832a26..e6005378 100644 --- a/FreeSql.Tests/FreeSql.Tests/Sqlite/Curd/SqliteSelectTest.cs +++ b/FreeSql.Tests/FreeSql.Tests/Sqlite/Curd/SqliteSelectTest.cs @@ -1281,6 +1281,11 @@ WHERE (((cast(a.""Id"" as character)) in (SELECT b.""Title"" }; Assert.Equal(5, g.sqlite.Insert(model4s).ExecuteAffrows()); + var by0 = g.sqlite.Select() + .Where(a => a.model2id <= model1.id) + .ToList(); + by0.IncludeMany(g.sqlite, "childs", "model2111Idaaa=model2id", 2, "id"); + var t0 = g.sqlite.Select() .IncludeMany(a => a.childs.Where(m3 => m3.model2111Idaaa == a.model2id)) .Where(a => a.model2id <= model1.id) diff --git a/FreeSql/Extensions/FreeSqlGlobalExtensions.cs b/FreeSql/Extensions/FreeSqlGlobalExtensions.cs index d34a81a9..58f93671 100644 --- a/FreeSql/Extensions/FreeSqlGlobalExtensions.cs +++ b/FreeSql/Extensions/FreeSqlGlobalExtensions.cs @@ -282,8 +282,8 @@ public static partial class FreeSqlGlobalExtensions #region IncludeMany /// /// 本方法实现从已知的内存 List 数据,进行和 ISelect.IncludeMany 相同功能的贪婪加载 - /// 示例:new List<Song>(new[] { song1, song2, song3 }).IncludeMany(g.sqlite, a => a.Tags); - /// 文档:https://github.com/2881099/FreeSql/wiki/%e8%b4%aa%e5%a9%aa%e5%8a%a0%e8%bd%bd#%E5%AF%BC%E8%88%AA%E5%B1%9E%E6%80%A7-onetomanymanytomany + /// 示例:new List<Song>(new[] { song1, song2, song3 }).IncludeMany(fsql, a => a.Tags); + /// 文档:https://github.com/dotnetcore/FreeSql/wiki/%E8%B4%AA%E5%A9%AA%E5%8A%A0%E8%BD%BD /// /// /// @@ -309,7 +309,6 @@ public static partial class FreeSqlGlobalExtensions select.SetList(list); return list; } - #if net40 #else async public static Task> IncludeManyAsync(this List list, IFreeSql orm, Expression>> navigateSelector, Action> then = null, CancellationToken cancellationToken = default) where T1 : class where TNavigate : class @@ -326,6 +325,109 @@ public static partial class FreeSqlGlobalExtensions return list; } #endif + /// + /// 本方法实现从已知的内存 List 数据,进行和 ISelect.IncludeMany 相同功能的贪婪加载 + /// 示例:new List<Song>(new[] { song1, song2, song3 }).IncludeMany(fsql, "Tags", "ParentId=Id", 5, "Id,Name"); + /// 文档:https://github.com/dotnetcore/FreeSql/wiki/%E8%B4%AA%E5%A9%AA%E5%8A%A0%E8%BD%BD + /// + /// + /// + /// + /// 选择一个集合属性 + /// 设置临时的关系映射,格式:子类属性=T1属性 + /// 每个子集合只取条数 + /// 设置只查询部分字段 + /// + /// + /// + public static List IncludeMany(this List list, IFreeSql orm, string property, string where = null, int take = 0, string select = null) where T1 : class + { + if (list == null || list.Any() == false) return list; + IncludeManyByPropertyNameCommonGetSelect(orm, property, where, take, select).SetList(list); + return list; + } +#if net40 +#else + async public static Task> IncludeManyAsync(this List list, IFreeSql orm, string property, string where = null, int take = 0, string select = null) where T1 : class + { + if (list == null || list.Any() == false) return list; + await IncludeManyByPropertyNameCommonGetSelect(orm, property, where, take, select).SetListAsync(list); + return list; + } +#endif + + static Select1Provider IncludeManyByPropertyNameCommonGetSelect(IFreeSql orm, string property, string where = null, int take = 0, string select = null) where T1 : class + { + if (orm.CodeFirst.IsAutoSyncStructure) + { + var tb = orm.CodeFirst.GetTableByEntity(typeof(T1)); + if (tb == null || tb.Primarys.Any() == false) + (orm.CodeFirst as CodeFirstProvider)._dicSycedTryAdd(typeof(T1)); //._dicSyced.TryAdd(typeof(TReturn), true); + } + var sel = orm.Select() as Select1Provider; + var exp = sel.ConvertStringPropertyToExpression(property, true); + if (exp == null) throw new ArgumentException($"{nameof(property)} 无法解析为表达式树"); + var propElementType = exp.Type.GetGenericArguments().FirstOrDefault() ?? exp.Type.GetElementType(); + var reftb = orm.CodeFirst.GetTableByEntity(propElementType); + if (reftb == null) throw new ArgumentException($"{nameof(property)} 参数错误,它不是集合属性,必须为 IList 或者 ICollection"); + + if (string.IsNullOrWhiteSpace(where) == false) + { + var refparamExp = Expression.Parameter(reftb.Type); + var reffuncType = typeof(Func<,>).MakeGenericType(reftb.Type, typeof(bool)); + var refWhereMethod = Select0Provider.GetMethodEnumerable("Where").MakeGenericMethod(reftb.Type); + + var whereSplit = where.Split(','); + Expression whereExp = null; + for (var a = 0; a < whereSplit.Length; a++) + { + var keyval = whereSplit[a].Split('=').Select(x => x.Trim()).Where(x => string.IsNullOrWhiteSpace(x) == false).ToArray(); + if (keyval.Length != 2) throw new ArgumentException($"{nameof(where)} 参数错误,格式 \"TopicId=Id,多组使用逗号连接\" "); + + if (reftb.ColumnsByCs.TryGetValue(keyval[0], out var keycol) == false) + throw new ArgumentException($"{nameof(where)} 参数错误,{where[a]} 不是有效的属性名,在实体类 {reftb.Type.DisplayCsharp()} 无法找到"); + if (sel._tables[0].Table.ColumnsByCs.TryGetValue(keyval[1], out var valcol) == false) + throw new ArgumentException($"{nameof(where)} 参数错误,{where[a + 1]} 不是有效的属性名,在实体类 {sel._tables[0].Table.Type.DisplayCsharp()} 无法找到"); + + var tmpExp = Expression.Equal( + Expression.Convert(Expression.MakeMemberAccess(refparamExp, reftb.Properties[keyval[0]]), valcol.CsType), + Expression.MakeMemberAccess(sel._tables[0].Parameter, sel._tables[0].Table.Properties[keyval[1]])); + whereExp = whereExp == null ? tmpExp : Expression.And(whereExp, tmpExp); + } + whereExp = Expression.Lambda(reffuncType, whereExp, refparamExp); + exp = Expression.Call(refWhereMethod, exp, whereExp); + } + if (take > 0) + { + var takeMethod = Select0Provider.GetMethodEnumerable("Take").MakeGenericMethod(reftb.Type); + exp = Expression.Call(takeMethod, exp, Expression.Constant(take, typeof(int))); + } + if (select?.Any() == true) + { + var refparamExp = Expression.Parameter(reftb.Type); + var reffuncType = typeof(Func<,>).MakeGenericType(reftb.Type, reftb.Type); + var refWhereMethod = Select0Provider.GetMethodEnumerable("Select").MakeGenericMethod(reftb.Type, reftb.Type); + + Expression memberInitExp = Expression.MemberInit( + reftb.Type.InternalNewExpression(), + select.Split(',').Select(x => x.Trim()).Where(x => string.IsNullOrWhiteSpace(x) == false).Select(a => + { + if (reftb.ColumnsByCs.TryGetValue(a, out var col) == false) + throw new ArgumentException($"{nameof(select)} 参数错误,{a} 不是有效的属性名,在实体类 {reftb.Type.DisplayCsharp()} 无法找到"); + return Expression.Bind(reftb.Properties[col.CsName], Expression.MakeMemberAccess(refparamExp, reftb.Properties[col.CsName])); + }).ToArray()); + + memberInitExp = Expression.Lambda(reffuncType, memberInitExp, refparamExp); + exp = Expression.Call(refWhereMethod, exp, memberInitExp); + } + + var funcType = typeof(Func<,>).MakeGenericType(sel._tables[0].Table.Type, typeof(IEnumerable<>).MakeGenericType(reftb.Type)); + var navigateSelector = Expression.Lambda(funcType, exp, sel._tables[0].Parameter); + var incMethod = sel.GetType().GetMethod("IncludeMany"); + if (incMethod == null) throw new Exception("运行时错误,反射获取 IncludeMany 方法失败"); + incMethod.MakeGenericMethod(reftb.Type).Invoke(sel, new object[] { navigateSelector, null }); + return sel; + } #endregion #region ToTreeList() 父子分类 diff --git a/FreeSql/FreeSql.xml b/FreeSql/FreeSql.xml index be8688b0..cc9f8df9 100644 --- a/FreeSql/FreeSql.xml +++ b/FreeSql/FreeSql.xml @@ -2540,7 +2540,7 @@ 贪婪加载集合的导航属性,其实是分两次查询,ToList 后进行了数据重装 - 文档:https://github.com/2881099/FreeSql/wiki/%e8%b4%aa%e5%a9%aa%e5%8a%a0%e8%bd%bd#%E5%AF%BC%E8%88%AA%E5%B1%9E%E6%80%A7-onetomanymanytomany + 文档:https://github.com/dotnetcore/FreeSql/wiki/%E8%B4%AA%E5%A9%AA%E5%8A%A0%E8%BD%BD 选择一个集合的导航属性,如: .IncludeMany(a => a.Tags) @@ -4638,8 +4638,8 @@ 本方法实现从已知的内存 List 数据,进行和 ISelect.IncludeMany 相同功能的贪婪加载 - 示例:new List<Song>(new[] { song1, song2, song3 }).IncludeMany(g.sqlite, a => a.Tags); - 文档:https://github.com/2881099/FreeSql/wiki/%e8%b4%aa%e5%a9%aa%e5%8a%a0%e8%bd%bd#%E5%AF%BC%E8%88%AA%E5%B1%9E%E6%80%A7-onetomanymanytomany + 示例:new List<Song>(new[] { song1, song2, song3 }).IncludeMany(fsql, a => a.Tags); + 文档:https://github.com/dotnetcore/FreeSql/wiki/%E8%B4%AA%E5%A9%AA%E5%8A%A0%E8%BD%BD @@ -4653,6 +4653,23 @@ 即能 ThenInclude,还可以二次过滤(这个 EFCore 做不到?) + + + 本方法实现从已知的内存 List 数据,进行和 ISelect.IncludeMany 相同功能的贪婪加载 + 示例:new List<Song>(new[] { song1, song2, song3 }).IncludeMany(fsql, "Tags", "ParentId=Id", 5, "Id,Name"); + 文档:https://github.com/dotnetcore/FreeSql/wiki/%E8%B4%AA%E5%A9%AA%E5%8A%A0%E8%BD%BD + + + + + 选择一个集合属性 + 设置临时的关系映射,格式:子类属性=T1属性 + 每个子集合只取条数 + 设置只查询部分字段 + + + + 查询数据,加工为树型 List 返回 diff --git a/FreeSql/Interface/Curd/ISelect/ISelect1.cs b/FreeSql/Interface/Curd/ISelect/ISelect1.cs index 5a483835..8ad115cc 100644 --- a/FreeSql/Interface/Curd/ISelect/ISelect1.cs +++ b/FreeSql/Interface/Curd/ISelect/ISelect1.cs @@ -331,7 +331,7 @@ namespace FreeSql ISelect IncludeIf(bool condition, Expression> navigateSelector) where TNavigate : class; /// /// 贪婪加载集合的导航属性,其实是分两次查询,ToList 后进行了数据重装 - /// 文档:https://github.com/2881099/FreeSql/wiki/%e8%b4%aa%e5%a9%aa%e5%8a%a0%e8%bd%bd#%E5%AF%BC%E8%88%AA%E5%B1%9E%E6%80%A7-onetomanymanytomany + /// 文档:https://github.com/dotnetcore/FreeSql/wiki/%E8%B4%AA%E5%A9%AA%E5%8A%A0%E8%BD%BD /// /// /// 选择一个集合的导航属性,如: .IncludeMany(a => a.Tags) diff --git a/FreeSql/Internal/CommonProvider/SelectProvider/Select0Provider.cs b/FreeSql/Internal/CommonProvider/SelectProvider/Select0Provider.cs index d3d13dea..3652ca52 100644 --- a/FreeSql/Internal/CommonProvider/SelectProvider/Select0Provider.cs +++ b/FreeSql/Internal/CommonProvider/SelectProvider/Select0Provider.cs @@ -168,6 +168,18 @@ namespace FreeSql.Internal.CommonProvider } return exp; } + + public static MethodInfo MethodStringContains = typeof(string).GetMethod("Contains", new[] { typeof(string) }); + public static MethodInfo MethodStringStartsWith = typeof(string).GetMethod("StartsWith", new[] { typeof(string) }); + public static MethodInfo MethodStringEndsWith = typeof(string).GetMethod("EndsWith", new[] { typeof(string) }); + static ConcurrentDictionary MethodEnumerableDic = new ConcurrentDictionary(); + public static MethodInfo GetMethodEnumerable(string methodName) => MethodEnumerableDic.GetOrAdd(methodName, et => + { + var methods = typeof(Enumerable).GetMethods().Where(a => a.Name == et); + if (et == "Select") + return methods.Where(a => a.GetParameters()[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>)).FirstOrDefault(); + return methods.FirstOrDefault(); + }); } public abstract partial class Select0Provider : Select0Provider, ISelect0 where TSelect : class @@ -588,11 +600,6 @@ namespace FreeSql.Internal.CommonProvider return this.OrderBy($"{field} DESC"); } - static MethodInfo MethodStringContains = typeof(string).GetMethod("Contains", new[] { typeof(string) }); - static MethodInfo MethodStringStartsWith = typeof(string).GetMethod("StartsWith", new[] { typeof(string) }); - static MethodInfo MethodStringEndsWith = typeof(string).GetMethod("EndsWith", new[] { typeof(string) }); - static ConcurrentDictionary MethodEnumerableContainsDic = new ConcurrentDictionary(); - static MethodInfo GetMethodEnumerableContains(Type elementType) => MethodEnumerableContainsDic.GetOrAdd(elementType, et => typeof(Enumerable).GetMethods().Where(a => a.Name == "Contains").FirstOrDefault().MakeGenericMethod(elementType)); public TSelect WhereDynamicFilter(DynamicFilterInfo filter) { if (filter == null) return this as TSelect; @@ -686,7 +693,7 @@ namespace FreeSql.Internal.CommonProvider var fiValueAnyArray = getFiListValue(); if (fiValueAnyArray.Length == 0) break; var fiValueAnyArrayType = exp.Type.MakeArrayType(); - exp = Expression.Call(GetMethodEnumerableContains(exp.Type), Expression.Constant(Utils.GetDataReaderValue(fiValueAnyArrayType, fiValueAnyArray), fiValueAnyArrayType), exp); + exp = Expression.Call(GetMethodEnumerable("Contains").MakeGenericMethod(exp.Type), Expression.Constant(Utils.GetDataReaderValue(fiValueAnyArrayType, fiValueAnyArray), fiValueAnyArrayType), exp); if (fi.Operator == DynamicFilterOperator.NotAny) exp = Expression.Not(exp); break; }