From b814921a6ac35973861f8d46c86db062fdd7fb1d Mon Sep 17 00:00:00 2001 From: 2881099 <2881099@qq.com> Date: Wed, 13 Dec 2023 20:59:25 +0800 Subject: [PATCH] =?UTF-8?q?-=20=E5=A2=9E=E5=8A=A0=20=E4=BD=8E=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=89=A9=E5=B1=95=E5=8C=85=20FreeSql.Extensions.ZoreE?= =?UTF-8?q?ntity=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FreeSql.Extensions.ZoreEntity.csproj | 43 + .../FreeSql.Extensions.ZoreEntity.xml | 67 + .../ZoreDbContext.SelectImpl.cs | 915 ++++++++++ .../ZoreDbContext.cs | 1476 +++++++++++++++++ .../ZoreDescriptor.cs | 97 ++ .../FreeSql.Extensions.ZoreEntity/key.snk | Bin 0 -> 596 bytes FreeSql-lite.sln | 19 +- FreeSql/FreeSql.xml | 210 +-- 8 files changed, 2727 insertions(+), 100 deletions(-) create mode 100644 Extensions/FreeSql.Extensions.ZoreEntity/FreeSql.Extensions.ZoreEntity.csproj create mode 100644 Extensions/FreeSql.Extensions.ZoreEntity/FreeSql.Extensions.ZoreEntity.xml create mode 100644 Extensions/FreeSql.Extensions.ZoreEntity/ZoreDbContext.SelectImpl.cs create mode 100644 Extensions/FreeSql.Extensions.ZoreEntity/ZoreDbContext.cs create mode 100644 Extensions/FreeSql.Extensions.ZoreEntity/ZoreDescriptor.cs create mode 100644 Extensions/FreeSql.Extensions.ZoreEntity/key.snk diff --git a/Extensions/FreeSql.Extensions.ZoreEntity/FreeSql.Extensions.ZoreEntity.csproj b/Extensions/FreeSql.Extensions.ZoreEntity/FreeSql.Extensions.ZoreEntity.csproj new file mode 100644 index 00000000..807c517a --- /dev/null +++ b/Extensions/FreeSql.Extensions.ZoreEntity/FreeSql.Extensions.ZoreEntity.csproj @@ -0,0 +1,43 @@ + + + + netstandard2.0;net45;net40 + true + FreeSql;ncc;YeXiangQin + FreeSql 扩展包,实现 低代码、零实体类型,并且支持导航属性,级联保存 等功能. + https://github.com/2881099/FreeSql + https://github.com/2881099/FreeSql + git + MIT + FreeSql;ORM;低代码; + $(AssemblyName) + logo.png + $(AssemblyName) + true + true + true + key.snk + false + 3.2.806-preview20231214 + readme.md + + + + + + + + + FreeSql.Extensions.ZoreEntity.xml + 3 + + + + net40 + + + + + + + diff --git a/Extensions/FreeSql.Extensions.ZoreEntity/FreeSql.Extensions.ZoreEntity.xml b/Extensions/FreeSql.Extensions.ZoreEntity/FreeSql.Extensions.ZoreEntity.xml new file mode 100644 index 00000000..667a67a9 --- /dev/null +++ b/Extensions/FreeSql.Extensions.ZoreEntity/FreeSql.Extensions.ZoreEntity.xml @@ -0,0 +1,67 @@ + + + + FreeSql.Extensions.ZoreEntity + + + + + 【有状态管理】自动 Include 查询 + + + + + 【无状态管理】指定表查询 + + + + + Type = Update 的时候,获取更新之前的对象 + + + + + 实体变化记录 + + + + + 实体变化事件 + + + + + 举例1:LeftJoin("table1", "id", "user.id") -> LEFT JOIN [table1] b ON b.[id] = a.[id] + 举例2:LeftJoin("table1", "id", "user.id", "xid", "user.xid") -> LEFT JOIN [table1] b ON b.[id] = [a].id] AND b.[xid] = a.[xid] + + + + + 举例1:InnerJoin("table1", "id", "user.id") -> INNER JOIN [table1] b ON b.[id] = a.[id] + 举例2:InnerJoin("table1", "id", "user.id", "xid", "user.xid") -> INNER JOIN [table1] b ON b.[id] = [a].id] AND b.[xid] = a.[xid] + + + + + 举例1:RightJoin("table1", "id", "user.id") -> RIGTH JOIN [table1] b ON b.[id] = a.[id] + 举例2:RightJoin("table1", "id", "user.id", "xid", "user.xid") -> RIGTH JOIN [table1] b ON b.[id] = [a].id] AND b.[xid] = a.[xid] + + + + + WHERE [Id] IN (...) + + + + + Where(new { Year = 2017, CategoryId = 198, IsPublished = true }) + WHERE [Year] = 2017 AND [CategoryId] = 198 AND [IsPublished] = 1 + + + + + WHERE [field] = .. + + + + diff --git a/Extensions/FreeSql.Extensions.ZoreEntity/ZoreDbContext.SelectImpl.cs b/Extensions/FreeSql.Extensions.ZoreEntity/ZoreDbContext.SelectImpl.cs new file mode 100644 index 00000000..574f7d36 --- /dev/null +++ b/Extensions/FreeSql.Extensions.ZoreEntity/ZoreDbContext.SelectImpl.cs @@ -0,0 +1,915 @@ +using FreeSql.DataAnnotations; +using FreeSql.Internal; +using FreeSql.Internal.CommonProvider; +using FreeSql.Internal.Model; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Text; +using T = System.Collections.Generic.Dictionary; + +namespace FreeSql.Extensions.ZoreEntity +{ + partial class ZoreDbContext + { + public class SelectImpl + { + ZoreDbContext _dbcontext; + IFreeSql _orm => _dbcontext._orm; + List _tables => _dbcontext._tables; + int _mainTableIndex = -1; + List _tableAlias; + ISelect _select; + Select0Provider _selectProvider; + string _field; + Dictionary _fieldAlias; + List> _fieldReader; + string _groupBy; + List _params = new List(); + CommonUtils _common => _selectProvider._commonUtils; + bool _useStates = true; + bool _includeAll = false; + + SelectImpl() { } + internal SelectImpl(ZoreDbContext dbcontext, string tableName) + { + _dbcontext = dbcontext; + var tableIndex = _tables.FindIndex(a => a.CsName.ToLower() == tableName?.ToLower()); + if (tableIndex == -1) throw new Exception($"未定义表名 {tableName}"); + + _mainTableIndex = tableIndex; + _tableAlias = new List(); + _select = _orm.Select() + .AsTable((t, old) => _tables[tableIndex].DbName); + _selectProvider = _select as Select0Provider; + _fieldAlias = new Dictionary(); + _fieldReader = new List>(); + FlagFetchResult(_tables[_mainTableIndex], "a", ""); + } + + public SelectImpl NoTracking() + { + _useStates = false; + return this; + } + public SelectImpl IncludeAll() + { + var ignores = new Dictionary(); + _includeAll = true; + LocalAutoInclude(_tables[_mainTableIndex], "a"); + return this; + + void LocalAutoInclude(ZoreTableInfo table, string alias, string navPath = "") + { + if (ignores.ContainsKey(table.CsName)) return; + ignores.Add(table.CsName, true); + TableAliasInfo tableAlias = null; + if (table != _tables[_mainTableIndex]) + tableAlias = FlagFetchResult(table, alias.ToString(), navPath); + + var leftJoins = table.Navigates.Where(a => a.Value.RefType == TableRefType.ManyToOne || a.Value.RefType == TableRefType.OneToOne).ToArray(); + foreach (var join in leftJoins) + { + var joinTable = join.Value.RefTable; + if (ignores.ContainsKey(joinTable.CsName)) return; + + var joinAlias = GetMaxAlias(); + var joinOn = string.Join(" AND ", join.Value.RefColumns.Select((bname, idx) => + $"{joinAlias}.{_common.QuoteSqlName(join.Value.RefTable.ColumnsByCs[bname].Attribute.Name)} = {alias}.{_common.QuoteSqlName(join.Value.Table.ColumnsByCs[join.Value.Columns[idx]].Attribute.Name)}")); + _select.LeftJoin($"{_common.QuoteSqlName(join.Value.RefTable.DbName)} {joinAlias} ON {joinOn}"); + + LocalAutoInclude(joinTable, joinAlias, $"{navPath}.{join.Key}"); + } + if (tableAlias == null) tableAlias = _tableAlias.Where(a => a.Alias == alias).First(); + var includeManys = table.Navigates.Where(a => a.Value.RefType == TableRefType.OneToMany || a.Value.RefType == TableRefType.ManyToMany).ToArray(); + foreach (var includeMany in includeManys) + tableAlias.IncludeMany.Add(NativeTuple.Create(includeMany.Key, (Action)null)); + } + } + public SelectImpl Include(string navigate, Action then = null) + { + var alias = _tableAlias[0]; + var navPath = navigate.Split('.'); + var navPath2 = ""; + for (var navIdx = 0; navIdx < navPath.Length; navIdx++) + { + if (alias.Table.Navigates.TryGetValue(navPath[navIdx], out var nav) == false) throw new Exception($"{alias.Table.CsName} 未定义导航属性 {navPath[navIdx]}"); + if (nav.RefType == TableRefType.OneToMany || nav.RefType == TableRefType.ManyToMany) + { + if (navIdx < navPath.Length - 1) throw new Exception($"导航属性 OneToMany/ManyToMany {navPath[navIdx]} 不能处于中间位置"); + if (alias.IncludeMany.Where(a => a.Item1 == nav.NavigateKey).Any() == false) + alias.IncludeMany.Add(NativeTuple.Create(nav.NavigateKey, then)); + break; + } + navPath2 = navIdx == 0 ? nav.NavigateKey : $"{navPath2}.{nav.NavigateKey}"; + var navAlias = _tableAlias.Where(a => string.Join(".", a.NavPath) == navPath2).FirstOrDefault(); + if (navAlias == null) + { + var joinAlias = GetMaxAlias(); + var joinOn = string.Join(" AND ", nav.RefColumns.Select((bname, idx) => + $"{joinAlias}.{_common.QuoteSqlName(nav.RefTable.ColumnsByCs[bname].Attribute.Name)} = {alias.Alias}.{_common.QuoteSqlName(nav.Table.ColumnsByCs[nav.Columns[idx]].Attribute.Name)}")); + _select.LeftJoin($"{_common.QuoteSqlName(nav.RefTable.DbName)} {joinAlias} ON {joinOn}"); + navAlias = FlagFetchResult(nav.RefTable, joinAlias, navPath2); + } + alias = navAlias; + } + return this; + } + //public SelectImpl IncludeSubQuery(string resultKey, string tableName, Action then) + //{ + // var query = _dbcontext.SelectNoTracking(tableName); + // query._tableAlias.AddRange(_tableAlias); + // return this; + //} + + /// + /// 举例1:LeftJoin("table1", "id", "user.id") -> LEFT JOIN [table1] b ON b.[id] = a.[id] + /// 举例2:LeftJoin("table1", "id", "user.id", "xid", "user.xid") -> LEFT JOIN [table1] b ON b.[id] = [a].id] AND b.[xid] = a.[xid] + /// + public SelectImpl LeftJoin(string tableName, params string[] onFields) => Join("LEFT JOIN", tableName, onFields); + /// + /// 举例1:InnerJoin("table1", "id", "user.id") -> INNER JOIN [table1] b ON b.[id] = a.[id] + /// 举例2:InnerJoin("table1", "id", "user.id", "xid", "user.xid") -> INNER JOIN [table1] b ON b.[id] = [a].id] AND b.[xid] = a.[xid] + /// + public SelectImpl InnerJoin(string tableName, params string[] onFields) => Join("INNER JOIN", tableName, onFields); + /// + /// 举例1:RightJoin("table1", "id", "user.id") -> RIGTH JOIN [table1] b ON b.[id] = a.[id] + /// 举例2:RightJoin("table1", "id", "user.id", "xid", "user.xid") -> RIGTH JOIN [table1] b ON b.[id] = [a].id] AND b.[xid] = a.[xid] + /// + public SelectImpl RightJoin(string tableName, params string[] onFields) => Join("RIGTH JOIN", tableName, onFields); + SelectImpl Join(string joinType, string tableName, params string[] onFields) + { + if (onFields == null || onFields.Length == 0) throw new Exception($"{joinType} 参数 {nameof(onFields)} 不能为空"); + if (onFields.Length % 2 != 0) throw new Exception($"{joinType} 参数 {nameof(onFields)} 数组长度必须为偶数,正确:.LeftJoin(\"table1\", \"id\", \"user.id\")"); + + var table = _tables.Where(a => a.CsName.ToLower() == tableName?.ToLower()).FirstOrDefault(); + if (table == null) throw new Exception($"未定义表名 {tableName}"); + var alias = GetMaxAlias(); + var navKey = tableName; + for (var a = 2; true; a++) + { + if (_tables[_mainTableIndex].ColumnsByCs.ContainsKey(navKey)) + { + navKey = $"{tableName}{a}"; + continue; + } + if (_tableAlias.Any(b => b.NavPath.Length == 1 && b.NavPath.Last() == navKey)) + { + navKey = $"{tableName}{a}"; + continue; + } + break; + } + FlagFetchResult(table, alias, navKey); + var joinOn = new string[onFields.Length / 2]; + for (var a = 0; a < onFields.Length; a += 2) + { + var field1 = ParseField(table, alias, onFields[a]); + if (field1 == null) throw new Exception($"未匹配字段名 {onFields[a]}"); + var field2 = ParseField(table, alias, onFields[a + 1]); + if (field2 == null) throw new Exception($"未匹配字段名 {onFields[a + 1]}"); + joinOn[a / 2] = $"{field1.Item1} = {field2.Item1}"; + } + _select.RawJoin($"{joinType} {_common.QuoteSqlName(table.DbName)} {alias} ON {string.Join(" AND ", joinOn)}"); + return this; + } + + class TableAliasInfo + { + public string Alias { get; set; } + public ZoreTableInfo Table { get; set; } + public string[] NavPath { get; set; } + public List>> IncludeMany { get; set; } = new List>>(); + } + class DbDataReaderContext + { + public DbDataReader Reader { get; set; } + public int Index { get; set; } + public T Result { get; set; } + } + string GetMaxAlias() + { + var max = (int)_tableAlias.Where(a => a.Alias.Length == 1).Max(a => a.Alias[0]); + if (max < 'a') max = 'a'; + for (var a = 1; true; a++) + { + var alias = ((char)(max + a)).ToString(); + if (_tableAlias.Where(b => b.Alias == alias).Any()) continue; + return alias; + } + } + TableAliasInfo FlagFetchResult(ZoreTableInfo table, string alias, string navPath) + { + var tableAlias = _tableAlias.Where(a => a.Alias == alias).FirstOrDefault(); + if (tableAlias == null) + { + var navPathArray = navPath.Split('.').Where(a => string.IsNullOrWhiteSpace(a) == false).ToArray(); + _tableAlias.Add(tableAlias = new TableAliasInfo + { + Alias = alias, + Table = table, + NavPath = navPathArray + }); + } + var sbfield = new StringBuilder(); + if (string.IsNullOrEmpty(_field) == false) sbfield.Append(", ").Append(_field); + foreach (var col in table.Columns.Values) + { + var colName = col.Attribute.Name; + for (var a = 2; true; a++) + { + if (_fieldAlias.ContainsKey(colName)) colName = $"{col.Attribute.Name}{a}"; + else break; + } + _fieldAlias.Add(colName, col.Attribute.Name); + sbfield.Append(", ").Append(alias).Append(".").Append(_common.QuoteSqlName(col.Attribute.Name)); + if (colName != col.Attribute.Name) sbfield.Append(_common.FieldAsAlias(colName)); + } + _field = sbfield.Remove(0, 2).ToString(); + sbfield.Clear(); + + _fieldReader.Add(dr => + { + var pkIsNull = false; + foreach (var col in table.Columns.Values) + { + if (pkIsNull == false && col.Attribute.IsPrimary) + { + pkIsNull = dr.Reader.IsDBNull(dr.Index); + if (pkIsNull) dr.Result.Clear(); + } + if (pkIsNull == false) dr.Result[col.CsName] = Utils.GetDataReaderValue(col.CsType, dr.Reader.GetValue(dr.Index)); + dr.Index++; + } + }); + return tableAlias; + } + T FetchResult(DbDataReader reader) + { + var fieldIndex = 0; + var result = new T(); + for (var aliasIndex = 0; aliasIndex < _tableAlias.Count; aliasIndex++) + { + var navValue = result; + var drctx = new DbDataReaderContext { Index = fieldIndex, Reader = reader }; + if (aliasIndex == 0) + { + drctx.Result = result; + } + else + { + var isNull = false; + for (var navidx = 0; navidx < _tableAlias[aliasIndex].NavPath.Length - 1; navidx++) + { + var navKey = _tableAlias[aliasIndex].NavPath[navidx]; + if (navValue.ContainsKey(navKey) == false) + { + isNull = true; + break; + } + navValue = navValue[navKey] as T; + if (navValue == null) + { + isNull = true; + break; + } + } + if (isNull) + { + fieldIndex += _tableAlias[aliasIndex].Table.Columns.Count; + continue; + } + drctx.Result = new T(); + navValue[_tableAlias[aliasIndex].NavPath.LastOrDefault()] = drctx.Result; + } + _fieldReader[aliasIndex](drctx); + fieldIndex = drctx.Index; + if (aliasIndex > 0 && drctx.Result.Any() == false) navValue.Remove(_tableAlias[aliasIndex].NavPath.LastOrDefault()); + } + return result; + } + + void IncludeMany(TableAliasInfo alias, NativeTuple> navMany, List list, List flagIndexs = null) + { + if (list?.Any() != true) return; + if (flagIndexs == null) flagIndexs = new List(); + flagIndexs.Add(alias.Table.CsName); + + var nav = alias.Table.Navigates[navMany.Item1]; + if (_includeAll && flagIndexs.Contains(nav.RefTable.CsName)) return; + + if (nav.RefType == TableRefType.OneToMany) + { + var subTable = nav.RefTable; + var subSelect = new SelectImpl(_dbcontext, subTable.CsName); + if (_includeAll) subSelect.IncludeAll(); + + Func> getWhereDic = () => + { + var sbDic = new Dictionary(); + for (var y = 0; y < list.Count; y++) + { + var sbWhereOne = new StringBuilder(); + sbWhereOne.Append("("); + for (var z = 0; z < nav.Columns.Count; z++) + { + if (z > 0) sbWhereOne.Append(" AND "); + var refcol = nav.RefTable.ColumnsByCs[nav.RefColumns[z]]; + var val = Utils.GetDataReaderValue(refcol.Attribute.MapType, list[y][nav.Columns[z]]); + sbWhereOne.Append(_common.FormatSql($"a.{_common.QuoteSqlName(refcol.Attribute.Name)}={{0}}", val)); + } + sbWhereOne.Append(")"); + var whereOne = sbWhereOne.ToString(); + sbWhereOne.Clear(); + if (sbDic.ContainsKey(whereOne) == false) sbDic.Add(whereOne, true); + } + return sbDic; + }; + if (nav.Columns.Count == 1) + { + var refcol = nav.RefTable.ColumnsByCs[nav.RefColumns[0]]; + var args1 = $"a.{_common.QuoteSqlName(refcol.Attribute.Name)}"; + var left = _common.FormatSql("{0}", new object[] { list.Select(a => a[nav.Columns[0]]).Distinct().ToArray() }); + subSelect._select.Where($"({args1} in {left.Replace(", \r\n \r\n", $") \r\n OR {args1} in (")})"); + } + else + { + var sbDic = getWhereDic(); + var sbWhere = new StringBuilder(); + foreach (var sbd in sbDic) + sbWhere.Append(" OR ").Append(sbd.Key); + subSelect._select.Where(sbWhere.Remove(0, 4).ToString()); + sbWhere.Clear(); + sbDic.Clear(); + } + navMany.Item2?.Invoke(subSelect); + var subList = subSelect.ToListPrivate(null, null, flagIndexs); + foreach (var item in list) + { + item[nav.NavigateKey] = subList.Where(a => + { + for (var z = 0; z < nav.Columns.Count; z++) + if (CompareEntityPropertyValue(nav.Table.ColumnsByCs[nav.Columns[z]].Attribute.MapType, item[nav.Columns[z]], a[nav.RefColumns[z]]) == false) + return false; + return true; + }).ToList(); + } + subList.Clear(); + } + else if (nav.RefType == TableRefType.ManyToMany) + { + var subTable = nav.RefTable; + var subSelect = new SelectImpl(_dbcontext, subTable.CsName); + if (_includeAll) subSelect.IncludeAll(); + + var middleJoinOn = string.Join(" AND ", nav.RefColumns.Select((bname, idx) => + $"midtb.{_common.QuoteSqlName(nav.RefMiddleTable.ColumnsByCs[nav.MiddleColumns[nav.Columns.Count + idx]].Attribute.Name)} = a.{_common.QuoteSqlName(nav.RefTable.ColumnsByCs[bname].Attribute.Name)}")); + subSelect._select.InnerJoin($"{_common.QuoteSqlName(nav.RefMiddleTable.DbName)} midtb ON {middleJoinOn}"); + + Func> getWhereDic = () => + { + var sbDic = new Dictionary(); + for (var y = 0; y < list.Count; y++) + { + var sbWhereOne = new StringBuilder(); + sbWhereOne.Append("("); + for (var z = 0; z < nav.Columns.Count; z++) + { + if (z > 0) sbWhereOne.Append(" AND "); + var midcol = nav.RefMiddleTable.ColumnsByCs[nav.MiddleColumns[z]]; + var val = Utils.GetDataReaderValue(midcol.Attribute.MapType, list[y][nav.Columns[z]]); + sbWhereOne.Append(_common.FormatSql($"midtb.{_common.QuoteSqlName(midcol.Attribute.Name)}={{0}}", val)); + } + sbWhereOne.Append(")"); + var whereOne = sbWhereOne.ToString(); + sbWhereOne.Clear(); + if (sbDic.ContainsKey(whereOne) == false) sbDic.Add(whereOne, true); + } + return sbDic; + }; + if (nav.Columns.Count == 1) + { + var midcol = nav.RefMiddleTable.ColumnsByCs[nav.MiddleColumns[0]]; + var args1 = $"midtb.{_common.QuoteSqlName(midcol.Attribute.Name)}"; + var left = _common.FormatSql("{0}", new object[] { list.Select(a => a[nav.Columns[0]]).Distinct().ToArray() }); + subSelect._select.Where($"({args1} in {left.Replace(", \r\n \r\n", $") \r\n OR {args1} in (")})"); + } + else + { + var sbDic = getWhereDic(); + var sbWhere = new StringBuilder(); + foreach (var sbd in sbDic) + sbWhere.Append(" OR ").Append(sbd.Key); + subSelect._select.Where(sbWhere.Remove(0, 4).ToString()); + sbWhere.Clear(); + sbDic.Clear(); + } + navMany.Item2?.Invoke(subSelect); + var subList = subSelect.ToListPrivate( + string.Join(", ", nav.MiddleColumns.Select((a, idx) => $"midtb.{_common.QuoteSqlName(nav.RefMiddleTable.ColumnsByCs[a].Attribute.Name)}{_common.FieldAsAlias($"midtb_field__{idx}")}")), + (dict, dr) => + { + var fieldCount = dr.FieldCount - nav.MiddleColumns.Count; + for (var z = 0; z < nav.MiddleColumns.Count; z++) + dict[$"midtb_field__{z}"] = Utils.GetDataReaderValue(nav.RefMiddleTable.ColumnsByCs[nav.MiddleColumns[z]].CsType, dr.GetValue(fieldCount + z)); + }, flagIndexs); + foreach (var item in list) + { + item[nav.NavigateKey] = subList.Where(a => + { + for (var z = 0; z < nav.Columns.Count; z++) + if (CompareEntityPropertyValue(nav.Table.ColumnsByCs[nav.Columns[z]].Attribute.MapType, item[nav.Columns[z]], a[$"midtb_field__{z}"]) == false) + return false; + return true; + }).ToList(); + } + foreach (var subItem in subList) + for (var z = 0; z < nav.MiddleColumns.Count; z++) + subItem.Remove($"midtb_field__{z}"); + subList.Clear(); + } + } + + public string ToSql(string field = null) + { + if (string.IsNullOrWhiteSpace(field)) return _select.ToSql(_field); + return _select.ToSql(field); + } + List ToListPrivate(string otherField, Action otherReader, List flagIndexs = null) + { + var sql = string.IsNullOrWhiteSpace(otherField) ? this.ToSql() : this.ToSql($"{_field},{otherField}"); + var ret = new List(); + var dbParms = _params.ToArray(); + var before = new Aop.CurdBeforeEventArgs(_tables[_mainTableIndex].Type, _tables[_mainTableIndex], Aop.CurdType.Select, sql, dbParms); + _orm.Aop.CurdBeforeHandler?.Invoke(this, before); + Exception exception = null; + try + { + _orm.Ado.ExecuteReader(_dbcontext._transaction?.Connection, _dbcontext._transaction, fetch => + { + var item = FetchResult(fetch.Object); + otherReader?.Invoke(item, fetch.Object); + ret.Add(item); + }, CommandType.Text, sql, _dbcontext._commandTimeout, dbParms); + } + catch (Exception ex) + { + exception = ex; + throw; + } + finally + { + var after = new Aop.CurdAfterEventArgs(before, exception, ret); + _orm.Aop.CurdAfterHandler?.Invoke(this, after); + } + foreach (var join in _tableAlias) + { + if (join.IncludeMany.Any() == false) continue; + var list = new List(); + if (join.Alias == "a") list = ret; + else + { + foreach (var obj in ret) + { + T item = obj; + foreach (var navKey in join.NavPath) + { + if (string.IsNullOrWhiteSpace(navKey)) continue; + item.TryGetValue(navKey, out var obj2); + item = obj2 as T; + if (item == null) break; + } + if (item != null) list.Add(item); + } + } + foreach(var navMany in join.IncludeMany) + IncludeMany(join, navMany, list, flagIndexs); + } + if (_useStates && flagIndexs == null) + foreach (var item in ret) + _dbcontext.AttachCascade(_tables[_mainTableIndex], item, true); + return ret; + } + public List ToList() => ToListPrivate(null, null); + public T ToOne() => ToListPrivate(null, null).FirstOrDefault(); + public T First() => ToOne(); + public bool Any() => _select.Any(); + public long Count() => _select.Count(); + public SelectImpl Count(out long count) + { + _select.Count(out count); + return this; + } + public SelectImpl WithTransaction(DbTransaction transaction) + { + _select.WithTransaction(transaction); + return this; + } + public SelectImpl WithConnection(DbConnection connection) + { + _select.WithConnection(connection); + return this; + } + public SelectImpl CommandTimeout(int timeout) + { + _select.CommandTimeout(timeout); + return this; + } + public SelectImpl Distinct() + { + _select.Distinct(); + return this; + } + public SelectImpl Master() + { + _select.Master(); + return this; + } + public SelectImpl ForUpdate(bool nowait = false) + { + _select.ForUpdate(nowait); + return this; + } + + NativeTuple ParseField(ZoreTableInfo firstTable, string firstTableAlias, string property) + { + if (string.IsNullOrEmpty(property)) return null; + var field = property.Split('.').Select(a => a.Trim()).ToArray(); + + if (field.Length == 1) + { + if (firstTable != null && firstTable.ColumnsByCs.TryGetValue(field[0], out var col2) == true) + return NativeTuple.Create($"{firstTableAlias}.{_common.QuoteSqlName(col2.Attribute.Name)}", col2); + + foreach (var ta2 in _tableAlias) + { + if (ta2.Table.ColumnsByCs.TryGetValue(field[0], out col2)) + return NativeTuple.Create($"{ta2.Alias}.{_common.QuoteSqlName(col2.Attribute.Name)}", col2); + } + } + else if (field.Length == 2) + { + if (firstTable != null && firstTable.CsName.ToLower() == field[0].ToLower() && firstTable.ColumnsByCs.TryGetValue(field[1], out var col2) == true) + return NativeTuple.Create($"{firstTableAlias}.{_common.QuoteSqlName(col2.Attribute.Name)}", col2); + + var ta2s = _tableAlias.Where(a => a.Table.CsName.ToLower() == field[0].ToLower()).ToArray(); + if (ta2s.Length == 1 && ta2s[0].Table.ColumnsByCs.TryGetValue(field[1], out col2) == true) + return NativeTuple.Create($"{ta2s[0].Alias}.{_common.QuoteSqlName(col2.Attribute.Name)}", col2); + if (ta2s.Length > 1) + { + ta2s = _tableAlias.Where(a => a.Table.CsName == field[0]).ToArray(); + if (ta2s.Length == 1 && ta2s[0].Table.ColumnsByCs.TryGetValue(field[1], out col2) == true) + return NativeTuple.Create($"{ta2s[0].Alias}.{_common.QuoteSqlName(col2.Attribute.Name)}", col2); + } + if (_tableAlias.Where(a => a.Alias == field[0]).FirstOrDefault()?.Table.ColumnsByCs.TryGetValue(field[1], out col2) == true) + return NativeTuple.Create($"{field[0]}.{_common.QuoteSqlName(col2.Attribute.Name)}", col2); + } + + var navPath = string.Join(".", field.Skip(1).Take(field.Length - 1)); + var ta = _tableAlias.Where(a => string.Join(".", a.NavPath) == navPath).FirstOrDefault(); + if (ta?.Table.ColumnsByCs.TryGetValue(field.Last(), out var col) == true) + return NativeTuple.Create($"{ta.Alias}.{_common.QuoteSqlName(col.Attribute.Name)}", col); + throw new Exception(CoreStrings.Cannot_Match_Property(property)); + } + + /// + /// WHERE [Id] IN (...) + /// + public SelectImpl Where(IEnumerable items) + { + var alias = _tableAlias.Where(a => a.Table == _tables[_mainTableIndex]).FirstOrDefault()?.Alias; + if (!string.IsNullOrWhiteSpace(alias)) alias = $"{alias}."; + var where = _common.WhereItems(_tables[_mainTableIndex].Primarys, alias, items); + _select.Where(where); + return this; + } + /// + /// Where(new { Year = 2017, CategoryId = 198, IsPublished = true }) + /// WHERE [Year] = 2017 AND [CategoryId] = 198 AND [IsPublished] = 1 + /// + public SelectImpl Where(object multipleFields) + { + if (multipleFields == null) return this; + foreach (var prop in multipleFields.GetType().GetProperties()) + WhereDynamicFilter(new DynamicFilterInfo { Field = prop.Name, Operator = DynamicFilterOperator.Eq, Value = prop.GetValue(multipleFields, null) }); + return this; + } + /// + /// WHERE [field] = .. + /// + public SelectImpl Where(string field, object value) => WhereDynamicFilter(new DynamicFilterInfo { Field = field, Operator = DynamicFilterOperator.Eq, Value = value }); + public SelectImpl Where(string field, string @operator, object value) + { + switch (@operator?.ToLower().Trim()) + { + case "=": + case "==": + case "eq": + return WhereDynamicFilter(new DynamicFilterInfo { Field = field, Operator = DynamicFilterOperator.Eq, Value = value }); + case "!=": + case "<>": + return WhereDynamicFilter(new DynamicFilterInfo { Field = field, Operator = DynamicFilterOperator.NotEqual, Value = value }); + case ">": + return WhereDynamicFilter(new DynamicFilterInfo { Field = field, Operator = DynamicFilterOperator.GreaterThan, Value = value }); + case ">=": + return WhereDynamicFilter(new DynamicFilterInfo { Field = field, Operator = DynamicFilterOperator.GreaterThanOrEqual, Value = value }); + case "<": + return WhereDynamicFilter(new DynamicFilterInfo { Field = field, Operator = DynamicFilterOperator.LessThan, Value = value }); + case "<=": + return WhereDynamicFilter(new DynamicFilterInfo { Field = field, Operator = DynamicFilterOperator.LessThanOrEqual, Value = value }); + case "like": + case "contains": + return WhereDynamicFilter(new DynamicFilterInfo { Field = field, Operator = DynamicFilterOperator.Contains, Value = value }); + case "!like": + case "notlike": + case "not like": + return WhereDynamicFilter(new DynamicFilterInfo { Field = field, Operator = DynamicFilterOperator.NotContains, Value = value }); + case "in": + return WhereDynamicFilter(new DynamicFilterInfo { Field = field, Operator = DynamicFilterOperator.Any, Value = value }); + case "!in": + case "notin": + case "not in": + return WhereDynamicFilter(new DynamicFilterInfo { Field = field, Operator = DynamicFilterOperator.Any, Value = value }); + } + throw new Exception($"未实现 {@operator}"); + } + public SelectImpl WhereColumns(string field1, string @operator, string field2) + { + var field1Result = ParseField(null, null, field1); + if (field1Result == null) throw new Exception($"未匹配字段名 {field1}"); + var field2Result = ParseField(null, null, field2); + if (field2Result == null) throw new Exception($"未匹配字段名 {field2}"); + switch (@operator?.ToLower().Trim()) + { + case "=": + case "==": + case "eq": + _select.Where($"{field1Result.Item1} = {field2Result.Item1}"); + return this; + case "!=": + case "<>": + _select.Where($"{field1Result.Item1} <> {field2Result.Item1}"); + return this; + case ">": + _select.Where($"{field1Result.Item1} > {field2Result.Item1}"); + return this; + case ">=": + _select.Where($"{field1Result.Item1} >= {field2Result.Item1}"); + return this; + case "<": + _select.Where($"{field1Result.Item1} < {field2Result.Item1}"); + return this; + case "<=": + _select.Where($"{field1Result.Item1} <= {field2Result.Item1}"); + return this; + } + throw new Exception($"未实现 {@operator}"); + } + public SelectImpl WhereDynamicFilter(DynamicFilterInfo filter) + { + var sql = ParseDynamicFilter(filter); + _selectProvider._where.Append(sql); + return this; + } + string ParseDynamicFilter(DynamicFilterInfo filter) + { + var replacedFilter = new DynamicFilterInfo(); + var replacedMap = new List>(); + LocalCloneFilter(filter, replacedFilter); + var oldWhere = _selectProvider._where.ToString(); + var newWhere = ""; + try + { + _selectProvider._where.Clear(); + _select.WhereDynamicFilter(replacedFilter); + newWhere = _selectProvider._where.ToString(); + } + finally + { + _selectProvider._where.Clear().Append(oldWhere); + } + foreach (var rm in replacedMap) + { + var find = $"a.{_common.QuoteSqlName(rm.Item1)}"; + var idx = newWhere.IndexOf(find); + if (idx != -1) newWhere = $"{newWhere.Substring(0, idx)}{rm.Item2}{newWhere.Substring(idx + find.Length)}"; + } + return newWhere; + + void LocalCloneFilter(DynamicFilterInfo source, DynamicFilterInfo target) + { + target.Field = source.Field; + target.Operator = source.Operator; + target.Value = source.Value; + target.Logic = source.Logic; + if (string.IsNullOrWhiteSpace(source.Field) == false) + { + var parseResult = ParseField(null, null, source.Field); + if (parseResult != null) + { + if (TestDynamicFilterInfo._dictTypeToPropertyname.TryGetValue(parseResult.Item2.Attribute.MapType, out var pname)) + target.Field = pname; + else + target.Field = TestDynamicFilterInfo._dictTypeToPropertyname[typeof(string)]; + replacedMap.Add(NativeTuple.Create(target.Field, parseResult.Item1)); + } + } + if (source.Filters?.Any() == true) + { + target.Filters = new List(); + foreach (var sourceChild in source.Filters) + { + var targetChild = new DynamicFilterInfo(); + target.Filters.Add(targetChild); + LocalCloneFilter(sourceChild, targetChild); + } + } + } + } + public class SubQuery + { + internal SelectImpl _parentQuery; + internal SubQuery() { } + public SelectImpl From(string tableName) + { + var query = _parentQuery._dbcontext.SelectNoTracking(tableName); + query._selectProvider._tables[0].Alias = + query._tableAlias[0].Alias = $"sub_{_parentQuery._tableAlias[0].Alias}"; + query._tableAlias.AddRange(_parentQuery._tableAlias); + return query; + } + } + public SelectImpl WhereExists(Func q) + { + var query = q?.Invoke(new SubQuery { _parentQuery = this }); + switch (_orm.Ado.DataType) + { + case DataType.Oracle: + case DataType.OdbcOracle: + case DataType.CustomOracle: + case DataType.Dameng: + case DataType.OdbcDameng: + case DataType.GBase: + query.Limit(-1); + break; + default: + query.Limit(1); //#462 ORACLE rownum <= 2 会影响索引变慢 + break; + } + _selectProvider._where.Append($" AND EXISTS({query.ToSql("1").Replace(" \r\n", " \r\n ")})"); + return this; + } + public SelectImpl GroupByRaw(string sql) + { + if (string.IsNullOrWhiteSpace(sql)) return this; + _useStates = false; + _groupBy = $"{_groupBy}, {sql}"; + _select.GroupBy(_groupBy); + return this; + } + public SelectImpl GroupBy(string[] fields) + { + var count = 0; + for (var a = 0; a < fields.Length; a++) + { + if (string.IsNullOrWhiteSpace(fields[a])) continue; + var field1 = ParseField(null, null, fields[a]); + if (field1 == null) throw new Exception($"未匹配字段名 {fields[a]}"); + _groupBy = $"{_groupBy}, {field1.Item1}"; + count++; + } + if (count > 0) + { + _useStates = false; + _select.GroupBy(_groupBy); + } + return this; + } + public SelectImpl HavingRaw(string sql) + { + _select.Having(sql); + return this; + } + public SelectImpl OrderByRaw(string sql) + { + _select.OrderBy(sql); + return this; + } + SelectImpl OrderBy(bool isdesc, string[] fields) + { + for (var a = 0; a < fields.Length; a ++) + { + if (string.IsNullOrWhiteSpace(fields[a])) continue; + var field1 = ParseField(null, null, fields[a]); + if (field1 == null) throw new Exception($"未匹配字段名 {fields[a]}"); + if (isdesc) _select.OrderBy($"{field1.Item1} DESC"); + else _select.OrderBy(field1.Item1); + } + return this; + } + public SelectImpl OrderBy(params string[] fields) => OrderBy(false, fields); + public SelectImpl OrderByDescending(params string[] fields) => OrderBy(true, fields); + public SelectImpl Offset(int offset) + { + _select.Offset(offset); + return this; + } + public SelectImpl Limit(int limit) + { + _select.Limit(limit); + return this; + } + public SelectImpl Skip(int offset) => Offset(offset); + public SelectImpl Take(int limit) => Limit(limit); + public SelectImpl Page(int pageNumber, int pageSize) + { + _select.Page(pageNumber, pageSize); + return this; + } + + TResult InternalQuerySingle(string field) => _orm.Ado.CommandFluent(this.ToSql(field)) + .WithConnection(_selectProvider._connection) + .WithTransaction(_selectProvider._transaction).QuerySingle(); + public decimal Sum(string field) + { + var field1 = ParseField(null, null, field); + if (field1 == null) throw new Exception($"未匹配字段名 {field}"); + return InternalQuerySingle($"sum({field1.Item1})"); + } + public TMember Min(string field) + { + var field1 = ParseField(null, null, field); + if (field1 == null) throw new Exception($"未匹配字段名 {field}"); + return InternalQuerySingle($"min({field1.Item1})"); + } + public TMember Max(string field) + { + var field1 = ParseField(null, null, field); + if (field1 == null) throw new Exception($"未匹配字段名 {field}"); + return InternalQuerySingle($"max({field1.Item1})"); + } + public double Avg(string field) + { + var field1 = ParseField(null, null, field); + if (field1 == null) throw new Exception($"未匹配字段名 {field}"); + return InternalQuerySingle($"avg({field1.Item1})"); + } + + + [Table(DisableSyncStructure = true)] + class TestDynamicFilterInfo + { + public Guid DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf00 { get; set; } + public bool DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf01 { get; set; } + public string DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf02 { get; set; } + public char DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf03 { get; set; } + public DateTime DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf04 { get; set; } + public DateTimeOffset DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf05 { get; set; } + public TimeSpan DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf06 { get; set; } + + public int DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf07 { get; set; } + public long DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf08 { get; set; } + public short DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf09 { get; set; } + public sbyte DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf10 { get; set; } + + public uint DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf11 { get; set; } + public ulong DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf12 { get; set; } + public ushort DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf13 { get; set; } + public byte DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf14 { get; set; } + public byte[] DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf15 { get; set; } + + public double DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf16 { get; set; } + public float DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf17 { get; set; } + public decimal DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf18 { get; set; } + + internal static Dictionary _dictTypeToPropertyname = new Dictionary + { + [typeof(Guid)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf00), + [typeof(bool)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf01), + [typeof(string)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf02), + [typeof(char)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf03), + [typeof(DateTime)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf04), + [typeof(DateTimeOffset)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf05), + [typeof(TimeSpan)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf06), + + [typeof(int)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf07), + [typeof(long)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf08), + [typeof(short)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf09), + [typeof(sbyte)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf10), + + [typeof(uint)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf11), + [typeof(ulong)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf12), + [typeof(ushort)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf13), + [typeof(byte)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf14), + [typeof(byte[])] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf15), + + [typeof(double)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf16), + [typeof(float)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf17), + [typeof(decimal)] = nameof(DynamicField_4bf98fbe2b4d4d14bbb3fc66fa08bf18), + }; + } + } + } +} diff --git a/Extensions/FreeSql.Extensions.ZoreEntity/ZoreDbContext.cs b/Extensions/FreeSql.Extensions.ZoreEntity/ZoreDbContext.cs new file mode 100644 index 00000000..2cf5d95f --- /dev/null +++ b/Extensions/FreeSql.Extensions.ZoreEntity/ZoreDbContext.cs @@ -0,0 +1,1476 @@ +using FreeSql.DataAnnotations; +using FreeSql.Internal; +using FreeSql.Internal.CommonProvider; +using FreeSql.Internal.Model; +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Reflection; +using T = System.Collections.Generic.Dictionary; + +namespace FreeSql.Extensions.ZoreEntity +{ + + /* +理解本机制之前,请先忘记 Repository/DbContext 等之前有关级联的内容,他们没有关联。 + +schemas[] 是一组表映射信息定义,包含表名、列名、导航属性、索引等信息 +导航属性:OneToOne/OneToMany/ManyToOne/ManyToMany +聚合根:OneToOne/OneToMany/多对多中间表,作为一个整体看待 +外部根:ManyToOne/ManyToMany外部表,作为外部看待,它有自己的聚合根整体 +User 为聚合根 +UserExt/UserClaim/UserRole 这三个表是子成员,一起存储/删除 +Role 为外部根(相对 User 而言,它自己是独立的聚合根) + +CURD 都是基于 schemas[0] 聚合根进行操作 +查询:贪婪加载所有子成员,以及外部根,以及外部根的外部根(递归) +状态管理:快照聚合根副本(由于外部根也是聚合根,即外部根与聚合根是并行存储关系) + +对比保存: +将当前操作的聚合根与状态管理的副本进行对比,计算出发生变化的列 +OneToOne 副本NULL 最新Object 添加新记录 +OneToOne 副本Object 最新NULL 删除副本记录 +OneToOne 副本Object 最新Object 发生变化则更新,否则忽略 +OneToMany 副本NULL/Empty 最新List 添加最新List记录 +OneToMany 副本List 最新NULL 忽略 +OneToMany 副本List 最新Empty 删除副本List记录 +OneToMany 副本List 最新List 对比保存,计算出添加/更新/删除三种行为 +多对多中间表 与 OneToMany 一致 + +插入: +OneToOne 级联插入 +OneToMany 级联插入 +ManyToOne 先对比保存外部根,关联外部根ID,再插入聚合根 +ManyToMany 先对比保存外部根,插入聚合根,再插入中间表 + +更新: +OneToOne 级联对比保存 +OneToMany 级联对比保存 +ManyToOne 先对比保存外部根,再关联外部根ID,再更新聚合根 +ManyToMany 先对比保存外部根,再更新聚合根,再对比保存中间表 + +删除: +OneToOne 级联删除 +OneToMany 级联删除 +ManyToOne 忽略 +ManyToMany 级联删除中间表(注意不删除外部根) + */ + public partial class ZoreDbContext + { + internal IFreeSql _orm; + internal DbTransaction _transaction; + internal int _commandTimeout; + internal List _tables; + public ZoreDbContext(IFreeSql orm, TableDescriptor[] schemas) + { + _orm = orm; + _tables = VilidateSchemaToInfo(orm, schemas); + if (orm.CodeFirst.IsAutoSyncStructure) + foreach (var table in _tables) + orm.CodeFirst.SyncStructure(table, table.DbName, false); + } + + static List VilidateSchemaToInfo(IFreeSql orm, IEnumerable schemas) + { + var common = (orm.Ado as AdoProvider)._util; + var tables = new List(); + foreach (var dtd in schemas) + { + if (string.IsNullOrWhiteSpace(dtd.Name)) continue; + if (string.IsNullOrWhiteSpace(dtd.DbName)) dtd.DbName = dtd.Name; + var tabattr = new TableAttribute + { + Name = dtd.DbName, + AsTable = dtd.AsTable, + }; + var tabindexs = dtd.Indexes.Select(a => new IndexAttribute(a.Name, a.Fields, a.IsUnique) + { + IndexMethod = a.IndexMethod, + }); + var tab = new ZoreTableInfo(); + tab.Comment = dtd.Comment; + tab.Type = typeof(object); + tab.CsName = dtd.Name; + tab.DbName = dtd.DbName; + var isQuery = tab.DbName.StartsWith("(") && tab.DbName.EndsWith(")"); + + if (isQuery == false) + { + if (orm.CodeFirst.IsSyncStructureToLower) tab.DbName = tab.DbName.ToLower(); + if (orm.CodeFirst.IsSyncStructureToUpper) tab.DbName = tab.DbName.ToUpper(); + } + tab.DisableSyncStructure = isQuery || dtd.DisableSyncStructure; + tab.IsDictionaryType = true; + var columnsList = new List(); + foreach (var dtdcol in dtd.Columns) + { + if (string.IsNullOrWhiteSpace(dtdcol.Name) || dtdcol.MapType == null || tab.ColumnsByCs.ContainsKey(dtdcol.Name)) continue; + var tp = common.CodeFirst.GetDbInfo(dtdcol.MapType); + var colattr = dtdcol.ToAttribute(); + var col = Utils.ColumnAttributeToInfo(tab, null, colattr.Name, colattr.MapType, false, ref colattr, tp, common); + if (col == null) continue; + + col.Comment = dtdcol.Comment; + tab.Columns.Add(col.Attribute.Name, col); + tab.ColumnsByCs.Add(col.CsName, col); + columnsList.Add(col); + } + Utils.AuditTableInfo(tab, tabattr, tabindexs, columnsList, common); + columnsList.Clear(); + tables.Add(tab); + } + var tabindex = 0; + foreach (var dtd in schemas) + { + var tab = tables[tabindex++]; + foreach (var dtdnav in dtd.Navigates) + { + if (tab.Navigates.ContainsKey(dtdnav.Name)) continue; + var error = $"表“{tab.CsName}”导航属性 {dtdnav.Name} 配置错误:"; + var nav = new ZoreTableRef(); + nav.NavigateKey = dtdnav.Name; + nav.Table = tab; + nav.RefTable = tables.Where(a => a.CsName == dtdnav.RelTable).FirstOrDefault(); + if (nav.RefTable == null) throw new Exception($"{error}未定义“{dtdnav.RelTable}”"); + + switch (dtdnav.Type) + { + case TableDescriptor.NavigateType.OneToOne: + nav.RefType = TableRefType.OneToOne; + nav.Columns.AddRange(nav.Table.Primarys.Select(a => a.CsName)); + if (string.IsNullOrWhiteSpace(dtdnav.Bind)) + nav.RefColumns.AddRange(nav.RefTable.Primarys.Select(a => a.CsName)); + else + nav.RefColumns.AddRange(dtdnav.Bind.Split(',') + .Select(a => nav.RefTable.ColumnsByCs.TryGetValue(a.Trim(), out var refcol) ? refcol.CsName : "") + .Where(a => string.IsNullOrWhiteSpace(a) == false)); + break; + case TableDescriptor.NavigateType.ManyToOne: + nav.RefType = TableRefType.ManyToOne; + nav.Columns.AddRange(dtdnav.Bind.Split(',') + .Select(a => nav.Table.ColumnsByCs.TryGetValue(a.Trim(), out var refcol) ? refcol.CsName : "") + .Where(a => string.IsNullOrWhiteSpace(a) == false)); + nav.RefColumns.AddRange(nav.RefTable.Primarys.Select(a => a.CsName)); + break; + case TableDescriptor.NavigateType.OneToMany: + nav.RefType = TableRefType.OneToMany; + nav.Columns.AddRange(nav.Table.Primarys.Select(a => a.CsName)); + nav.RefColumns.AddRange(dtdnav.Bind.Split(',') + .Select(a => nav.RefTable.ColumnsByCs.TryGetValue(a.Trim(), out var refcol) ? refcol.CsName : "") + .Where(a => string.IsNullOrWhiteSpace(a) == false)); + break; + case TableDescriptor.NavigateType.ManyToMany: + nav.RefType = TableRefType.ManyToMany; + var midtab = tables.Where(a => a.CsName == dtdnav.ManyToMany).FirstOrDefault(); + nav.RefMiddleTable = midtab; + if (nav.RefMiddleTable == null) throw new Exception($"{error}ManyToMany未定义“{dtdnav.ManyToMany}”"); + var midtabRaw = schemas.Where(a => a.Name == midtab.CsName).FirstOrDefault(); + var midTabNav1 = midtabRaw.Navigates.Where(a => a.Type == TableDescriptor.NavigateType.ManyToOne && a.RelTable == nav.Table.CsName).FirstOrDefault(); + if (midTabNav1 == null) throw new Exception($"{error}ManyToMany中间表“{dtdnav.ManyToMany}”没有与表“{nav.Table.CsName}”形成 ManyToOne 关联"); + var midTabNav1Columns = midTabNav1.Bind.Split(',') + .Select(a => midtab.ColumnsByCs.TryGetValue(a.Trim(), out var refcol) ? refcol.CsName : "") + .Where(a => string.IsNullOrWhiteSpace(a) == false).ToArray(); + var midTabNav2 = midtabRaw.Navigates.Where(a => a.Type == TableDescriptor.NavigateType.ManyToOne && a.RelTable == nav.RefTable.CsName).FirstOrDefault(); + if (midTabNav2 == null) throw new Exception($"{error}ManyToMany中间表“{dtdnav.ManyToMany}”没有与表“{nav.RefTable.CsName}”形成 ManyToOne 关联"); + var midTabNav2Columns = midTabNav2.Bind.Split(',') + .Select(a => midtab.ColumnsByCs.TryGetValue(a.Trim(), out var refcol) ? refcol.CsName : "") + .Where(a => string.IsNullOrWhiteSpace(a) == false).ToArray(); + if (midTabNav1Columns.Length != nav.Table.Primarys.Length) throw new Exception($"{error}ManyToMany中间表“{dtdnav.ManyToMany}”关联字段的数目不相等"); + if (midTabNav1Columns.Where((a, idx) => midtab.ColumnsByCs[a].CsType != nav.Table.Primarys[idx].CsType).Any()) throw new Exception($"{error}ManyToMany中间表“{dtdnav.ManyToMany}”关联字段的类型不相等"); + if (midTabNav2Columns.Length != nav.RefTable.Primarys.Length) throw new Exception($"{error}ManyToMany中间表“{dtdnav.ManyToMany}”与表“{nav.RefTable.CsName}”关联字段的数目不相等"); + if (midTabNav2Columns.Where((a, idx) => midtab.ColumnsByCs[a].CsType != nav.RefTable.Primarys[idx].CsType).Any()) throw new Exception($"{error}ManyToMany中间表“{dtdnav.ManyToMany}”与表“{nav.RefTable.CsName}”关联字段的类型不相等"); + nav.Columns.AddRange(nav.Table.Primarys.Select(a => a.CsName)); + nav.MiddleColumns.AddRange(midTabNav1Columns); + nav.MiddleColumns.AddRange(midTabNav2Columns); + nav.RefColumns.AddRange(nav.RefTable.Primarys.Select(a => a.CsName)); + break; + } + switch (dtdnav.Type) + { + case TableDescriptor.NavigateType.OneToOne: + case TableDescriptor.NavigateType.ManyToOne: + case TableDescriptor.NavigateType.OneToMany: + if (nav.Columns.Any() == false || nav.Columns.Count != nav.RefColumns.Count) throw new Exception($"{error}与表“{dtdnav.RelTable}”关联字段的数目不相等"); + if (nav.Columns.Where((a, idx) => nav.Table.ColumnsByCs[a].CsType != nav.RefTable.ColumnsByCs[nav.RefColumns[idx]].CsType).Any()) throw new Exception($"{error}与表“{dtdnav.RelTable}”关联字段的类型不匹配"); + break; + } + tab.Navigates.Add(dtdnav.Name, nav); + } + } + return tables; + } + + public ZoreDbContext WithTransaction(DbTransaction value) + { + _transaction = value; + return this; + } + public ZoreDbContext CommandTimeout(int seconds) + { + _commandTimeout = seconds; + return this; + } + void TransactionInvoke(Action handler) + { + if (_transaction == null) + { + var threadTransaction = _orm.Ado.TransactionCurrentThread; + if (threadTransaction != null) this.WithTransaction(threadTransaction); + } + if (_transaction != null) + { + handler?.Invoke(); + return; + } + using (var conn = _orm.Ado.MasterPool.Get()) + { + _transaction = conn.Value.BeginTransaction(); + var transBefore = new Aop.TraceBeforeEventArgs("BeginTransaction", null); + try + { + _orm.Aop.TraceBeforeHandler?.Invoke(this, transBefore); + handler?.Invoke(); + _transaction.Commit(); + _orm.Aop.TraceAfterHandler?.Invoke(this, new Aop.TraceAfterEventArgs(transBefore, CoreStrings.Commit, null)); + } + catch (Exception ex) + { + _transaction.Rollback(); + _orm.Aop.TraceAfterHandler?.Invoke(this, new Aop.TraceAfterEventArgs(transBefore, CoreStrings.RollBack, ex)); + throw; + } + finally + { + _transaction = null; + } + } + } + /// + /// 【有状态管理】自动 Include 查询 + /// + public SelectImpl Select => new SelectImpl(this, _tables[0].CsName).IncludeAll(); + /// + /// 【无状态管理】指定表查询 + /// + public SelectImpl SelectNoTracking(string tableName) => new SelectImpl(this, tableName).NoTracking(); + + public int Insert(T entity) => Insert(new[] { entity }); + public int Insert(IEnumerable entities) + { + _cascadeAffrows = 0; + _cascadeIgnores.Clear(); + try + { + TransactionInvoke(() => + { + AuditCascade(_tables[0], entities); + InsertCascade(_tables[0], entities, true); + }); + return _cascadeAffrows; + } + finally + { + _cascadeAffrows = 0; + _cascadeIgnores.Clear(); + } + } + public int Update(T entity) => Update(new[] { entity }); + public int Update(IEnumerable entities) + { + _cascadeAffrows = 0; + _cascadeIgnores.Clear(); + try + { + TransactionInvoke(() => + { + AuditCascade(_tables[0], entities); + UpdateCascade(_tables[0], entities, true); + }); + return _cascadeAffrows; + } + finally + { + _cascadeAffrows = 0; + _cascadeIgnores.Clear(); + } + } + public int Delete(T entity) => Delete(new[] { entity }); + public int Delete(IEnumerable entities) + { + _cascadeAffrows = 0; + _cascadeIgnores.Clear(); + try + { + TransactionInvoke(() => + { + AuditCascade(_tables[0], entities); + DeleteCascade(_tables[0], entities, null); + }); + return _cascadeAffrows; + } + finally + { + _cascadeAffrows = 0; + _cascadeIgnores.Clear(); + } + } + public void FlushState() + { + _states.Clear(); + } + public void Attach(T entity) + { + AuditCascade(_tables[0], entity); + AttachCascade(_tables[0], entity, true); + } + + void AuditCascade(ZoreTableInfo entityTable, IEnumerable entities) + { + if (entities == null) return; + foreach (var entity in entities) AuditCascade(entityTable, entity); + } + internal void AuditCascade(ZoreTableInfo entityTable, T entity) + { + var ignores = new Dictionary>(); //比如 Tree 结构可以递归添加 + LocalAuditCascade(entityTable, entity); + ignores.Clear(); + + void LocalAuditCascade(ZoreTableInfo table, T entityFrom) + { + if (entityFrom == null) return; + + var stateKey = GetEntityKeyString(table, entityFrom); + if (ignores.TryGetValue(table.DbName, out var stateKeys) == false) ignores.Add(table.DbName, stateKeys = new Dictionary()); + if (stateKey != null) + { + if (stateKeys.ContainsKey(stateKey)) return; + stateKeys.Add(stateKey, true); + } + + foreach (var col in table.Columns.Values) + if (entityFrom.TryGetValue(col.CsName, out var colval)) + entityFrom[col.CsName] = Utils.GetDataReaderValue(col.CsType, colval); + + foreach (var nav in table.Navigates) + { + if (entityFrom.TryGetValue(nav.Key, out var propvalFrom) == false || propvalFrom == null) continue; + switch (nav.Value.RefType) + { + case TableRefType.OneToOne: + { + if (propvalFrom is T valFrom == false) valFrom = LocalAuditJsonElement(nv => entityFrom[nav.Key] = nv, propvalFrom) as T; + if (valFrom == null) continue; + SetNavigateRelationshipValue(nav.Value, entityFrom, valFrom); + LocalAuditCascade(nav.Value.RefTable, valFrom); + } + break; + case TableRefType.OneToMany: + { + if (propvalFrom is IEnumerable valFromList == false) valFromList = LocalAuditJsonElement(nv => entityFrom[nav.Key] = nv, propvalFrom) as IEnumerable; + else if (valFromList != null) + { + foreach (var fromObj in valFromList) + { + if (fromObj is T == false) valFromList = LocalAuditJsonElement(nv => entityFrom[nav.Key] = nv, propvalFrom) as IEnumerable; + break; + } + } + if (valFromList == null) continue; + SetNavigateRelationshipValue(nav.Value, entityFrom, valFromList); + foreach (var fromObj in valFromList) + { + if (fromObj is T fromItem == false || fromItem == null) continue; + LocalAuditCascade(nav.Value.RefTable, fromItem); + } + } + break; + case TableRefType.ManyToMany: + { + if (propvalFrom is IEnumerable valFromList == false) valFromList = LocalAuditJsonElement(nv => entityFrom[nav.Key] = nv, propvalFrom) as IEnumerable; + else if (valFromList != null) + { + foreach (var fromObj in valFromList) + { + if (fromObj is T == false) valFromList = LocalAuditJsonElement(nv => entityFrom[nav.Key] = nv, propvalFrom) as IEnumerable; + break; + } + } + if (valFromList == null) continue; + foreach (var fromObj in valFromList) + { + if (fromObj is T fromItem == false || fromItem == null) continue; + LocalAuditCascade(nav.Value.RefTable, fromItem); + } + } + break; + case TableRefType.ManyToOne: + { + if (propvalFrom is T valFrom == false) valFrom = LocalAuditJsonElement(nv => entityFrom[nav.Key] = nv, propvalFrom) as T; + if (valFrom == null) continue; + LocalAuditCascade(nav.Value.RefTable, valFrom); + } + break; + } + } + } + + object LocalAuditJsonElement(Func resetValue, object value) + { + if (value == null) return null; + var valueType = value.GetType(); + switch (valueType.FullName) + { + case "Newtonsoft.Json.Linq.JArray": + { + if (value is IEnumerable valueEach == false) return resetValue?.Invoke(null); + var newValue = new List(); + foreach (var valueItem in valueEach) + newValue.Add(LocalAuditJsonElement(null, valueItem)); + return resetValue?.Invoke(newValue) ?? newValue; + } + case "Newtonsoft.Json.Linq.JObject": + { + if (_AuditJsonElementMethodJObjectToObject == null) + lock (_AuditJsonElementMethodLock) + if (_AuditJsonElementMethodJObjectToObject == null) + _AuditJsonElementMethodJObjectToObject = valueType.GetMethod("ToObject", new[] { typeof(Type) }); + + var newValue = _AuditJsonElementMethodJObjectToObject.Invoke(value, new object[] { typeof(T) }); + return resetValue?.Invoke(newValue) ?? newValue; + } + case "System.Text.Json.JsonElement": + { + if (_AuditJsonElementPropertyJsonElementValueKind == null) + lock (_AuditJsonElementMethodLock) + if (_AuditJsonElementPropertyJsonElementValueKind == null) + { + _AuditJsonElementPropertyJsonElementValueKind = valueType.GetProperty("ValueKind"); + _AuditJsonElementMethodJsonElementEnumerateObject = valueType.GetMethod("EnumerateObject", new Type[0]); + _AuditJsonElementMethodJsonElementEnumerateArray = valueType.GetMethod("EnumerateArray", new Type[0]); + } + + var valueKind = _AuditJsonElementPropertyJsonElementValueKind.GetValue(value, null).ToString(); + switch (valueKind) + { + case "Object": + { + var valueEach = _AuditJsonElementMethodJsonElementEnumerateObject.Invoke(value, new object[0]) as IEnumerable; + if (valueEach == null) return resetValue?.Invoke(null); + var newValue = new T(); + foreach (var valueItem in valueEach) + { + if (valueItem == null) continue; + var valueItemType = valueItem.GetType(); + if (_AuditJsonElementPropertyJsonPropertyName == null) + lock (_AuditJsonElementMethodLock) + if (_AuditJsonElementPropertyJsonPropertyName == null) + { + _AuditJsonElementPropertyJsonPropertyName = valueItemType.GetProperty("Name"); + _AuditJsonElementPropertyJsonPropertyValue = valueItemType.GetProperty("Value"); + } + + var name = _AuditJsonElementPropertyJsonPropertyName.GetValue(valueItem, null)?.ToString(); + if (name != null) newValue[name] = _AuditJsonElementPropertyJsonPropertyValue.GetValue(valueItem, null); + } + return resetValue?.Invoke(newValue) ?? newValue; + } + case "Array": + { + var valueEach = _AuditJsonElementMethodJsonElementEnumerateArray.Invoke(value, new object[0]) as IEnumerable; + if (valueEach == null) return resetValue?.Invoke(null); + var newValue = new List(); + foreach (var valueItem in valueEach) + newValue.Add(LocalAuditJsonElement(null, valueItem)); + return resetValue?.Invoke(newValue) ?? newValue; + } + } + break; + } + } + return value; + } + } + static object _AuditJsonElementMethodLock = new object(); + static MethodInfo _AuditJsonElementMethodJObjectToObject, _AuditJsonElementMethodJsonElementEnumerateObject, _AuditJsonElementMethodJsonElementEnumerateArray; + static PropertyInfo _AuditJsonElementPropertyJsonElementValueKind, _AuditJsonElementPropertyJsonPropertyName, _AuditJsonElementPropertyJsonPropertyValue; + + public class ChangeReport + { + public class ChangeInfo + { + public T Object { get; set; } + /// + /// Type = Update 的时候,获取更新之前的对象 + /// + public T BeforeObject { get; set; } + public ChangeType Type { get; set; } + public string TableName { get; set; } + } + public enum ChangeType { Insert, Update, Delete } + /// + /// 实体变化记录 + /// + public List Report { get; } = new List(); + /// + /// 实体变化事件 + /// + public Action> OnChange { get; set; } + } + internal List _changeReport = new List(); + int _cascadeAffrows = 0; + Dictionary> _cascadeIgnores = new Dictionary>(); //比如 Tree 结构可以递归添加 + Dictionary> _cascadeAuditEntityIgnores = new Dictionary>(); + bool CanCascade(TableInfo entityTable, T entity, bool isadd) + { + var stateKey = GetEntityKeyString(entityTable, entity, false); + if (stateKey == null) return true; + if (_cascadeIgnores.TryGetValue(entityTable.DbName, out var stateKeys) == false) + { + if (isadd) + { + _cascadeIgnores.Add(entityTable.DbName, stateKeys = new Dictionary()); + stateKeys.Add(stateKey, true); + } + return true; + } + if (stateKeys.ContainsKey(stateKey) == false) + { + if (isadd) stateKeys.Add(stateKey, true); + return true; + } + return false; + } + void InsertCascade(ZoreTableInfo entityTable, IEnumerable entities, bool cascade) + { + var navs = entityTable.Navigates.OrderBy(a => a.Value.RefType).ThenBy(a => a.Key).ToArray(); + SaveOutsideCascade(entities, navs); + + if (entityTable.Primarys.Any(col => col.Attribute.IsIdentity)) + { + foreach (var idcol in entityTable.Primarys.Where(col => col.Attribute.IsIdentity)) + foreach (var entity in entities) + entity.Remove(idcol.CsName); + } + LocalAddRange(entityTable, entities); + foreach (var entity in entities) + { + if (cascade == false) AttachCascade(entityTable, entity, false); + CanCascade(entityTable, entity, true); //刷新 _cascadeIgnores + } + if (cascade == false) return; + + foreach (var nav in navs) + { + switch (nav.Value.RefType) + { + case TableRefType.OneToOne: + { + var otoList = entities.Select(entity => + { + if (entity.TryGetValue(nav.Key, out var otoItemObj) == false || + otoItemObj is T otoItem == false || + otoItem == null || CanCascade(nav.Value.RefTable, otoItem, false) == false) return null; + SetNavigateRelationshipValue(nav.Value, entity, otoItem); + return otoItem; + }).Where(entity => entity != null).ToArray(); + if (otoList.Any()) + InsertCascade(nav.Value.RefTable, otoList, true); + break; + } + case TableRefType.OneToMany: + { + var otmList = entities.Select(entity => + { + if (entity.TryGetValue(nav.Key, out var otmEachObj) == false || + otmEachObj is IEnumerable otmEach == false || + otmEach == null) return null; + var otmItems = new List(); + foreach (var otmItemObj in otmEach) + { + var otmItem = otmItemObj as T; + if (otmItem == null || CanCascade(nav.Value.RefTable, otmItem, false) == false) continue; + otmItems.Add(otmItem); + } + SetNavigateRelationshipValue(nav.Value, entity, otmItems); + return otmItems; + }).Where(entity => entity != null).SelectMany(entity => entity).ToArray(); + if (otmList.Any()) + InsertCascade(nav.Value.RefTable, otmList, true); + break; + } + case TableRefType.ManyToMany: + { + var mtmMidList = new List(); + foreach (var entity in entities) + { + var mids = GetManyToManyObjects(nav.Value, entity, nav.Key); + if (mids?.Any() == true) mtmMidList.AddRange(mids); + } + if (mtmMidList.Any()) + InsertCascade(nav.Value.RefMiddleTable, mtmMidList, false); + break; + } + } + } + + foreach (var entity in entities) AttachCascade(entityTable, entity, false); + + #region LocalAdd + InsertProvider OrmInsert(TableInfo table) + { + var insertDict = _orm.Insert().WithTransaction(_transaction).CommandTimeout(_commandTimeout) as InsertProvider; + insertDict._table = table; + return insertDict; + } + void LocalAdd(TableInfo table, T data, bool isCheck, ColumnInfo[] _tableReturnColumns, ColumnInfo[] _tableIdentitys) + { + if (isCheck && LocalCanAdd(table, data, true) == false) return; + if (_tableReturnColumns == null) _tableReturnColumns = table.ColumnsByPosition.Where(a => a.Attribute.IsIdentity || string.IsNullOrWhiteSpace(a.DbInsertValue) == false).ToArray(); + if (_tableReturnColumns.Length > 0) + { + if (_tableIdentitys == null) _tableIdentitys = table.ColumnsByPosition.Where(a => a.Attribute.IsIdentity).ToArray(); + switch (_orm.Ado.DataType) + { + case DataType.SqlServer: + case DataType.OdbcSqlServer: + case DataType.CustomSqlServer: + case DataType.PostgreSQL: + case DataType.OdbcPostgreSQL: + case DataType.CustomPostgreSQL: + case DataType.KingbaseES: + case DataType.OdbcKingbaseES: + case DataType.ShenTong: + case DataType.Firebird: //firebird 只支持单条插入 returning + if (_tableIdentitys.Length == 1 && _tableReturnColumns.Length == 1) + { + var idtval = OrmInsert(table).AppendData(data).ExecuteIdentity(); + _cascadeAffrows++; + data[_tableIdentitys[0].CsName] = Utils.GetDataReaderValue(_tableIdentitys[0].CsType, idtval); + _changeReport.Add(new ChangeReport.ChangeInfo { TableName = table.DbName, Object = data, Type = ChangeReport.ChangeType.Insert }); + } + else + { + var newval = OrmInsert(table).AppendData(data).ExecuteInserted().First(); + _cascadeAffrows++; + foreach (var col in table.Columns.Values) + if (newval.TryGetValue(col.CsName, out var colval)) + data[col.CsName] = Utils.GetDataReaderValue(col.CsType, colval); + _changeReport.Add(new ChangeReport.ChangeInfo { TableName = table.DbName, Object = data, Type = ChangeReport.ChangeType.Insert }); + } + return; + default: + if (_tableIdentitys.Length == 1) + { + var idtval = OrmInsert(table).AppendData(data).ExecuteIdentity(); + _cascadeAffrows++; + data[_tableIdentitys[0].CsName] = Utils.GetDataReaderValue(_tableIdentitys[0].CsType, idtval); + _changeReport.Add(new ChangeReport.ChangeInfo { TableName = table.DbName, Object = data, Type = ChangeReport.ChangeType.Insert }); + return; + } + break; + } + } + OrmInsert(table).AppendData(data).ExecuteAffrows(); + _cascadeAffrows++; + _changeReport.Add(new ChangeReport.ChangeInfo { TableName = table.DbName, Object = data, Type = ChangeReport.ChangeType.Insert }); + } + void LocalAddRange(TableInfo table, IEnumerable data) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + if (data is List == false) data = data?.ToList(); + if (data.Any() == false) return; + foreach (var s in data) if (LocalCanAdd(table, s, true) == false) return; + + var _tableReturnColumns = table.ColumnsByPosition.Where(a => a.Attribute.IsIdentity || string.IsNullOrWhiteSpace(a.DbInsertValue) == false).ToArray(); + if (_tableReturnColumns.Length > 0) + { + var _tableIdentitys = table.ColumnsByPosition.Where(a => a.Attribute.IsIdentity).ToArray(); + switch (_orm.Ado.DataType) + { + case DataType.SqlServer: + case DataType.OdbcSqlServer: + case DataType.CustomSqlServer: + case DataType.PostgreSQL: + case DataType.OdbcPostgreSQL: + case DataType.CustomPostgreSQL: + case DataType.KingbaseES: + case DataType.OdbcKingbaseES: + case DataType.ShenTong: + var rets = OrmInsert(table).AppendData(data).ExecuteInserted(); + _cascadeAffrows += rets.Count; + if (rets.Count != data.Count()) throw new Exception($"特别错误:批量添加失败,{_orm.Ado.DataType} 的返回数据,与添加的数目不匹配"); + var idx = 0; + foreach (var s in data) + { + foreach (var col in table.Columns.Values) + if (rets[idx].TryGetValue(col.CsName, out var colval)) + s[col.CsName] = Utils.GetDataReaderValue(col.CsType, colval); + idx++; + } + _changeReport.AddRange(data.Select(a => new ChangeReport.ChangeInfo { TableName = table.DbName, Object = a, Type = ChangeReport.ChangeType.Insert })); + return; + default: + if (_tableIdentitys.Length == 1) + { + foreach (var s in data) + LocalAdd(table, s, false, _tableReturnColumns, _tableIdentitys); + return; + } + break; + } + } + _cascadeAffrows += OrmInsert(table).AppendData(data).ExecuteAffrows(); + _changeReport.AddRange(data.Select(a => new ChangeReport.ChangeInfo { TableName = table.DbName, Object = a, Type = ChangeReport.ChangeType.Insert })); + } + bool LocalCanAdd(TableInfo table, T data, bool isThrow) + { + if (data == null) + { + if (isThrow) throw new ArgumentNullException(nameof(data)); + return false; + } + if (table.Primarys.Any() == false) + { + if (isThrow) throw new Exception($"不可添加,实体没有主键:{GetEntityString(table, data)}"); + return false; + } + InsertProvider.AuditDataValue(this, data, _orm, table, null); + var key = GetEntityKeyString(table, data, true); + if (string.IsNullOrEmpty(key)) + { + switch (_orm.Ado.DataType) + { + case DataType.SqlServer: + case DataType.OdbcSqlServer: + case DataType.CustomSqlServer: + case DataType.PostgreSQL: + case DataType.OdbcPostgreSQL: + case DataType.CustomPostgreSQL: + case DataType.KingbaseES: + case DataType.OdbcKingbaseES: + case DataType.ShenTong: + case DataType.ClickHouse: + return true; + default: + if (table.Primarys.Where(a => a.Attribute.IsIdentity).Count() == 1 && table.Primarys.Length == 1) + return true; + if (isThrow) throw new Exception($"不可添加,未设置主键的值:{GetEntityString(table, data)}"); + return false; + } + } + else + { + if (_states.ContainsKey(key)) + { + if (isThrow) throw new Exception($"不可添加,已存在于状态管理:{GetEntityString(table, data)}"); + return false; + } + if (_orm.Ado.DataType == DataType.ClickHouse) return true; + var idcol = table.Primarys.FirstOrDefault(a => a.Attribute.IsIdentity); + if (table.Primarys.Any(a => a.Attribute.IsIdentity)) + { + if (idcol != null && data.TryGetValue(idcol.CsName, out var idval) && (long)idval > 0) + { + if (isThrow) throw new Exception($"不可添加,自增属性有值:{GetEntityString(table, data)}"); + return false; + } + } + } + return true; + } + #endregion + } + + void SaveOutsideCascade(IEnumerable entities, IEnumerable> navs) + { + foreach (var nav in navs) + { + List> outsideList = null; + switch (nav.Value.RefType) + { + case TableRefType.ManyToOne: + outsideList = new List>(); + foreach (var entity in entities) + { + if (entity.TryGetValue(nav.Key, out var outsideItemObj) == false || + outsideItemObj is T outsideItem == false || + outsideItem == null) continue; + outsideList.Add(NativeTuple.Create(entity, outsideItem)); + } + break; + case TableRefType.ManyToMany: + outsideList = new List>(); + foreach (var entity in entities) + { + if (entity.TryGetValue(nav.Key, out var mtmEachObj) == false || + mtmEachObj is IEnumerable mtmEach == false || + mtmEach == null) continue; + foreach (var mtmItemObj in mtmEach) + { + var mtmItem = mtmItemObj as T; + if (mtmItem == null) continue; + outsideList.Add(NativeTuple.Create(entity, mtmItem)); + } + } + break; + default: + continue; + } + SaveOutsideCascade(nav.Value.RefTable, nav.Value, outsideList); + } + } + void SaveOutsideCascade(ZoreTableInfo entityTable, ZoreTableRef nav, IEnumerable> outsideData) + { + outsideData = outsideData.Where(a => CanCascade(entityTable, a.Item2, true)).ToList(); + if (outsideData.Any() == false) return; + var exists = outsideData.Select(a => new { outside = a, exists = ExistsInStates(entityTable, a.Item2) }).ToArray(); + var existsFalse = exists.Where(a => a.exists == false).Select(a => a.outside).ToArray(); + if (existsFalse.Any()) + { + var query = new SelectImpl(this, entityTable.CsName).IncludeAll(); + var existsFalseResult = query.Where(existsFalse.Select(a => a.Item2)).ToList(); + foreach (var item in existsFalseResult) AttachCascade(entityTable, item, true); + } + var outsideListInsert = exists.Where(a => a.exists == null).Select(a => a.outside).Concat( + existsFalse.Where(a => ExistsInStates(entityTable, a.Item2) == false) + ).ToArray(); + if (outsideListInsert.Any()) + { + var list = outsideListInsert.Select(a => a.Item2).ToArray(); + InsertCascade(entityTable, list, true); //插入 + if (nav != null && nav.RefType == TableRefType.ManyToOne) + foreach (var outside in outsideListInsert) + SetNavigateRelationshipValue(nav, outside.Item1, outside.Item2); + } + var outsideListUpdate = exists.Where(a => a.exists == true).Select(a => a.outside.Item2).Concat( + existsFalse.Where(a => ExistsInStates(entityTable, a.Item2) == true).Select(a => a.Item2) + ).ToArray(); + if (outsideListUpdate.Any()) + { + UpdateCascade(entityTable, outsideListUpdate, true); //更新 + } + } + + void UpdateCascade(ZoreTableInfo entityTable, IEnumerable entities, bool cascade) + { + SaveOutsideCascade(entities, entityTable.Navigates.OrderBy(a => a.Value.RefType).ThenBy(a => a.Key)); + var tracking = new TrackingChangeInfo(); + foreach (var entity in entities) + { + var stateKey = GetEntityKeyString(entityTable, entity); + if (_states.TryGetValue(entityTable.DbName, out var kv) == false || kv.TryGetValue(stateKey, out var state) == false) throw new Exception($"{nameof(ZoreDbContext)} 查询之后,才可以更新数据 {GetEntityString(entityTable, entity)}"); + CompareEntityValue(entityTable, state.Value, entity, tracking); + } + SaveTrackingChange(tracking); + foreach (var entity in entities) AttachCascade(entityTable, entity, false); + } + void DeleteCascade(ZoreTableInfo entityTable, IEnumerable entities, List deletedOutput) + { + var tracking = new TrackingChangeInfo(); + foreach (var entity in entities) + { + var stateKey = GetEntityKeyString(entityTable, entity); + if (stateKey == null) continue; + CompareEntityValue(entityTable, entity, null, tracking); + _states.Remove(stateKey); + } + tracking.InsertLog.Clear(); + tracking.UpdateLog.Clear(); + SaveTrackingChange(tracking); + } + void SaveTrackingChange(TrackingChangeInfo tracking) + { + var insertLogDict = tracking.InsertLog.GroupBy(a => a.Item1).ToDictionary(a => a.Key, a => tracking.InsertLog.Where(b => b.Item1 == a.Key).Select(b => b.Item2).ToArray()); + foreach (var il in insertLogDict) + InsertCascade(il.Key, il.Value, true); + + for (var a = tracking.DeleteLog.Count - 1; a >= 0; a--) + { + var del = _orm.Delete().WithTransaction(_transaction).CommandTimeout(_commandTimeout).AsTable(tracking.DeleteLog[a].Item1.DbName); + var where = (del as DeleteProvider)._commonUtils.WhereItems(tracking.DeleteLog[a].Item1.Primarys, "", tracking.DeleteLog[a].Item2); + _cascadeAffrows += del.Where(where).ExecuteAffrows(); + _changeReport?.AddRange(tracking.DeleteLog[a].Item2.Select(x => + new ChangeReport.ChangeInfo + { + Type = ChangeReport.ChangeType.Delete, + TableName = tracking.DeleteLog[a].Item1.DbName, + Object = x + })); + if (_states.TryGetValue(tracking.DeleteLog[a].Item1.DbName, out var kv)) + foreach (var entity in tracking.DeleteLog[a].Item2) + kv.Remove(GetEntityKeyString(tracking.DeleteLog[a].Item1, entity)); + } + + var updateLogDict = tracking.UpdateLog.GroupBy(a => a.Item1).ToDictionary(a => a.Key, a => tracking.UpdateLog.Where(b => b.Item1 == a.Key).Select(b => new + { + BeforeObject = b.Item2, + AfterObject = b.Item3, + UpdateColumns = b.Item4, + UpdateColumnsString = string.Join(",", b.Item4.OrderBy(c => c)) + }).ToArray()); + var updateLogDict2 = updateLogDict.ToDictionary(a => a.Key, a => + a.Value.GroupBy(b => b.UpdateColumnsString).ToDictionary(b => b.Key, b => a.Value.Where(c => c.UpdateColumnsString == b.Key).ToArray())); + foreach (var dl in updateLogDict2) + { + foreach (var dl2 in dl.Value) + { + var upd = _orm.Update().WithTransaction(_transaction).CommandTimeout(_commandTimeout); + var updProvider = (upd as UpdateProvider); + updProvider._table = dl.Key; + updProvider._tempPrimarys = dl.Key?.Primarys ?? new ColumnInfo[0]; + updProvider._versionColumn = dl.Key?.VersionColumn; + _cascadeAffrows += upd + .SetSource(dl2.Value.Select(a => a.AfterObject).ToArray()) + .UpdateColumns(dl2.Value.First().UpdateColumns.ToArray()) + .ExecuteAffrows(); + _changeReport?.AddRange(dl2.Value.Select(x => + new ChangeReport.ChangeInfo + { + Type = ChangeReport.ChangeType.Update, + TableName = dl.Key.DbName, + Object = x.AfterObject, + BeforeObject = x.BeforeObject + })); + } + } + } + + #region EntityState + internal void AttachCascade(ZoreTableInfo entityTable, T entity, bool includeOutside) + { + var ignores = new Dictionary>(); //比如 Tree 结构可以递归添加 + LocalAttachCascade(entityTable, entity, true); + ignores.Clear(); + + void LocalAttachCascade(ZoreTableInfo table, T entityFrom, bool flag) + { + if (flag == false) return; + if (entityFrom == null) return; + + var key = GetEntityKeyString(table, entityFrom); + if (key == null) return; + var state = new EntityState(new T(), key); + + LocalMapEntityValue(table, entityFrom, state.Value); + + if (_states.TryGetValue(table.DbName, out var kv) == false) _states.Add(table.DbName, kv = new Dictionary()); + if (kv.ContainsKey(state.Key)) kv[state.Key] = state; + else kv.Add(state.Key, state); + } + bool LocalMapEntityValue(ZoreTableInfo table, T entityFrom, T entityTo) + { + if (entityFrom == null || entityTo == null) return true; + + var stateKey = GetEntityKeyString(table, entityFrom); + if (stateKey == null) return false; + if (ignores.TryGetValue(table.DbName, out var stateKeys) == false) ignores.Add(table.DbName, stateKeys = new Dictionary()); + if (stateKeys.ContainsKey(stateKey)) return false; + stateKeys.Add(stateKey, true); + + foreach (var col in table.Columns.Values) + if (entityFrom.TryGetValue(col.CsName, out var colval)) + entityTo[col.CsName] = colval; + + foreach (var nav in table.Navigates) + { + if (entityFrom.TryGetValue(nav.Key, out var propvalFrom) == false || propvalFrom == null) + { + entityTo.Remove(nav.Key); + continue; + } + switch (nav.Value.RefType) + { + case TableRefType.OneToOne: + { + var valFrom = propvalFrom as T; + if (valFrom == null) + { + //entityTo.Remove(nav.Key); + continue; + } + var valTo = new T(); + SetNavigateRelationshipValue(nav.Value, entityFrom, valFrom); + if (LocalMapEntityValue(nav.Value.RefTable, valFrom, valTo)) + entityTo[nav.Key] = valTo; + } + break; + case TableRefType.OneToMany: + { + var valFromList = propvalFrom as IEnumerable; + if (valFromList == null) + { + //entityTo.Remove(nav.Key); + continue; + } + SetNavigateRelationshipValue(nav.Value, entityFrom, valFromList); + var valTo = new List(); + foreach (var fromObj in valFromList) + { + if (fromObj is T fromItem == false || fromItem == null) continue; + var toItem = new T(); + if (LocalMapEntityValue(nav.Value.RefTable, fromItem, toItem)) + valTo.Add(toItem); + } + entityTo[nav.Key] = valTo; + } + break; + case TableRefType.ManyToMany: + { + var valFromList = propvalFrom as IEnumerable; + if (valFromList == null) + { + //entityTo.Remove(nav.Key); + continue; + } + var valTo = new List(); + foreach (var fromObj in valFromList) + { + if (fromObj is T fromItem == false || fromItem == null) continue; + LocalAttachCascade(nav.Value.RefTable, fromItem, includeOutside); //创建新的 states + var toItem = new T(); + foreach (var col in nav.Value.RefTable.Primarys) //多对多状态只存储 PK + if (fromItem.TryGetValue(col.CsName, out var colval)) + toItem[col.CsName] = colval; + valTo.Add(toItem); + } + entityTo[nav.Key] = valTo; + } + break; + case TableRefType.ManyToOne: + { + var valFrom = propvalFrom as T; + if (valFrom == null) + { + //entityTo.Remove(nav.Key); + continue; + } + LocalAttachCascade(nav.Value.RefTable, valFrom, includeOutside); //创建新的 states + var valTo = new T(); + foreach (var col in nav.Value.RefTable.Columns.Values) + if (valFrom.TryGetValue(col.CsName, out var colval)) + valTo[col.CsName] = colval; + entityTo[nav.Key] = valTo; + } + break; + } + } + return true; + } + } + class EntityState + { + public EntityState(T value, string key) + { + this.Value = value; + this.Key = key; + this.Time = DateTime.Now; + } + public T OldValue { get; set; } + public T Value { get; set; } + public string Key { get; set; } + public DateTime Time { get; set; } + } + Dictionary> _states = new Dictionary>(); + bool? ExistsInStates(ZoreTableInfo table, T data) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + var key = GetEntityKeyString(table, data); + if (string.IsNullOrEmpty(key)) return null; + return _states.TryGetValue(table.DbName, out var kv) && kv.ContainsKey(key); + } + string GetEntityKeyString(TableInfo table, T data, bool genGuid = false) + { + if (genGuid) + { + foreach (var col in table.Primarys) + { + if (col.CsType == typeof(Guid)) + { + if (data.TryGetValue(col.CsName, out var colval) == false || CompareEntityPropertyValue(col.CsType, colval, Guid.Empty)) + data[col.CsName] = FreeUtil.NewMongodbId(); + } + else if (col.CsType == typeof(Guid?)) + { + if (data.TryGetValue(col.CsName, out var colval) == false || CompareEntityPropertyValue(col.CsType, colval, Guid.Empty) || CompareEntityPropertyValue(col.CsType, colval, null)) + data[col.CsName] = FreeUtil.NewMongodbId(); + } + } + } + foreach (var col in table.Primarys) + { + if (col.Attribute.IsIdentity) + { + if (data.TryGetValue(col.CsName, out var colval) == false || CompareEntityPropertyValue(col.CsType, colval, col.CsType.CreateInstanceGetDefaultValue())) + return null; + } + else if (col.CsType == typeof(Guid)) + { + if (data.TryGetValue(col.CsName, out var colval) == false || CompareEntityPropertyValue(col.CsType, colval, Guid.Empty)) + return null; + } + else if (col.CsType == typeof(Guid?)) + { + if (data.TryGetValue(col.CsName, out var colval) == false || CompareEntityPropertyValue(col.CsType, colval, Guid.Empty) || CompareEntityPropertyValue(col.CsType, colval, null)) + return null; + } + } + return $"{table.DbName}:{string.Join("*|_,[,_|*", table.Primarys.Select(a => data.TryGetValue(a.Attribute.Name, out var val) ? val : null))}"; + } + string GetEntityString(TableInfo table, T data) => $"({string.Join(", ", table.ColumnsByCs.Select(a => data.TryGetValue(a.Key, out var colval) ? colval : ""))})"; + #endregion + + class TrackingChangeInfo + { + public List> InsertLog { get; } = new List>(); + public List>> UpdateLog { get; } = new List>>(); + public List> DeleteLog { get; } = new List>(); + } + void CompareEntityValue(ZoreTableInfo rootTable, T rootEntityBefore, T rootEntityAfter, TrackingChangeInfo tracking) + { + var rootIgnores = new Dictionary>(); //比如 Tree 结构可以递归添加 + LocalCompareEntityValue(rootTable, rootEntityBefore, rootEntityAfter, true); + rootIgnores.Clear(); + + void LocalCompareEntityValue(ZoreTableInfo table, T entityBefore, T entityAfter, bool cascade) + { + if (entityBefore != null) + { + var stateKey = $":before://{GetEntityKeyString(table, entityBefore)}"; + if (rootIgnores.TryGetValue(table.DbName, out var stateKeys) == false) rootIgnores.Add(table.DbName, stateKeys = new Dictionary()); + if (stateKeys.ContainsKey(stateKey)) return; + stateKeys.Add(stateKey, true); + } + if (entityAfter != null) + { + var stateKey = $":after://{GetEntityKeyString(table, entityAfter)}"; + if (rootIgnores.TryGetValue(table.DbName, out var stateKeys) == false) rootIgnores.Add(table.DbName, stateKeys = new Dictionary()); + if (stateKeys.ContainsKey(stateKey)) return; + stateKeys.Add(stateKey, true); + } + + if (entityBefore == null && entityAfter == null) return; + if (entityBefore == null && entityAfter != null) + { + tracking.InsertLog.Add(NativeTuple.Create(table, entityAfter)); + return; + } + if (entityBefore != null && entityAfter == null) + { + tracking.DeleteLog.Add(NativeTuple.Create(table, new[] { entityBefore })); + NavigateReader(table, entityBefore, (path, nav, ctb, stackvs) => + { + var dellist = (stackvs.Last() as object[] ?? new[] { stackvs.Last() }).Select(a => a as T).Where(a => a != null).ToArray(); + tracking.DeleteLog.Add(NativeTuple.Create(ctb, dellist)); + }); + return; + } + var changes = new List(); + foreach (var col in table.ColumnsByCs.Values) + { + if (col.Attribute.IsVersion) continue; + entityBefore.TryGetValue(col.CsName, out var propvalBefore); + entityAfter.TryGetValue(col.CsName, out var propvalAfter); + //if (object.Equals(propvalBefore, propvalAfter) == false) changes.Add(col.CsName); + if (CompareEntityPropertyValue(col.CsType, propvalBefore, propvalAfter) == false) changes.Add(col.CsName); + continue; + } + if (changes.Any()) tracking.UpdateLog.Add(NativeTuple.Create(table, entityBefore, entityAfter, changes)); + if (cascade == false) return; + + foreach (var nav in table.Navigates.OrderBy(a => a.Value.RefType).ThenBy(a => a.Key)) + { + entityBefore.TryGetValue(nav.Key, out var propvalBefore); + entityAfter.TryGetValue(nav.Key, out var propvalAfter); + switch (nav.Value.RefType) + { + case TableRefType.OneToOne: + SetNavigateRelationshipValue(nav.Value, entityBefore, propvalBefore); + SetNavigateRelationshipValue(nav.Value, entityAfter, propvalAfter); + LocalCompareEntityValue(nav.Value.RefTable, propvalBefore as T, propvalAfter as T, true); + break; + case TableRefType.OneToMany: + SetNavigateRelationshipValue(nav.Value, entityBefore, propvalBefore); + SetNavigateRelationshipValue(nav.Value, entityAfter, propvalAfter); + LocalCompareEntityValueCollection(nav.Value.RefTable, propvalBefore as IEnumerable, propvalAfter as IEnumerable, true); + break; + case TableRefType.ManyToMany: + var middleValuesBefore = GetManyToManyObjects(nav.Value, entityBefore, nav.Key); + var middleValuesAfter = GetManyToManyObjects(nav.Value, entityAfter, nav.Key); + LocalCompareEntityValueCollection(nav.Value.RefMiddleTable, middleValuesBefore, middleValuesAfter, false); + break; + } + } + } + void LocalCompareEntityValueCollection(ZoreTableInfo elementTable, IEnumerable collectionBefore, IEnumerable collectionAfter, bool cascade) + { + if (collectionBefore == null && collectionAfter == null) return; + if (collectionBefore == null && collectionAfter != null) + { + foreach (var itemObj in collectionAfter) + { + if (itemObj is T item == false || item == null) continue; + tracking.InsertLog.Add(NativeTuple.Create(elementTable, item)); + } + return; + } + if (collectionBefore != null && collectionAfter == null) + { + return; + } + Dictionary dictBefore = new Dictionary(); + Dictionary dictAfter = new Dictionary(); + foreach (var itemObj in collectionBefore) + { + if (itemObj is T item == false || item == null) continue; + var key = GetEntityKeyString(elementTable, item); + if (key != null) dictBefore.Add(key, item); + } + foreach (var itemObj in collectionAfter) + { + if (itemObj is T item == false || item == null) continue; + var key = GetEntityKeyString(elementTable, item); + if (key != null) + { + if (dictAfter.ContainsKey(key) == false) + dictAfter.Add(key, item); + } + else tracking.InsertLog.Add(NativeTuple.Create(elementTable, item)); + } + foreach (var key in dictBefore.Keys.ToArray()) + { + if (dictAfter.ContainsKey(key) == false) + { + var value = dictBefore[key]; + tracking.DeleteLog.Add(NativeTuple.Create(elementTable, new[] { value })); + NavigateReader(elementTable, value, (path, nav, ctb, stackvs) => + { + var dellist = (stackvs.Last() as object[] ?? new[] { stackvs.Last() }).Select(a => a as T).Where(a => a != null).ToArray(); + tracking.DeleteLog.Add(NativeTuple.Create(ctb, dellist)); + }); + dictBefore.Remove(key); + } + } + foreach (var key in dictAfter.Keys.ToArray()) + { + if (dictBefore.ContainsKey(key) == false) + { + tracking.InsertLog.Add(NativeTuple.Create(elementTable, dictAfter[key])); + dictAfter.Remove(key); + } + } + foreach (var key in dictBefore.Keys) + LocalCompareEntityValue(elementTable, dictBefore[key], dictAfter.TryGetValue(key, out var afterItem) ? afterItem : null, cascade); + } + void NavigateReader(ZoreTableInfo readerTable, T readerEntity, Action> callback) + { + var ignores = new Dictionary>(); //比如 Tree 结构可以递归添加 + var statckPath = new Stack(); + var stackValues = new List(); + statckPath.Push("_"); + stackValues.Add(readerEntity); + LocalNavigateReader(readerTable, readerEntity); + ignores.Clear(); + + void LocalNavigateReader(ZoreTableInfo table, T entity) + { + if (entity == null) return; + var stateKey = GetEntityKeyString(table, entity); + if (stateKey == null) return; + if (ignores.TryGetValue(table.DbName, out var stateKeys) == false) ignores.Add(table.DbName, stateKeys = new Dictionary()); + if (stateKeys.ContainsKey(stateKey)) return; + stateKeys.Add(stateKey, true); + + foreach (var nav in table.Navigates.OrderBy(a => a.Value.RefType).ThenBy(a => a.Key)) + { + switch (nav.Value.RefType) + { + case TableRefType.OneToOne: + if (entity.TryGetValue(nav.Key, out var propval) == false || propval == null) continue; + statckPath.Push(nav.Key); + stackValues.Add(propval); + SetNavigateRelationshipValue(nav.Value, entity, propval); + callback?.Invoke(string.Join(".", statckPath), nav.Value, nav.Value.RefTable, stackValues); + LocalNavigateReader(nav.Value.RefTable, propval as T); + stackValues.RemoveAt(stackValues.Count - 1); + statckPath.Pop(); + break; + case TableRefType.OneToMany: + if (entity.TryGetValue(nav.Key, out var propval2) == false || propval2 is IEnumerable propvalOtm == false || propvalOtm == null) continue; + SetNavigateRelationshipValue(nav.Value, entity, propvalOtm); + var propvalOtmList = new List(); + foreach (var val in propvalOtm) + propvalOtmList.Add(val); + statckPath.Push($"{nav.Key}[]"); + stackValues.Add(propvalOtmList.ToArray()); + callback?.Invoke(string.Join(".", statckPath), nav.Value, nav.Value.RefTable, stackValues); + foreach (var val in propvalOtm) + LocalNavigateReader(nav.Value.RefTable, val as T); + stackValues.RemoveAt(stackValues.Count - 1); + statckPath.Pop(); + break; + case TableRefType.ManyToMany: + var middleValues = GetManyToManyObjects(nav.Value, entity, nav.Key)?.ToArray(); + if (middleValues == null) continue; + statckPath.Push($"{nav.Key}[]"); + stackValues.Add(middleValues); + callback?.Invoke(string.Join(".", statckPath), nav.Value, nav.Value.RefMiddleTable, stackValues); + stackValues.RemoveAt(stackValues.Count - 1); + statckPath.Pop(); + break; + } + } + } + } + } + + static ConcurrentDictionary _dicCompareEntityPropertyValue = new ConcurrentDictionary + { + [typeof(string)] = true, + [typeof(DateTime)] = true, + [typeof(DateTime?)] = true, + [typeof(DateTimeOffset)] = true, + [typeof(DateTimeOffset?)] = true, + [typeof(TimeSpan)] = true, + [typeof(TimeSpan?)] = true, + }; + internal static bool CompareEntityPropertyValue(Type type, object propvalBefore, object propvalAfter) + { + if (propvalBefore == null && propvalAfter == null) return true; + if (type.IsNumberType() || + _dicCompareEntityPropertyValue.ContainsKey(type) || + type.IsEnum || + type.IsValueType || + type.NullableTypeOrThis().IsEnum) return object.Equals(propvalBefore, propvalAfter); + if (propvalBefore == null && propvalAfter != null) return false; + if (propvalBefore != null && propvalAfter == null) return false; + + if (Utils.dicExecuteArrayRowReadClassOrTuple.ContainsKey(type)) + { + if (type.FullName.StartsWith("Newtonsoft.")) + return object.Equals(propvalBefore.ToString(), propvalAfter.ToString()); + + if (typeof(IDictionary).IsAssignableFrom(type)) + { + var dictBefore = (propvalBefore as IDictionary); + var dictAfter = (propvalAfter as IDictionary); + if (dictBefore.Count != dictAfter.Count) return false; + foreach (var key in dictBefore.Keys) + { + if (dictAfter.Contains(key) == false) return false; + var valBefore = dictBefore[key]; + var valAfter = dictAfter[key]; + if (valBefore == null && valAfter == null) continue; + if (valBefore == null && valAfter != null) return false; + if (valBefore != null && valAfter == null) return false; + if (CompareEntityPropertyValue(valBefore.GetType(), valBefore, valAfter) == false) return false; + } + return true; + } + + if (type.IsArrayOrList()) + { + var enumableBefore = propvalBefore as IEnumerable; + var enumableAfter = propvalAfter as IEnumerable; + var itorBefore = enumableBefore.GetEnumerator(); + var itorAfter = enumableAfter.GetEnumerator(); + while (true) + { + var moveNextBefore = itorBefore.MoveNext(); + var moveNextAfter = itorAfter.MoveNext(); + if (moveNextBefore != moveNextAfter) return false; + if (moveNextBefore == false) break; + var currentBefore = itorBefore.Current; + var currentAfter = itorAfter.Current; + if (currentBefore == null && enumableAfter == null) continue; + if (currentBefore == null && currentAfter != null) return false; + if (currentBefore != null && currentAfter == null) return false; + if (CompareEntityPropertyValue(currentBefore.GetType(), currentBefore, currentAfter) == false) return false; + } + return true; + } + + if (type.FullName.StartsWith("System.") || + type.FullName.StartsWith("Npgsql.") || + type.FullName.StartsWith("NetTopologySuite.")) + return object.Equals(propvalBefore, propvalAfter); + + if (type.IsClass) + { + foreach (var prop in type.GetProperties()) + { + var valBefore = prop.GetValue(propvalBefore, new object[0]); + var valAfter = prop.GetValue(propvalAfter, new object[0]); + if (CompareEntityPropertyValue(prop.PropertyType, valBefore, valAfter) == false) return false; + } + return true; + } + } + return object.Equals(propvalBefore, propvalAfter); + } + + List GetManyToManyObjects(ZoreTableRef nav, T entity, string navName) + { + if (nav.RefType != TableRefType.ManyToMany) return null; + if (entity.TryGetValue(navName, out var rightsObj) == false || rightsObj is IEnumerable rights == false || rights == null) return null; + var middles = new List(); + foreach (var ritem in rights) + { + if (ritem is T right == false || right == null) continue; + var midval = new T(); + for (var x = 0; x < nav.Columns.Count; x++) + if (entity.TryGetValue(nav.Columns[x], out var colval)) + midval[nav.MiddleColumns[x]] = colval; + + for (var x = nav.Columns.Count; x < nav.MiddleColumns.Count; x++) + { + var refcol = nav.RefColumns[x - nav.Columns.Count]; + if (right.TryGetValue(refcol, out var refval) == false || + refval == nav.RefTable.ColumnsByCs[refcol].CsType.CreateInstanceGetDefaultValue()) throw new Exception($"ManyToMany 关联对象的主键属性({nav.RefTable.CsName}.{refcol})不能为空"); + midval[nav.MiddleColumns[x]] = refval; + } + middles.Add(midval); + } + return middles; + } + void SetNavigateRelationshipValue(ZoreTableRef nav, T leftItem, object rightItem) + { + switch (nav.RefType) + { + case TableRefType.OneToOne: + { + if (rightItem == null || rightItem is T rightItemDict == false || rightItemDict == null) return; + for (var idx = 0; idx < nav.Columns.Count; idx++) + { + if (leftItem.TryGetValue(nav.Columns[idx], out var colval)) + rightItemDict[nav.RefColumns[idx]] = colval; + } + } + break; + case TableRefType.OneToMany: + { + if (rightItem == null || rightItem is IEnumerable rightEachOtm == false || rightEachOtm == null) return; + foreach (var rightEle in rightEachOtm) + { + if (rightEle is T rightItemDict == false || rightItemDict == null) continue; + for (var idx = 0; idx < nav.Columns.Count; idx++) + if (leftItem.TryGetValue(nav.Columns[idx], out var colval)) + rightItemDict[nav.RefColumns[idx]] = colval; + } + } + break; + case TableRefType.ManyToOne: + for (var idx = 0; idx < nav.RefColumns.Count; idx++) + { + if (rightItem is T rightItemDict == false || rightItemDict == null) + leftItem[nav.Columns[idx]] = nav.Table.ColumnsByCs[nav.Columns[idx]].CsType.CreateInstanceGetDefaultValue(); + else if (rightItemDict.TryGetValue(nav.RefColumns[idx], out var colval)) + leftItem[nav.Columns[idx]] = colval; + } + break; + } + } + } +} diff --git a/Extensions/FreeSql.Extensions.ZoreEntity/ZoreDescriptor.cs b/Extensions/FreeSql.Extensions.ZoreEntity/ZoreDescriptor.cs new file mode 100644 index 00000000..9fec513e --- /dev/null +++ b/Extensions/FreeSql.Extensions.ZoreEntity/ZoreDescriptor.cs @@ -0,0 +1,97 @@ +using FreeSql.DataAnnotations; +using FreeSql.Internal.Model; +using System; +using System.Collections.Generic; + +namespace FreeSql.Extensions.ZoreEntity +{ + public class TableDescriptor + { + public string Name { get; set; } + public string DbName { get; set; } + public string AsTable { get; set; } + public bool DisableSyncStructure { get; set; } + public string Comment { get; set; } + public List Columns { get; } = new List(); + public List Navigates { get; } = new List(); + public List Indexes { get; } = new List(); + + public class ColumnDescriptor + { + public string Name { get; set; } + public string DbType { get; set; } + bool? _IsPrimary, _IsIdentity, _IsNullable, _IsVersion; + public bool IsPrimary { get => _IsPrimary ?? false; set => _IsPrimary = value; } + public bool IsIdentity { get => _IsIdentity ?? false; set => _IsIdentity = value; } + public bool IsNullable { get => _IsNullable ?? false; set => _IsNullable = value; } + public bool IsVersion { get => _IsVersion ?? false; set => _IsVersion = value; } + public Type MapType { get; set; } + public DateTimeKind ServerTime { get; set; } + public string InsertValueSql { get; set; } + int? _StringLength; + public int StringLength { get => _StringLength ?? 0; set => _StringLength = value; } + int? _Precision; + public int Precision { get => _Precision ?? 0; set => _Precision = value; } + int? _Scale; + public int Scale { get => _Scale ?? 0; set => _Scale = value; } + public string Comment { get; set; } + + public ColumnAttribute ToAttribute() + { + var attr = new ColumnAttribute + { + Name = Name, + DbType = DbType, + MapType = MapType, + ServerTime = ServerTime, + InsertValueSql = InsertValueSql, + }; + if (_IsPrimary != null) attr.IsPrimary = IsPrimary; + if (_IsIdentity != null) attr.IsIdentity = IsIdentity; + if (_IsNullable != null) attr.IsNullable = IsNullable; + if (_IsVersion != null) attr.IsVersion = IsVersion; + if (_StringLength != null) attr.StringLength = StringLength; + if (_Precision != null) attr.Precision = Precision; + if (_Scale != null) attr.Scale = Scale; + return attr; + } + } + public class IndexDescriptor + { + public string Name { get; set; } + public string Fields { get; set; } + public bool IsUnique { get; set; } + public IndexMethod IndexMethod { get; set; } + } + public class NavigateDescriptor + { + public string Name { get; set; } + public NavigateType Type { get; set; } + public string RelTable { get; set; } + public string Bind { get; set; } + public string ManyToMany { get; set; } + } + public enum NavigateType + { + OneToOne, ManyToOne, OneToMany, ManyToMany + } + } + + + class ZoreTableRef + { + internal string NavigateKey { get; set; } + public TableRefType RefType { get; set; } + internal ZoreTableInfo Table { get; set; } + internal ZoreTableInfo RefTable { get; set; } + internal ZoreTableInfo RefMiddleTable { get; set; } + + public List Columns { get; set; } = new List(); + public List MiddleColumns { get; set; } = new List(); + public List RefColumns { get; set; } = new List(); + } + class ZoreTableInfo : TableInfo + { + public Dictionary Navigates { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/Extensions/FreeSql.Extensions.ZoreEntity/key.snk b/Extensions/FreeSql.Extensions.ZoreEntity/key.snk new file mode 100644 index 0000000000000000000000000000000000000000..e580bc8d5d64e7c5a0c62b971545d38cfbe7d837 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096c(W3|+clf|4d2=6Xc+R`Gd@9@k@Meh} zR8`}1=JPk=q?Zlr?i$1O?SgX-{{&z z|LRF?-aWODhAO}h_7M!wz}uPXx}n-g{((r9{{%_ z4)%gVXcj;Ru@GYAIZI@e#GBtO#O5m-Qr4X_lbAV}=qNRkd0^`@I6i9k`wSe@ZPxVo zk;MXig(S-cYHE!0GWWlp7EH@E!WkF6jS+3z4rvW0%Sq;U1bq`B9*HNJjxo*23*7Vw zHyt>{2CR~8==`lYLgAmwsXPXYZ_AEAKy|PuUz0(G)L1xO{{*n6Bn?mV~QKg!055THihpc>GOh(U-NgO?4_DzY4uq!p9=Q;`G i9;v3GkC674Mbx4_b$)?7a0%Z%&zUjzbGs@!l^s|B literal 0 HcmV?d00001 diff --git a/FreeSql-lite.sln b/FreeSql-lite.sln index 5cac5dff..b19aedbc 100644 --- a/FreeSql-lite.sln +++ b/FreeSql-lite.sln @@ -89,6 +89,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FreeSql.Provider.QuestDb", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FreeSql.Provider.Xugu", "Providers\FreeSql.Provider.Xugu\FreeSql.Provider.Xugu.csproj", "{353F3732-0704-40F2-972B-036E9CC01881}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FreeSql.Extensions.ZoreEntity", "Extensions\FreeSql.Extensions.ZoreEntity\FreeSql.Extensions.ZoreEntity.csproj", "{4367B7AC-604F-4503-A1D4-643ADBFCF703}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -495,6 +497,18 @@ Global {353F3732-0704-40F2-972B-036E9CC01881}.Release|x64.Build.0 = Release|Any CPU {353F3732-0704-40F2-972B-036E9CC01881}.Release|x86.ActiveCfg = Release|Any CPU {353F3732-0704-40F2-972B-036E9CC01881}.Release|x86.Build.0 = Release|Any CPU + {4367B7AC-604F-4503-A1D4-643ADBFCF703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4367B7AC-604F-4503-A1D4-643ADBFCF703}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4367B7AC-604F-4503-A1D4-643ADBFCF703}.Debug|x64.ActiveCfg = Debug|Any CPU + {4367B7AC-604F-4503-A1D4-643ADBFCF703}.Debug|x64.Build.0 = Debug|Any CPU + {4367B7AC-604F-4503-A1D4-643ADBFCF703}.Debug|x86.ActiveCfg = Debug|Any CPU + {4367B7AC-604F-4503-A1D4-643ADBFCF703}.Debug|x86.Build.0 = Debug|Any CPU + {4367B7AC-604F-4503-A1D4-643ADBFCF703}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4367B7AC-604F-4503-A1D4-643ADBFCF703}.Release|Any CPU.Build.0 = Release|Any CPU + {4367B7AC-604F-4503-A1D4-643ADBFCF703}.Release|x64.ActiveCfg = Release|Any CPU + {4367B7AC-604F-4503-A1D4-643ADBFCF703}.Release|x64.Build.0 = Release|Any CPU + {4367B7AC-604F-4503-A1D4-643ADBFCF703}.Release|x86.ActiveCfg = Release|Any CPU + {4367B7AC-604F-4503-A1D4-643ADBFCF703}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -529,10 +543,11 @@ Global {9D7EA01A-110A-4A0C-A46B-9A0FBC88DD3D} = {94C8A78D-AA15-47B2-A348-530CD86BFC1B} {FEE501EB-60D1-4370-BC65-F939BCA7F32E} = {2A381C57-2697-427B-9F10-55DA11FD02E4} {353F3732-0704-40F2-972B-036E9CC01881} = {2A381C57-2697-427B-9F10-55DA11FD02E4} + {4367B7AC-604F-4503-A1D4-643ADBFCF703} = {4A92E8A6-9A6D-41A1-9CDA-DE10899648AA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {089687FA-5D21-40AC-BA8A-AA0D1E1H7F98} - RESX_PrefixTranslations = True RESX_NeutralResourcesLanguage = en-US + RESX_PrefixTranslations = True + SolutionGuid = {089687FA-5D21-40AC-BA8A-AA0D1E1H7F98} EndGlobalSection EndGlobal diff --git a/FreeSql/FreeSql.xml b/FreeSql/FreeSql.xml index ed01ba68..e4fdd619 100644 --- a/FreeSql/FreeSql.xml +++ b/FreeSql/FreeSql.xml @@ -1104,82 +1104,6 @@ - - - 动态创建实体类型 - - - - - 配置Class - - 类名 - 类标记的特性[Table(Name = "xxx")] [Index(xxxx)] - - - - - 配置属性 - - 属性名称 - 属性类型 - 属性标记的特性-支持多个 - - - - - 配置属性 - - 属性名称 - 属性类型 - 该属性是否重写父类属性 - 属性标记的特性-支持多个 - - - - - 配置属性 - - 属性名称 - 属性类型 - 该属性是否重写父类属性 - 属性默认值 - 属性标记的特性-支持多个 - - - - - 配置父类 - - 父类类型 - - - - - Override属性 - - - - - - Emit动态创建出Class - Type - - - - - - 首字母小写 - - - - - - - 首字母大写 - - - - 获取实体的主键值,以 "*|_,[,_|*" 分割,当任意一个主键属性无值时,返回 null @@ -5884,28 +5808,6 @@ 请使用 fsql.InsertDict(dict) 方法插入字典数据 - - - 动态构建Class Type - - - - - - 根据字典,创建 table 对应的实体对象 - - - - - - - - 根据实体对象,创建 table 对应的字典 - - - - - C#: that >= between && that <= and @@ -6422,3 +6324,115 @@ +`0})"> + + 插入数据,传入实体集合 + + + + + + + + 插入或更新数据,此功能依赖数据库特性(低版本可能不支持),参考如下: + MySql 5.6+: on duplicate key update + PostgreSQL 9.4+: on conflict do update + SqlServer 2008+: merge into + Oracle 11+: merge into + Sqlite: replace into + 达梦: merge into + 人大金仓:on conflict do update + 神通:merge into + MsAccess:不支持 + 注意区别:FreeSql.Repository 仓储也有 InsertOrUpdate 方法(不依赖数据库特性) + + + + + + + 修改数据 + + + + + + + 修改数据,传入动态条件,如:主键值 | new[]{主键值1,主键值2} | TEntity1 | new[]{TEntity1,TEntity2} | new{id=1} + + + 主键值、主键值集合、实体、实体集合、匿名对象、匿名对象集合 + + + + + 查询数据 + + + + + + + 查询数据,传入动态条件,如:主键值 | new[]{主键值1,主键值2} | TEntity1 | new[]{TEntity1,TEntity2} | new{id=1} + + + 主键值、主键值集合、实体、实体集合、匿名对象、匿名对象集合 + + + + + 删除数据 + + + + + + + 删除数据,传入动态条件,如:主键值 | new[]{主键值1,主键值2} | TEntity1 | new[]{TEntity1,TEntity2} | new{id=1} + + + 主键值、主键值集合、实体、实体集合、匿名对象、匿名对象集合 + + + + + 开启事务(不支持异步) + v1.5.0 关闭了线程事务超时自动提交的机制 + + 事务体 () => {} + + + + 开启事务(不支持异步) + v1.5.0 关闭了线程事务超时自动提交的机制 + + + 事务体 () => {} + + + + 数据库访问对象 + + + + + 所有拦截方法都在这里 + + + + + CodeFirst 模式开发相关方法 + + + + + DbFirst 模式开发相关方法 + + + + + 全局过滤设置,可默认附加为 Select/Update/Delete 条件 + + + +