/* 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.Reflection; using System.Collections; using Marr.Data.Mapping; 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 DbCommand _command; /// /// A database provider agnostic initialization. /// /// The database connection string. public DataMapper(DbProviderFactory dbProviderFactory, string connectionString) { SqlMode = SqlModes.StoredProcedure; if (dbProviderFactory == null) throw new ArgumentNullException("dbProviderFactory"); if (string.IsNullOrEmpty(connectionString)) throw new ArgumentNullException("connectionString"); ProviderFactory = dbProviderFactory; ConnectionString = connectionString; } public string ConnectionString { get; private set; } public DbProviderFactory ProviderFactory { get; private set; } /// /// Creates a new command utilizing the connection string. /// private DbCommand CreateNewCommand() { DbConnection conn = ProviderFactory.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; Parameters.Add(parameter); return parameter; } #endregion #region - SP / SQL Mode - /// /// 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; set; } /// /// 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"); 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"); 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"); Command.CommandText = sql; try { OpenConnection(); var 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"); 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 = ProviderFactory.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 = ProviderFactory.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 = ProviderFactory.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 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 = ProviderFactory.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 = ProviderFactory.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 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 = 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 = SqlMode; SqlMode = SqlModes.Text; var mappingHelper = new MappingHelper(this); if (tableName == null) { tableName = MapRepository.Instance.GetTableName(typeof(T)); } var dialect = 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) { var 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() { OpeningConnection = null; ClosingConnection = null; } public void BeginTransaction(IsolationLevel isolationLevel) { OpenConnection(); DbTransaction trans = Command.Connection.BeginTransaction(isolationLevel); 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 } }