using FreeSql.DataAnnotations; using FreeSql.Extensions.ZeroEntity.Models; 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.ZeroEntity { /* 理解本机制之前,请先忘记 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 ZeroDbContext { internal IFreeSql _orm; internal DbTransaction _transaction; internal int _commandTimeout; internal List _tables; /// /// 创建新的ZeroDbCotext实例 /// /// IfreeSql 对象 /// 动态表结构描述 /// 是否强制同步表结构 /// Schema 未验证通过时抛出验证异常 public ZeroDbContext(IFreeSql orm, TableDescriptor[] schemas, bool syncStructure = false) { _orm = orm; _tables = ValidateSchemaToInfoInternal(orm, schemas); if (syncStructure || orm.CodeFirst.IsAutoSyncStructure) { foreach (var table in _tables) orm.CodeFirst.SyncStructure(table, table.DbName, false); } } /// /// 初始化一个 ZeroDbContext 对象,暂不指定任何Schema /// /// public ZeroDbContext(IFreeSql orm) { _orm = orm; _tables = new List(); } public SchemaValidationResult ValidateSchema(IEnumerable schemas) { try { ValidateSchemaToInfoInternal(_orm, schemas); } catch (SchemaValidationException ex) { return new SchemaValidationResult(ex.Message); } return SchemaValidationResult.SuccessedResult; } public TableInfo GetTableInfo(string name) => _tables.Where(a => a.CsName == name).FirstOrDefault(); public void SyncStructure() { foreach (var table in _tables) _orm.CodeFirst.SyncStructure(table, table.DbName, false); } public void SyncStructure(TableDescriptor[] schemas) { _tables = ValidateSchemaToInfoInternal(_orm, schemas); foreach (var table in _tables) _orm.CodeFirst.SyncStructure(table, table.DbName, false); } /// /// 同步指定表结构 /// /// public void SyncTableStructure(string name) { var table = GetTableInfo(name); _orm.CodeFirst.SyncStructure(table, table.DbName, false); } static List ValidateSchemaToInfoInternal(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 ZeroTableInfo(); 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 ZeroTableRef(); nav.NavigateKey = dtdnav.Name; nav.Table = tab; nav.RefTable = tables.Where(a => a.CsName == dtdnav.RelTable).FirstOrDefault(); if (nav.RefTable == null) throw new SchemaValidationException($"{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 SchemaValidationException($"{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 SchemaValidationException($"{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 SchemaValidationException($"{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 SchemaValidationException($"{error}ManyToMany中间表“{dtdnav.ManyToMany}”关联字段的数目不相等"); if (midTabNav1Columns.Where((a, idx) => midtab.ColumnsByCs[a].CsType != nav.Table.Primarys[idx].CsType).Any()) throw new SchemaValidationException($"{error}ManyToMany中间表“{dtdnav.ManyToMany}”关联字段的类型不相等"); if (midTabNav2Columns.Length != nav.RefTable.Primarys.Length) throw new SchemaValidationException($"{error}ManyToMany中间表“{dtdnav.ManyToMany}”与表“{nav.RefTable.CsName}”关联字段的数目不相等"); if (midTabNav2Columns.Where((a, idx) => midtab.ColumnsByCs[a].CsType != nav.RefTable.Primarys[idx].CsType).Any()) throw new SchemaValidationException($"{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 SchemaValidationException($"{error}与表“{dtdnav.RelTable}”关联字段的数目不相等"); if (nav.Columns.Where((a, idx) => nav.Table.ColumnsByCs[a].CsType != nav.RefTable.ColumnsByCs[nav.RefColumns[idx]].CsType).Any()) throw new SchemaValidationException($"{error}与表“{dtdnav.RelTable}”关联字段的类型不匹配"); break; } tab.Navigates.Add(dtdnav.Name, nav); } } return tables; } public ZeroDbContext WithTransaction(DbTransaction value) { _transaction = value; return this; } public ZeroDbContext 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(ZeroTableInfo entityTable, IEnumerable entities) { if (entities == null) return; foreach (var entity in entities) AuditCascade(entityTable, entity); } internal void AuditCascade(ZeroTableInfo entityTable, T entity) { var ignores = new Dictionary>(); //比如 Tree 结构可以递归添加 LocalAuditCascade(entityTable, entity); ignores.Clear(); void LocalAuditCascade(ZeroTableInfo 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(ZeroTableInfo 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(ZeroTableInfo entityTable, ZeroTableRef 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(ZeroTableInfo 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(ZeroDbContext)} 查询之后,才可以更新数据 {GetEntityString(entityTable, entity)}"); CompareEntityValue(entityTable, state.Value, entity, tracking); } SaveTrackingChange(tracking); foreach (var entity in entities) AttachCascade(entityTable, entity, false); } void DeleteCascade(ZeroTableInfo 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(ZeroTableInfo entityTable, T entity, bool includeOutside) { var ignores = new Dictionary>(); //比如 Tree 结构可以递归添加 LocalAttachCascade(entityTable, entity, true); ignores.Clear(); void LocalAttachCascade(ZeroTableInfo 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(ZeroTableInfo 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(ZeroTableInfo 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(ZeroTableInfo rootTable, T rootEntityBefore, T rootEntityAfter, TrackingChangeInfo tracking) { var rootIgnores = new Dictionary>(); //比如 Tree 结构可以递归添加 LocalCompareEntityValue(rootTable, rootEntityBefore, rootEntityAfter, true); rootIgnores.Clear(); void LocalCompareEntityValue(ZeroTableInfo 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(ZeroTableInfo 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(ZeroTableInfo 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(ZeroTableInfo 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(ZeroTableRef 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(ZeroTableRef 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; } } } }