using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using DOL.Database.Attributes;
using DOL.Database.Connection;
using DOL.Database.Handlers;
using DOL.Logging;
namespace DOL.Database
{
///
/// Default Object Database Base Implementation
///
public abstract class ObjectDatabase : IObjectDatabase
{
protected static readonly Logger log = LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType);
protected const long LONG_EXEC_THRESHOLD = 100;
///
/// Number Format Info to Use for Database
///
protected static readonly NumberFormatInfo Nfi = new CultureInfo("en-US", false).NumberFormat;
private static readonly ConcurrentDictionary, Array>> _castToArrayCache = new();
///
/// Data Table Handlers for this Database Handler
///
protected readonly Dictionary TableDatasets = new Dictionary();
///
/// Connection String for this Database
///
protected string ConnectionString { get; set; }
///
/// Creates a new Instance of
///
/// Database Connection String
protected ObjectDatabase(string ConnectionString)
{
this.ConnectionString = ConnectionString;
}
///
/// Helper to Retrieve Table Handler from Object Type
/// Return Real Table Handler for Modifications Queries
///
/// Object Type
/// DataTableHandler for this Object Type or null.
protected DataTableHandler GetTableHandler(Type objectType)
{
var tableName = AttributeUtil.GetTableName(objectType);
DataTableHandler handler;
return TableDatasets.TryGetValue(tableName, out handler) ? handler : null;
}
///
/// Helper to Retrieve Table or View Handler from Object Type
/// Return View or Table for Select Queries
///
/// Object Type
/// DataTableHandler for this Object Type or null.
protected DataTableHandler GetTableOrViewHandler(Type objectType)
{
var tableName = AttributeUtil.GetTableOrViewName(objectType);
DataTableHandler handler;
return TableDatasets.TryGetValue(tableName, out handler) ? handler : null;
}
#region Public Add Objects Implementation
///
/// Insert a new DataObject into the database and save it
///
/// DataObject to Add into database
/// True if the DataObject was added.
public bool AddObject(DataObject dataObject)
{
return AddObject(new [] { dataObject });
}
///
/// Insert new DataObjects into the database and save them
///
/// DataObjects to Add into database
/// True if All DataObjects were added.
public bool AddObject(IEnumerable dataObjects)
{
var success = true;
foreach (var grp in dataObjects.GroupBy(obj => obj.GetType()))
{
var tableHandler = GetTableHandler(grp.Key);
if (tableHandler == null)
{
if (log.IsErrorEnabled)
log.ErrorFormat("AddObject: DataObject Type ({0}) not registered !", grp.Key.FullName);
success = false;
continue;
}
foreach (var allowed in grp.GroupBy(item => item.AllowAdd))
{
if (allowed.Key)
{
var objs = allowed.ToArray();
var results = AddObjectImpl(tableHandler, objs);
var resultsByObjs = results.Select((result, index) => new { Success = result, DataObject = objs[index] })
.GroupBy(obj => obj.Success);
foreach (var resultGrp in resultsByObjs)
{
if (resultGrp.Key)
{
// Save in Precache if tablehandler use it
if (tableHandler.UsesPreCaching)
{
var primary = tableHandler.PrimaryKey;
if (primary != null)
{
foreach (var successObj in resultGrp.Select(obj => obj.DataObject))
tableHandler.SetPreCachedObject(primary.GetValue(successObj), successObj);
}
}
// Success Objects Need Relations Save
if (tableHandler.HasRelations)
success &= SaveObjectRelations(tableHandler, resultGrp.Select(obj => obj.DataObject));
}
else
{
if (log.IsErrorEnabled)
{
foreach(var obj in resultGrp)
log.ErrorFormat("AddObjects: DataObject ({0}) could not be inserted into database...", obj.DataObject);
}
success = false;
}
}
}
else
{
if (log.IsWarnEnabled)
{
foreach (var obj in allowed)
log.WarnFormat("AddObject: DataObject ({0}) not allowed to be added to Database", obj);
}
success = false;
}
}
}
return success;
}
#endregion
#region Public Save Objects Implementation
///
/// Saves a DataObject to database if saving is allowed and object is dirty
///
/// DataObject to Save in database
/// True is the DataObject was saved.
public bool SaveObject(DataObject dataObject)
{
return SaveObject(new [] { dataObject });
}
///
/// Save DataObjects to database if saving is allowed and object is dirty
///
/// DataObjects to Save in database
/// True if All DataObjects were saved.
public bool SaveObject(IEnumerable dataObjects)
{
var success = true;
foreach (var grp in dataObjects.GroupBy(obj => obj.GetType()))
{
var tableHandler = GetTableHandler(grp.Key);
if (tableHandler == null)
{
if (log.IsErrorEnabled)
log.ErrorFormat("SaveObject: DataObject Type ({0}) not registered !", grp.Key.FullName);
success = false;
continue;
}
var objs = grp.Where(obj => obj.Dirty).ToArray();
var results = SaveObjectImpl(tableHandler, objs);
var resultsByObjs = results.Select((result, index) => new { Success = result, DataObject = objs[index] })
.GroupBy(obj => obj.Success);
foreach (var resultGrp in resultsByObjs)
{
if (resultGrp.Key)
{
// Save in Precache if tablehandler use it
if (tableHandler.UsesPreCaching)
{
var primary = tableHandler.PrimaryKey;
if (primary != null)
{
foreach (var successObj in resultGrp.Select(obj => obj.DataObject))
tableHandler.SetPreCachedObject(primary.GetValue(successObj), successObj);
}
}
}
else
{
if (log.IsErrorEnabled)
{
foreach(var obj in resultGrp)
log.ErrorFormat("SaveObject: DataObject ({0}) could not be saved into database...", obj.DataObject);
}
success = false;
}
}
if (tableHandler.HasRelations)
success &= SaveObjectRelations(tableHandler, grp);
}
return success;
}
#endregion
#region Public Delete Objects Implementation
///
/// Delete a DataObject from database if deletion is allowed
///
/// DataObject to Delete from database
/// True if the DataObject was deleted.
public bool DeleteObject(DataObject dataObject)
{
return DeleteObject(new [] { dataObject });
}
///
/// Delete DataObjects from database if deletion is allowed
///
/// DataObjects to Delete from database
/// True if All DataObjects were deleted.
public bool DeleteObject(IEnumerable dataObjects)
{
var success = true;
foreach (var grp in dataObjects.GroupBy(obj => obj.GetType()))
{
var tableHandler = GetTableHandler(grp.Key);
if (tableHandler == null)
{
if (log.IsErrorEnabled)
log.ErrorFormat("DeleteObject: DataObject Type ({0}) not registered !", grp.Key.FullName);
success = false;
continue;
}
foreach (var allowed in grp.GroupBy(item => item.AllowDelete))
{
if (allowed.Key)
{
var objs = allowed.ToArray();
var results = DeleteObjectImpl(tableHandler, objs);
var resultsByObjs = results.Select((result, index) => new { Success = result, DataObject = objs[index] })
.GroupBy(obj => obj.Success);
foreach (var resultGrp in resultsByObjs)
{
if (resultGrp.Key)
{
// Delete in Precache if tablehandler use it
if (tableHandler.UsesPreCaching)
{
var primary = tableHandler.PrimaryKey;
if (primary != null)
{
foreach (var successObj in resultGrp.Select(obj => obj.DataObject))
tableHandler.DeletePreCachedObject(primary.GetValue(successObj));
}
}
// Success Objects Need to check Relations that should be deleted
if (tableHandler.HasRelations)
success &= DeleteObjectRelations(tableHandler, resultGrp.Select(obj => obj.DataObject));
}
else
{
if (log.IsErrorEnabled)
{
foreach(var obj in resultGrp)
log.ErrorFormat("DeleteObject: DataObject ({0}) could not be deleted from database...", obj.DataObject);
}
success = false;
}
}
}
else
{
if (log.IsWarnEnabled)
{
foreach (var obj in allowed)
log.WarnFormat("DeleteObject: DataObject ({0}) not allowed to be deleted from Database", obj);
}
success = false;
}
}
}
return success;
}
#endregion
#region Relation Update Handling
///
/// Save Relations Objects attached to DataObjects
///
/// TableHandler for Source DataObjects Relation
/// DataObjects to parse
/// True if all Relations were saved
protected bool SaveObjectRelations(DataTableHandler tableHandler, IEnumerable dataObjects)
{
var success = true;
foreach (var relation in tableHandler.ElementBindings.Where(bind => bind.Relation != null))
{
// Relation Check
var remoteHandler = GetTableHandler(relation.ValueType);
if (remoteHandler == null)
{
if (log.IsErrorEnabled)
log.ErrorFormat("SaveObjectRelations: Remote Table for Type ({0}) is not registered !", relation.ValueType.FullName);
success = false;
continue;
}
// Check For Array Type
var groups = relation.ValueType.HasElementType
? dataObjects.Select(obj => new { Source = obj, Enumerable = (IEnumerable)relation.GetValue(obj) })
.Where(obj => obj.Enumerable != null).Select(obj => obj.Enumerable.Select(rel => new { Local = obj.Source, Remote = rel }))
.SelectMany(obj => obj).Where(obj => obj.Remote != null).GroupBy(obj => obj.Remote.IsPersisted)
: dataObjects.Select(obj => new { Local = obj, Remote = (DataObject)relation.GetValue(obj) }).Where(obj => obj.Remote != null).GroupBy(obj => obj.Remote.IsPersisted);
foreach (var grp in groups)
{
// Group by object that can be added or saved
foreach (var allowed in grp.GroupBy(obj => grp.Key ? obj.Remote.Dirty : obj.Remote.AllowAdd))
{
if (allowed.Key)
{
var objs = allowed.ToArray();
var results = grp.Key ? SaveObjectImpl(remoteHandler, objs.Select(obj => obj.Remote)) : AddObjectImpl(remoteHandler, objs.Select(obj => obj.Remote));
var resultsByObjs = results.Select((result, index) => new { Success = result, RelObject = objs[index] });
foreach (var resultGrp in resultsByObjs.GroupBy(obj => obj.Success))
{
if (resultGrp.Key)
{
// Update in Precache if tablehandler use it
if (remoteHandler.UsesPreCaching)
{
var primary = remoteHandler.PrimaryKey;
if (primary != null)
{
foreach (var successObj in resultGrp.Select(obj => obj.RelObject.Remote))
remoteHandler.SetPreCachedObject(primary.GetValue(successObj), successObj);
}
}
}
else
{
if (log.IsErrorEnabled)
{
foreach (var result in resultGrp)
log.ErrorFormat("SaveObjectRelations: {0} Relation ({1}) of DataObject ({2}) failed for Object ({3})", grp.Key ? "Saving" : "Adding",
relation.ValueType, result.RelObject.Local, result.RelObject.Remote);
}
success = false;
}
}
}
else
{
// Objects that could not be added can lead to failure
if (!grp.Key)
{
if (log.IsWarnEnabled)
{
foreach (var obj in allowed)
log.WarnFormat("SaveObjectRelations: DataObject ({0}) not allowed to be added to Database", obj);
}
success = false;
}
}
}
}
}
return success;
}
///
/// Delete Relations Objects attached to DataObjects
///
/// TableHandler for Source DataObjects Relation
/// DataObjects to parse
/// True if all Relations were deleted
public bool DeleteObjectRelations(DataTableHandler tableHandler, IEnumerable dataObjects)
{
var success = true;
foreach (var relation in tableHandler.ElementBindings.Where(bind => bind.Relation != null && bind.Relation.AutoDelete))
{
// Relation Check
var remoteHandler = GetTableHandler(relation.ValueType);
if (remoteHandler == null)
{
if (log.IsErrorEnabled)
log.ErrorFormat("DeleteObjectRelations: Remote Table for Type ({0}) is not registered !", relation.ValueType.FullName);
success = false;
continue;
}
// Check For Array Type
var groups = relation.ValueType.HasElementType
? dataObjects.Select(obj => new { Source = obj, Enumerable = (IEnumerable)relation.GetValue(obj) })
.Where(obj => obj.Enumerable != null).Select(obj => obj.Enumerable.Select(rel => new { Local = obj.Source, Remote = rel }))
.SelectMany(obj => obj).Where(obj => obj.Remote != null && obj.Remote.IsPersisted)
: dataObjects.Select(obj => new { Local = obj, Remote = (DataObject)relation.GetValue(obj) }).Where(obj => obj.Remote != null && obj.Remote.IsPersisted);
foreach (var grp in groups.GroupBy(obj => obj.Remote.AllowDelete))
{
if (grp.Key)
{
var objs = grp.ToArray();
var results = DeleteObjectImpl(remoteHandler, objs.Select(obj => obj.Remote));
var resultsByObjs = results.Select((result, index) => new { Success = result, RelObject = objs[index] });
foreach (var resultGrp in resultsByObjs.GroupBy(obj => obj.Success))
{
if (resultGrp.Key)
{
// Delete in Precache if tablehandler use it
if (remoteHandler.UsesPreCaching)
{
var primary = remoteHandler.PrimaryKey;
if (primary != null)
{
foreach (var successObj in resultGrp.Select(obj => obj.RelObject.Remote))
remoteHandler.DeletePreCachedObject(primary.GetValue(successObj));
}
}
}
else
{
foreach (var result in resultGrp)
{
if (log.IsErrorEnabled)
log.ErrorFormat("DeleteObjectRelations: Deleting Relation ({0}) of DataObject ({1}) failed for Object ({2})",
relation.ValueType, result.RelObject.Local, result.RelObject.Remote);
}
success = false;
}
}
}
else
{
// Objects that could not be deleted can lead to failure
if (log.IsWarnEnabled)
{
foreach (var obj in grp)
log.WarnFormat("DeleteObjectRelations: DataObject ({0}) not allowed to be deleted from Database", obj);
}
success = false;
}
}
}
return success;
}
#endregion
#region Relation Select/Fill Handling
///
/// Populate or Refresh Objects Relations
///
/// Objects to Populate
public void FillObjectRelations(IEnumerable dataObjects)
{
// Interface Call, force Refresh
FillObjectRelations(dataObjects, true);
}
///
/// Populate or Refresh Object Relations
///
/// Object to Populate
public void FillObjectRelations(DataObject dataObject)
{
// Interface Call, force Refresh
FillObjectRelations(new [] { dataObject }, true);
}
///
/// Populate or Refresh Objects Relations
///
/// Objects to Populate
/// Force Refresh even if Autoload is False
protected virtual void FillObjectRelations(IEnumerable dataObjects, bool force)
{
var groups = dataObjects.GroupBy(obj => obj.GetType());
foreach (var grp in groups)
{
var dataType = grp.Key;
var tableName = AttributeUtil.GetTableOrViewName(dataType);
try
{
if (!TableDatasets.TryGetValue(tableName, out DataTableHandler tableHandler))
throw new DatabaseException(string.Format("Table {0} is not registered for Database Connection...", tableName));
if (!tableHandler.HasRelations)
{
TakeSnapshots(grp);
continue;
}
var relations = tableHandler.ElementBindings.Where(bind => bind.Relation != null);
foreach (var relation in relations)
{
// Check if Loading is needed
if (!(relation.Relation.AutoLoad || force))
continue;
var remoteName = AttributeUtil.GetTableOrViewName(relation.ValueType);
try
{
DataTableHandler remoteHandler;
if (!TableDatasets.TryGetValue(remoteName, out remoteHandler))
throw new DatabaseException(string.Format("Table {0} is not registered for Database Connection...", remoteName));
// Select Object On Relation Constraint
var localBind = tableHandler.FieldElementBindings.Single(bind => bind.ColumnName.Equals(relation.Relation.LocalField, StringComparison.OrdinalIgnoreCase));
var remoteBind = remoteHandler.FieldElementBindings.Single(bind => bind.ColumnName.Equals(relation.Relation.RemoteField, StringComparison.OrdinalIgnoreCase));
FillObjectRelationsImpl(relation, localBind, remoteBind, remoteHandler, grp);
}
catch (Exception re)
{
if (log.IsErrorEnabled)
log.ErrorFormat("Could not Retrieve Objects from Relation (Table {0}, Local {1}, Remote Table {2}, Remote {3})\n{4}", tableName,
relation.Relation.LocalField, AttributeUtil.GetTableOrViewName(relation.ValueType), relation.Relation.RemoteField, re);
}
}
TakeSnapshots(grp);
}
catch (Exception e)
{
if (log.IsErrorEnabled)
log.ErrorFormat("Could not Resolve Relations for Table {0}\n{1}", tableName, e);
}
}
static void TakeSnapshots(IGrouping group)
{
foreach (DataObject dataObject in group)
dataObject.TakeSnapshot();
}
}
///
/// Populate or Refresh Object Relation Implementation
///
/// Element Binding for Relation Field
/// Local Binding for Value Match
/// Remote Binding for Column Match
/// Remote Table Handler for Cache Retrieving
/// DataObjects to Populate
protected virtual void FillObjectRelationsImpl(ElementBinding relationBind, ElementBinding localBind, ElementBinding remoteBind, DataTableHandler remoteHandler, IEnumerable dataObjects)
{
Type type = relationBind.ValueType;
bool isElementType = false;
if (type.HasElementType)
{
type = type.GetElementType();
isElementType = true;
}
if (remoteHandler.UsesPreCaching)
{
// This Select directly corresponds to each item in dataObjects, maintaining order.
var objsResults = dataObjects.Select(obj =>
{
if (remoteHandler.PrimaryKeys.All(pk => pk.ColumnName.Equals(remoteBind.ColumnName, StringComparison.OrdinalIgnoreCase)))
{
object local = localBind.GetValue(obj);
if (local == null)
return Enumerable.Empty();
DataObject retrieve = remoteHandler.GetPreCachedObject(local);
if (retrieve == null)
return Enumerable.Empty();
return [retrieve];
}
else
{
return remoteHandler.SearchPreCachedObjects(rem =>
{
object local = localBind.GetValue(obj);
object remote = remoteBind.GetValue(rem);
if (local == null || remote == null)
return false;
if (localBind.ValueType == typeof(string) || remoteBind.ValueType == typeof(string))
return remote.ToString().Equals(local.ToString(), StringComparison.OrdinalIgnoreCase);
return remote.Equals(local);
});
}
});
var resultByObjsFromCache = dataObjects.Zip(objsResults, (dataObj, results) => new { DataObject = dataObj, Results = results });
AssignRelations(resultByObjsFromCache, isElementType, type, relationBind);
FillObjectRelations(resultByObjsFromCache.SelectMany(result => result.Results), false);
return;
}
List