/* Copyright (C) 2008 - 2011 Jordan Marr
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see . */
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Reflection;
using System.Collections;
using System.Linq;
using Marr.Data.Mapping;
using Marr.Data.Converters;
using Marr.Data.Parameters;
using Marr.Data.QGen;
using System.Linq.Expressions;
using System.Diagnostics;
namespace Marr.Data
{
///
/// This class is the main access point for making database related calls.
///
public class DataMapper : IDataMapper
{
#region - Contructor, Members -
private DbProviderFactory _dbProviderFactory;
private string _connectionString;
private DbCommand _command;
///
/// Initializes a DataMapper for the given provider type and connection string.
///
/// Ex:
/// The database connection string.
public DataMapper(string providerName, string connectionString)
: this(DbProviderFactories.GetFactory(providerName), connectionString)
{ }
///
/// A database provider agnostic initialization.
///
/// The database connection string.
public DataMapper(DbProviderFactory dbProviderFactory, string connectionString)
{
if (dbProviderFactory == null)
throw new ArgumentNullException("dbProviderFactory instance cannot be null.");
if (string.IsNullOrEmpty(connectionString))
throw new ArgumentNullException("connectionString cannot be null or empty.");
_dbProviderFactory = dbProviderFactory;
_connectionString = connectionString;
}
public string ConnectionString
{
get
{
return _connectionString;
}
}
public DbProviderFactory ProviderFactory
{
get
{
return _dbProviderFactory;
}
}
///
/// Creates a new command utilizing the connection string.
///
private DbCommand CreateNewCommand()
{
DbConnection conn = _dbProviderFactory.CreateConnection();
conn.ConnectionString = _connectionString;
DbCommand cmd = conn.CreateCommand();
SetSqlMode(cmd);
return cmd;
}
///
/// Creates a new command utilizing the connection string with a given SQL command.
///
private DbCommand CreateNewCommand(string sql)
{
DbCommand cmd = CreateNewCommand();
cmd.CommandText = sql;
return cmd;
}
///
/// Gets or creates a DbCommand object.
///
public DbCommand Command
{
get
{
// Lazy load
if (_command == null)
_command = CreateNewCommand();
else
SetSqlMode(_command); // Set SqlMode every time.
return _command;
}
}
#endregion
#region - Parameters -
public DbParameterCollection Parameters
{
get
{
return Command.Parameters;
}
}
public ParameterChainMethods AddParameter(string name, object value)
{
return new ParameterChainMethods(Command, name, value);
}
public IDbDataParameter AddParameter(IDbDataParameter parameter)
{
// Convert null values to DBNull.Value
if (parameter.Value == null)
parameter.Value = DBNull.Value;
this.Parameters.Add(parameter);
return parameter;
}
#endregion
#region - SP / SQL Mode -
private SqlModes _sqlMode = SqlModes.StoredProcedure; // Defaults to SP.
///
/// Gets or sets a value that determines whether the DataMapper will
/// use a stored procedure or a sql text command to access
/// the database. The default is stored procedure.
///
public SqlModes SqlMode
{
get
{
return _sqlMode;
}
set
{
_sqlMode = value;
}
}
///
/// Sets the DbCommand objects CommandType to the current SqlMode.
///
/// The DbCommand object we are modifying.
/// Returns the same DbCommand that was passed in.
private DbCommand SetSqlMode(DbCommand command)
{
if (SqlMode == SqlModes.StoredProcedure)
command.CommandType = CommandType.StoredProcedure;
else
command.CommandType = CommandType.Text;
return command;
}
#endregion
#region - ExecuteScalar, ExecuteNonQuery, ExecuteReader -
///
/// Executes a stored procedure that returns a scalar value.
///
/// The SQL command to execute.
/// A scalar value
public object ExecuteScalar(string sql)
{
if (string.IsNullOrEmpty(sql))
throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");
else
Command.CommandText = sql;
try
{
OpenConnection();
return Command.ExecuteScalar();
}
finally
{
CloseConnection();
}
}
///
/// Executes a non query that returns an integer.
///
/// The SQL command to execute.
/// An integer value
public int ExecuteNonQuery(string sql)
{
if (string.IsNullOrEmpty(sql))
throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");
else
Command.CommandText = sql;
try
{
OpenConnection();
return Command.ExecuteNonQuery();
}
finally
{
CloseConnection();
}
}
///
/// Executes a DataReader that can be controlled using a Func delegate.
/// (Note that reader.Read() will be called automatically).
///
/// The type that will be return in the result set.
/// The sql statement that will be executed.
/// The function that will build the the TResult set.
/// An IEnumerable of TResult.
public IEnumerable ExecuteReader(string sql, Func func)
{
if (string.IsNullOrEmpty(sql))
throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");
else
Command.CommandText = sql;
try
{
OpenConnection();
List list = new List();
DbDataReader reader = null;
try
{
reader = Command.ExecuteReader();
while (reader.Read())
{
list.Add(func(reader));
}
return list;
}
finally
{
if (reader != null) reader.Close();
}
}
finally
{
CloseConnection();
}
}
///
/// Executes a DataReader that can be controlled using an Action delegate.
///
/// The sql statement that will be executed.
/// The delegate that will work with the result set.
public void ExecuteReader(string sql, Action action)
{
if (string.IsNullOrEmpty(sql))
throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");
else
Command.CommandText = sql;
try
{
OpenConnection();
DbDataReader reader = null;
try
{
reader = Command.ExecuteReader();
while (reader.Read())
{
action(reader);
}
}
finally
{
if (reader != null) reader.Close();
}
}
finally
{
CloseConnection();
}
}
#endregion
#region - DataSets -
public DataSet GetDataSet(string sql)
{
return GetDataSet(sql, new DataSet(), null);
}
public DataSet GetDataSet(string sql, DataSet ds, string tableName)
{
if (string.IsNullOrEmpty(sql))
throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");
try
{
using (DbDataAdapter adapter = _dbProviderFactory.CreateDataAdapter())
{
Command.CommandText = sql;
adapter.SelectCommand = Command;
if (ds == null)
ds = new DataSet();
OpenConnection();
if (string.IsNullOrEmpty(tableName))
adapter.Fill(ds);
else
adapter.Fill(ds, tableName);
return ds;
}
}
finally
{
CloseConnection(); // Clears parameters
}
}
public DataTable GetDataTable(string sql)
{
return GetDataTable(sql, null, null);
}
public DataTable GetDataTable(string sql, DataTable dt, string tableName)
{
if (string.IsNullOrEmpty(sql))
throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");
try
{
using (DbDataAdapter adapter = _dbProviderFactory.CreateDataAdapter())
{
Command.CommandText = sql;
adapter.SelectCommand = Command;
if (dt == null)
dt = new DataTable();
adapter.Fill(dt);
if (!string.IsNullOrEmpty(tableName))
dt.TableName = tableName;
return dt;
}
}
finally
{
CloseConnection(); // Clears parameters
}
}
public int UpdateDataSet(DataSet ds, string sql)
{
if (string.IsNullOrEmpty(sql))
throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");
if (ds == null)
throw new ArgumentNullException("ds", "DataSet cannot be null.");
DbDataAdapter adapter = null;
try
{
adapter = _dbProviderFactory.CreateDataAdapter();
adapter.UpdateCommand = Command;
adapter.UpdateCommand.CommandText = sql;
return adapter.Update(ds);
}
finally
{
if (adapter.UpdateCommand != null)
adapter.UpdateCommand.Dispose();
adapter.Dispose();
}
}
public int InsertDataTable(DataTable table, string insertSP)
{
return this.InsertDataTable(table, insertSP, UpdateRowSource.None);
}
public int InsertDataTable(DataTable dt, string sql, UpdateRowSource updateRowSource)
{
if (string.IsNullOrEmpty(sql))
throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");
if (dt == null)
throw new ArgumentNullException("dt", "DataTable cannot be null.");
DbDataAdapter adapter = null;
try
{
adapter = _dbProviderFactory.CreateDataAdapter();
adapter.InsertCommand = Command;
adapter.InsertCommand.CommandText = sql;
adapter.InsertCommand.UpdatedRowSource = updateRowSource;
return adapter.Update(dt);
}
finally
{
if (adapter.InsertCommand != null)
adapter.InsertCommand.Dispose();
adapter.Dispose();
}
}
public int DeleteDataTable(DataTable dt, string sql)
{
if (string.IsNullOrEmpty(sql))
throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required");
if (dt == null)
throw new ArgumentNullException("dt", "DataSet cannot be null.");
DbDataAdapter adapter = null;
try
{
adapter = _dbProviderFactory.CreateDataAdapter();
adapter.DeleteCommand = Command;
adapter.DeleteCommand.CommandText = sql;
return adapter.Update(dt);
}
finally
{
if (adapter.DeleteCommand != null)
adapter.DeleteCommand.Dispose();
adapter.Dispose();
}
}
#endregion
#region - Find -
public T Find(string sql)
{
return this.Find(sql, default(T));
}
///
/// Returns an entity of type T.
///
/// The type of entity that is to be instantiated and loaded with values.
/// The SQL command to execute.
/// An instantiated and loaded entity of type T.
public T Find(string sql, T ent)
{
if (string.IsNullOrEmpty(sql))
throw new ArgumentNullException("sql", "A stored procedure name has not been specified for 'Find'.");
Type entityType = typeof(T);
Command.CommandText = sql;
MapRepository repository = MapRepository.Instance;
ColumnMapCollection mappings = repository.GetColumns(entityType);
bool isSimpleType = DataHelper.IsSimpleType(typeof(T));
try
{
OpenConnection();
var mappingHelper = new MappingHelper(this);
using (DbDataReader reader = Command.ExecuteReader())
{
if (reader.Read())
{
if (isSimpleType)
{
return mappingHelper.LoadSimpleValueFromFirstColumn(reader);
}
else
{
if (ent == null)
ent = (T)mappingHelper.CreateAndLoadEntity(mappings, reader, false);
else
mappingHelper.LoadExistingEntity(mappings, reader, ent, false);
}
}
}
}
finally
{
CloseConnection();
}
return ent;
}
#endregion
#region - Query -
///
/// Creates a QueryBuilder that allows you to build a query.
///
/// The type of object that will be queried.
/// Returns a QueryBuilder of T.
public QueryBuilder Query()
{
var dialect = QGen.QueryFactory.CreateDialect(this);
return new QueryBuilder(this, dialect);
}
///
/// Returns the results of a query.
/// Uses a List of type T to return the data.
///
/// Returns a list of the specified type.
public List Query(string sql)
{
return (List)Query(sql, new List());
}
///
/// Returns the results of a SP query.
///
/// Returns a list of the specified type.
public ICollection Query(string sql, ICollection entityList)
{
return Query(sql, entityList, false);
}
internal ICollection Query(string sql, ICollection entityList, bool useAltName)
{
if (entityList == null)
throw new ArgumentNullException("entityList", "ICollection instance cannot be null.");
if (string.IsNullOrEmpty(sql))
throw new ArgumentNullException("sql", "A query or stored procedure has not been specified for 'Query'.");
var mappingHelper = new MappingHelper(this);
Type entityType = typeof(T);
Command.CommandText = sql;
ColumnMapCollection mappings = MapRepository.Instance.GetColumns(entityType);
bool isSimpleType = DataHelper.IsSimpleType(typeof(T));
try
{
OpenConnection();
using (DbDataReader reader = Command.ExecuteReader())
{
while (reader.Read())
{
if (isSimpleType)
{
entityList.Add(mappingHelper.LoadSimpleValueFromFirstColumn(reader));
}
else
{
entityList.Add((T)mappingHelper.CreateAndLoadEntity(mappings, reader, useAltName));
}
}
}
}
finally
{
CloseConnection();
}
return entityList;
}
#endregion
#region - Query to Graph -
public List QueryToGraph(string sql)
{
return (List)QueryToGraph(sql, new List());
}
public ICollection QueryToGraph(string sql, ICollection entityList)
{
EntityGraph graph = new EntityGraph(typeof(T), (IList)entityList);
return QueryToGraph(sql, graph, new List());
}
///
/// Queries a view that joins multiple tables and returns an object graph.
///
///
///
///
/// Coordinates loading all objects in the graph..
///
internal ICollection QueryToGraph(string sql, EntityGraph graph, List childrenToLoad)
{
if (string.IsNullOrEmpty(sql))
throw new ArgumentNullException("sql", "sql");
var mappingHelper = new MappingHelper(this);
Type parentType = typeof(T);
Command.CommandText = sql;
try
{
OpenConnection();
using (DbDataReader reader = Command.ExecuteReader())
{
while (reader.Read())
{
// The entire EntityGraph is traversed for each record,
// and multiple entities are created from each view record.
foreach (EntityGraph lvl in graph)
{
if (lvl.IsParentReference)
{
// A child specified a circular reference to its previously loaded parent
lvl.AddParentReference();
}
else if (childrenToLoad.Count > 0 && !lvl.IsRoot && !childrenToLoad.ContainsMember(lvl.Member))
{
// A list of relationships-to-load was specified and this relationship was not included
continue;
}
else if (lvl.IsNewGroup(reader))
{
// Create a new entity with the data reader
var newEntity = mappingHelper.CreateAndLoadEntity(lvl.EntityType, lvl.Columns, reader, true);
// Add entity to the appropriate place in the object graph
lvl.AddEntity(newEntity);
}
}
}
}
}
finally
{
CloseConnection();
}
return (ICollection)graph.RootList;
}
#endregion
#region - Update -
public UpdateQueryBuilder Update()
{
return new UpdateQueryBuilder(this);
}
public int Update(T entity, Expression> filter)
{
return Update()
.Entity(entity)
.Where(filter)
.Execute();
}
public int Update(string tableName, T entity, Expression> filter)
{
return Update()
.TableName(tableName)
.Entity(entity)
.Where(filter)
.Execute();
}
public int Update(T entity, string sql)
{
return Update()
.Entity(entity)
.QueryText(sql)
.Execute();
}
#endregion
#region - Insert -
///
/// Creates an InsertQueryBuilder that allows you to build an insert statement.
/// This method gives you the flexibility to manually configure all options of your insert statement.
/// Note: You must manually call the Execute() chaining method to run the query.
///
public InsertQueryBuilder Insert()
{
return new InsertQueryBuilder(this);
}
///
/// Generates and executes an insert query for the given entity.
/// This overload will automatically run an identity query if you have mapped an auto-incrementing column,
/// and if an identity query has been implemented for your current database dialect.
///
public object Insert(T entity)
{
var columns = MapRepository.Instance.GetColumns(typeof(T));
var dialect = QueryFactory.CreateDialect(this);
var builder = Insert().Entity(entity);
// If an auto-increment column exists and this dialect has an identity query...
if (columns.Exists(c => c.ColumnInfo.IsAutoIncrement) && dialect.HasIdentityQuery)
{
builder.GetIdentity();
}
return builder.Execute();
}
///
/// Generates and executes an insert query for the given entity.
/// This overload will automatically run an identity query if you have mapped an auto-incrementing column,
/// and if an identity query has been implemented for your current database dialect.
///
public object Insert(string tableName, T entity)
{
var columns = MapRepository.Instance.GetColumns(typeof(T));
var dialect = QueryFactory.CreateDialect(this);
var builder = Insert().Entity(entity).TableName(tableName);
// If an auto-increment column exists and this dialect has an identity query...
if (columns.Exists(c => c.ColumnInfo.IsAutoIncrement) && dialect.HasIdentityQuery)
{
builder.GetIdentity();
}
return builder.Execute();
}
///
/// Executes an insert query for the given entity using the given sql insert statement.
/// This overload will automatically run an identity query if you have mapped an auto-incrementing column,
/// and if an identity query has been implemented for your current database dialect.
///
public object Insert(T entity, string sql)
{
var columns = MapRepository.Instance.GetColumns(typeof(T));
var dialect = QueryFactory.CreateDialect(this);
var builder = Insert().Entity(entity).QueryText(sql);
// If an auto-increment column exists and this dialect has an identity query...
if (columns.Exists(c => c.ColumnInfo.IsAutoIncrement) && dialect.HasIdentityQuery)
{
builder.GetIdentity();
}
return builder.Execute();
}
#endregion
#region - Delete -
public int Delete(Expression> filter)
{
return Delete(null, filter);
}
public int Delete(string tableName, Expression> filter)
{
// Remember sql mode
var previousSqlMode = this.SqlMode;
SqlMode = SqlModes.Text;
var mappingHelper = new MappingHelper(this);
if (tableName == null)
{
tableName = MapRepository.Instance.GetTableName(typeof(T));
}
var dialect = QGen.QueryFactory.CreateDialect(this);
TableCollection tables = new TableCollection();
tables.Add(new Table(typeof(T)));
var where = new WhereBuilder(Command, dialect, filter, tables, false, false);
IQuery query = QueryFactory.CreateDeleteQuery(dialect, tables[0], where.ToString());
Command.CommandText = query.Generate();
int rowsAffected = 0;
try
{
OpenConnection();
rowsAffected = Command.ExecuteNonQuery();
}
finally
{
CloseConnection();
}
// Return to previous sql mode
SqlMode = previousSqlMode;
return rowsAffected;
}
#endregion
#region - Events -
public event EventHandler OpeningConnection;
public event EventHandler ClosingConnection;
#endregion
#region - Connections / Transactions -
protected virtual void OnOpeningConnection()
{
if (OpeningConnection != null)
OpeningConnection(this, EventArgs.Empty);
}
protected virtual void OnClosingConnection()
{
WriteToTraceLog();
if (ClosingConnection != null)
ClosingConnection(this, EventArgs.Empty);
}
protected internal void OpenConnection()
{
OnOpeningConnection();
if (Command.Connection.State != ConnectionState.Open)
Command.Connection.Open();
}
protected internal void CloseConnection()
{
OnClosingConnection();
Command.Parameters.Clear();
Command.CommandText = string.Empty;
if (Command.Transaction == null)
Command.Connection.Close(); // Only close if no transaction is present
UnbindEvents();
}
private void WriteToTraceLog()
{
if (MapRepository.Instance.EnableTraceLogging)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine();
sb.AppendLine("==== Begin Query Trace ====");
sb.AppendLine();
sb.AppendLine("QUERY TYPE:");
sb.AppendLine(Command.CommandType.ToString());
sb.AppendLine();
sb.AppendLine("QUERY TEXT:");
sb.AppendLine(Command.CommandText);
sb.AppendLine();
sb.AppendLine("PARAMETERS:");
foreach (IDbDataParameter p in Parameters)
{
object val = (p.Value != null && p.Value is string) ? string.Format("\"{0}\"", p.Value) : p.Value;
sb.AppendFormat("{0} = [{1}]", p.ParameterName, val ?? "NULL").AppendLine();
}
sb.AppendLine();
sb.AppendLine("==== End Query Trace ====");
sb.AppendLine();
Trace.Write(sb.ToString());
}
}
private void UnbindEvents()
{
if (OpeningConnection != null)
OpeningConnection = null;
if (ClosingConnection != null)
ClosingConnection = null;
}
public void BeginTransaction()
{
OpenConnection();
DbTransaction trans = Command.Connection.BeginTransaction();
Command.Transaction = trans;
}
public void RollBack()
{
try
{
if (Command.Transaction != null)
Command.Transaction.Rollback();
}
finally
{
Command.Connection.Close();
}
}
public void Commit()
{
try
{
if (Command.Transaction != null)
Command.Transaction.Commit();
}
finally
{
Command.Connection.Close();
}
}
#endregion
#region - IDisposable Members -
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // In case a derived class implements a finalizer
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (Command.Transaction != null)
{
Command.Transaction.Dispose();
Command.Transaction = null;
}
if (Command.Connection != null)
{
Command.Connection.Dispose();
Command.Connection = null;
}
if (Command != null)
{
Command.Dispose();
_command = null;
}
}
}
#endregion
}
}