// ======================================================== #undef DEBUG
namespace Kerosene.ORM.Maps
{
using Kerosene.ORM.Core.Concrete;
using Kerosene.Tools;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Transactions;
// ====================================================
public static partial class KMap
{
/// <summary>
/// Reloads the given entity after the change operations have been executed.
/// </summary>
internal static void Reload(KMetaEntity meta)
{
object entity = meta.Entity; if (entity == null) return;
if (meta.MetaMap == null) return;
var cmd = meta.MetaMap.Query();
cmd.Top(1);
var tag = new DynamicNode.Argument("x");
var id = new KRecordBase(meta.MetaMapInternal.IdSchema);
meta.MetaMapInternal.WriteRecord(meta.Entity, id);
for (int i = 0; i < id.Count; i++)
{
var left = new DynamicNode.GetMember(tag, id.Schema[i].ColumnName);
var bin = new DynamicNode.Binary(left, ExpressionType.Equal, id[i]);
cmd.Where(x => bin);
bin.Dispose();
left.Dispose();
}
tag.Dispose();
id.Dispose();
object obj = cmd.First(); cmd.Dispose(); cmd = null;
if (obj != null && !object.ReferenceEquals(obj, entity))
{
var temp = KMetaEntity.Locate(obj);
meta.Record = temp.Record.Clone();
meta.MetaMapInternal.LoadEntity(meta.Record, entity);
meta.MetaMapInternal.CompleteEntity(meta);
}
}
/// <summary>
/// Executes the given action per each extended member in a change operation context.
/// </summary>
/// <param name="meta">The hosting entity.</param>
/// <param name="mode">The current change mode, to filter out the extended members whose mode does not match.</param>
/// <param name="action">The action to execute with the value of the extended member, or with its members in case
/// it is a child-alike object.</param>
internal static void DependencyLoop(KMetaEntity meta, KMapExtendedMemberMode mode, Action<object> action)
{
var entity = meta.Entity; if (entity == null) return;
var map = meta.MetaMap; if (map == null) return;
foreach (var xmember in map.ExtendedMembers)
{
if (xmember.Mode != mode) continue;
var value = xmember.ElementInfo.GetValue(entity); if (value == null) continue;
var type = xmember.ElementInfo.ElementType;
var isList = KMap.IsTypeIList(type) || KMap.IsTypeIListGenericT(type);
if (!isList) action(value);
else
{
var list = (IEnumerable)value;
var values = new List<object>(); foreach (var item in list) if (item != null) values.Add(item);
foreach (var item in values) action(item);
values.Clear(); values = null;
}
}
}
/// <summary>
/// Marks to refresh or submit to update the dependent (parent or child) meta from the host meta given, in
/// the context of an insert or update operation of the host meta, where the dependency meta was found in a
/// ready state.
/// </summary>
internal static void MarkToRefreshOrUpdate(KMetaEntity dependentMeta, KMetaEntity hostMeta)
{
// The easy case, when the dependent entity has changes per-se...
if (HasRecordChanges(dependentMeta))
{
dependentMeta.MetaMap.Update(dependentMeta.Entity).Submit();
return;
}
var link = dependentMeta.MetaLink.Link;
// Finding whether any child of the dependent entity (except from the source host) has changes...
DependencyLoop(dependentMeta, KMapExtendedMemberMode.Child, child =>
{
var childMeta = KMetaEntity.Locate(child, raise: false);
if (childMeta == null) return;
if (object.ReferenceEquals(childMeta, hostMeta)) return;
var childType = child.GetType();
IKMetaMap map = null;
switch (childMeta.State)
{
case KMetaState.Detached:
map = link.Map(childType);
if (map == null) map = link.Map(childType.BaseType);
if (map == null) throw new NotFoundException(string.Format("No map found for type '{0}' on link '{1}'.", childType.EasyName(), link));
map.Insert(child).Submit();
break;
case KMetaState.Ready:
if (HasRecordChanges(childMeta)) childMeta.MetaMap.Update(child).Submit(); // Limited to changes on the child it-self, no infinite cascade
break;
}
});
// In any case at least we want the dependent one to be marked to refresh...
dependentMeta.ToRefresh = true;
}
}
// ==================================================== Unit of Work pattern support
public partial class KMetaLink
{
/// <summary>
/// Gets and sets whether to use transactions when submitting changes or not.
/// <para>Its default value is yes.</para>
/// <para>The setter is provided to support debug and other specialized scenarios.</para>
/// </summary>
public bool UseTransactionsWhenSubmitChanges
{
get { return _UseTransactionsWhenSubmitChanges; }
set { _UseTransactionsWhenSubmitChanges = value; }
}
/// <summary>
/// Submits to the database all the pending changes annotated on the entities governed by this meta link.
/// </summary>
public void SubmitChanges()
{
SubmitChanges(UseTransactionsWhenSubmitChanges);
}
/// <summary>
/// Submits to the database all the pending changes annotated on the entities governed by this meta link,
/// optionally using a transaction.
/// </summary>
/// <param name="withTransaction">Whether to use a transaction to cover all the changes submitted to the
/// database by this operation or not.</param>
public void SubmitChanges(bool withTransaction)
{
if (IsDisposed) throw new ObjectDisposedException(this.ToString());
bool underTx = false;
List<KMetaEntity> list = null;
List<KMetaEntity> pending = null;
try
{
DebugHelper.IndentWriteLine("\n- Executing changes for '{0}'...", this);
if (withTransaction) Link.Transaction.Start();
if (Transaction.Current != null || Link.Transaction.IsActive) underTx = true;
WithMetaEntitiesLock(() =>
{
list = MetaEntities.FindAll(x =>
x.Entity != null && x.MetaMap != null &&
x.MetaOperation != null && !x.MetaOperation.Executed);
foreach (var meta in list)
{
try
{
DebugHelper.IndentWriteLine("\n- Executing '{0}'...", meta);
if(!meta.MetaOperation.Executed) meta.MetaOperationInternal.OnExecute();
}
catch (Exception e) { throw e; }
finally { DebugHelper.Unindent(); }
}
if (withTransaction) Link.Transaction.Commit();
DebugHelper.WriteLine("\n- Finalizing changes for '{0}'...", this);
foreach (var meta in list) meta.MetaOperationInternal.OnFinalize();
DebugHelper.WriteLine("\n- Reloading pendings for '{0}'...", this);
pending = MetaEntities.FindAll(x =>
x.Entity != null && x.MetaMap != null &&
x.ToRefresh);
foreach (var meta in pending) { meta.Completed = false; KMap.ResetProxyObtainedFlags(meta); }
foreach (var meta in pending) KMap.Reload(meta);
});
}
catch (Exception e)
{
DebugHelper.WriteLine("\n- Error: {0}.\n", e.ExtendedString());
if (withTransaction) Link.Transaction.Abort();
if (list != null)
{
DebugHelper.IndentWriteLine("\n- Reverting changes for '{0}'...", this);
list.Reverse(); foreach (var meta in list)
{
try
{
DebugHelper.IndentWriteLine("\n- Reverting '{0}'...", meta);
meta.MetaOperationInternal.OnRevert(underTx);
}
catch (Exception r)
{
DebugHelper.IndentWriteLine("\n- Error reverting '{0}'...", r.ExtendedString());
DebugHelper.Unindent();
}
finally { DebugHelper.Unindent(); }
}
DebugHelper.Unindent();
}
throw e;
}
finally
{
if (pending != null) pending.Clear(); pending = null;
if (list != null) list.Clear(); list = null;
DiscardChangesLoop();
DebugHelper.Unindent();
}
}
/// <summary>
/// Discards all the pending changes annotated on the entities governed by this meta link.
/// </summary>
public void DiscardChanges()
{
if (IsDisposed) throw new ObjectDisposedException(this.ToString());
DebugHelper.IndentWriteLine("\n- Discarding changes for '{0}'...", this);
DiscardChangesLoop();
DebugHelper.Unindent();
}
void DiscardChangesLoop()
{
WithMetaEntitiesLock(() =>
{
var list = MetaEntities.FindAll(x => x.MetaOperation != null || x.ToRefresh);
foreach (var meta in list)
{
if (meta.MetaOperation != null) meta.MetaOperation.Dispose(); meta.MetaOperationInternal = null;
meta.ToRefresh = false;
}
});
}
}
}
// ========================================================