2024-11-13 18:18:28 +08:00

1538 lines
81 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, object>;
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<ZeroTableInfo> _tables;
/// <summary>
/// 创建新的ZeroDbCotext实例
/// </summary>
/// <param name="orm">IfreeSql 对象</param>
/// <param name="schemas">动态表结构描述</param>
/// <param name="syncStructure">是否强制同步表结构</param>
/// <exception cref="SchemaValidationResult"> Schema 未验证通过时抛出验证异常</exception>
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);
}
}
/// <summary>
/// 初始化一个 ZeroDbContext 对象暂不指定任何Schema
/// </summary>
/// <param name="orm"></param>
public ZeroDbContext(IFreeSql orm)
{
_orm = orm;
_tables = new List<ZeroTableInfo>();
}
public SchemaValidationResult ValidateSchema(IEnumerable<TableDescriptor> 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);
}
/// <summary>
/// 同步指定表结构
/// </summary>
/// <param name="name"></param>
public void SyncTableStructure(string name)
{
var table = GetTableInfo(name);
_orm.CodeFirst.SyncStructure(table, table.DbName, false);
}
static List<ZeroTableInfo> ValidateSchemaToInfoInternal(IFreeSql orm, IEnumerable<TableDescriptor> schemas)
{
var common = (orm.Ado as AdoProvider)._util;
var tables = new List<ZeroTableInfo>();
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<ColumnInfo>();
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;
}
}
}
/// <summary>
/// 【有状态管理】自动 Include 查询
/// </summary>
public SelectImpl Select => new SelectImpl(this, _tables[0].CsName).IncludeAll();
/// <summary>
/// 【无状态管理】指定表查询
/// </summary>
public SelectImpl SelectNoTracking(string tableName) => new SelectImpl(this, tableName).NoTracking();
public int Insert(T entity) => Insert(new[] { entity });
public int Insert(IEnumerable<T> 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<T> 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<T> 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<T> entities)
{
if (entities == null) return;
foreach (var entity in entities) AuditCascade(entityTable, entity);
}
internal void AuditCascade(ZeroTableInfo entityTable, T entity)
{
var ignores = new Dictionary<string, Dictionary<string, bool>>(); //比如 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<string, bool>());
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<object, object> 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<object>();
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<object>();
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; }
/// <summary>
/// Type = Update 的时候,获取更新之前的对象
/// </summary>
public T BeforeObject { get; set; }
public ChangeType Type { get; set; }
public string TableName { get; set; }
}
public enum ChangeType { Insert, Update, Delete }
/// <summary>
/// 实体变化记录
/// </summary>
public List<ChangeInfo> Report { get; } = new List<ChangeInfo>();
/// <summary>
/// 实体变化事件
/// </summary>
public Action<List<ChangeInfo>> OnChange { get; set; }
}
internal List<ChangeReport.ChangeInfo> _changeReport = new List<ChangeReport.ChangeInfo>();
int _cascadeAffrows = 0;
Dictionary<string, Dictionary<string, bool>> _cascadeIgnores = new Dictionary<string, Dictionary<string, bool>>(); //比如 Tree 结构可以递归添加
Dictionary<string, Dictionary<string, bool>> _cascadeAuditEntityIgnores = new Dictionary<string, Dictionary<string, bool>>();
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<string, bool>());
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<T> 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<T>();
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<T>();
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<T> OrmInsert(TableInfo table)
{
var insertDict = _orm.Insert<T>().WithTransaction(_transaction).CommandTimeout(_commandTimeout) as InsertProvider<T>;
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<T> data)
{
if (data == null) throw new ArgumentNullException(nameof(data));
if (data is List<T> == 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<T>.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<T> entities, IEnumerable<KeyValuePair<string, ZeroTableRef>> navs)
{
foreach (var nav in navs)
{
List<NativeTuple<T, T>> outsideList = null;
switch (nav.Value.RefType)
{
case TableRefType.ManyToOne:
outsideList = new List<NativeTuple<T, T>>();
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<NativeTuple<T, T>>();
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<NativeTuple<T, T>> 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<T> 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<T> entities, List<object> 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<object>().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<T>().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<string, Dictionary<string, bool>>(); //比如 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<string, EntityState>());
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<string, bool>());
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<T>();
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<T>();
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<string, Dictionary<string, EntityState>> _states = new Dictionary<string, Dictionary<string, EntityState>>();
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<NativeTuple<ZeroTableInfo, T>> InsertLog { get; } = new List<NativeTuple<ZeroTableInfo, T>>();
public List<NativeTuple<ZeroTableInfo, T, T, List<string>>> UpdateLog { get; } = new List<NativeTuple<ZeroTableInfo, T, T, List<string>>>();
public List<NativeTuple<ZeroTableInfo, T[]>> DeleteLog { get; } = new List<NativeTuple<ZeroTableInfo, T[]>>();
}
void CompareEntityValue(ZeroTableInfo rootTable, T rootEntityBefore, T rootEntityAfter, TrackingChangeInfo tracking)
{
var rootIgnores = new Dictionary<string, Dictionary<string, bool>>(); //比如 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<string, bool>());
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<string, bool>());
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<string>();
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<string, T> dictBefore = new Dictionary<string, T>();
Dictionary<string, T> dictAfter = new Dictionary<string, T>();
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<string, ZeroTableRef, ZeroTableInfo, List<object>> callback)
{
var ignores = new Dictionary<string, Dictionary<string, bool>>(); //比如 Tree 结构可以递归添加
var statckPath = new Stack<string>();
var stackValues = new List<object>();
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<string, bool>());
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<object>();
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<Type, bool> _dicCompareEntityPropertyValue = new ConcurrentDictionary<Type, bool>
{
[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<T> 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<T>();
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;
}
}
}
}