I'm using the Save() method to insert or update records, but I would like to make it perform a bulk insert and bulk update with only one database hit. How do I do this?
In my case, I took advantage of the database.Execute() method.
I created a SQL parameter that had the first part of my insert:
var sql = new Sql("insert into myTable(Name, Age, Gender) values");
for (int i = 0; i < pocos.Count ; ++i)
{
var p = pocos[i];
sql.Append("(#0, #1, #2)", p.Name, p.Age , p.Gender);
if(i != pocos.Count -1)
sql.Append(",");
}
Database.Execute(sql);
I tried two different methods for inserting a large quantity of rows faster than the default Insert (which is pretty slow when you have a lot of rows).
1) Making up a List<T> with the poco's first and then inserting them at once within a loop (and in a transaction):
using (var tr = PetaPocoDb.GetTransaction())
{
foreach (var record in listOfRecords)
{
PetaPocoDb.Insert(record);
}
tr.Complete();
}
2) SqlBulkCopy a DataTable:
var bulkCopy = new SqlBulkCopy(connectionString, SqlBulkCopyOptions.TableLock);
bulkCopy.DestinationTableName = "SomeTable";
bulkCopy.WriteToServer(dt);
To get my List <T> to a DataTable I used Marc Gravells Convert generic List/Enumerable to DataTable? function which worked ootb for me (after I rearranged the Poco properties to be in the exact same order as the table fields in the db.)
The SqlBulkCopy was fastest, 50% or so faster than the transactions method in the (quick) perf tests I did with ~1000 rows.
Hth
Insert in one SQL query is much faster.
Here is a customer method for PetaPoco.Database class that adds ability to do a bulk insert of any collection:
public void BulkInsertRecords<T>(IEnumerable<T> collection)
{
try
{
OpenSharedConnection();
using (var cmd = CreateCommand(_sharedConnection, ""))
{
var pd = Database.PocoData.ForType(typeof(T));
var tableName = EscapeTableName(pd.TableInfo.TableName);
string cols = string.Join(", ", (from c in pd.QueryColumns select tableName + "." + EscapeSqlIdentifier(c)).ToArray());
var pocoValues = new List<string>();
var index = 0;
foreach (var poco in collection)
{
var values = new List<string>();
foreach (var i in pd.Columns)
{
values.Add(string.Format("{0}{1}", _paramPrefix, index++));
AddParam(cmd, i.Value.GetValue(poco), _paramPrefix);
}
pocoValues.Add("(" + string.Join(",", values.ToArray()) + ")");
}
var sql = string.Format("INSERT INTO {0} ({1}) VALUES {2}", tableName, cols, string.Join(", ", pocoValues));
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
}
}
finally
{
CloseSharedConnection();
}
}
Here is the updated verision of Steve Jansen answer that splits in chuncs of maximum 2100 pacos
I commented out the following code as it produces duplicates in the database...
//using (var reader = cmd.ExecuteReader())
//{
// while (reader.Read())
// {
// inserted.Add(reader[0]);
// }
//}
Updated Code
/// <summary>
/// Performs an SQL Insert against a collection of pocos
/// </summary>
/// <param name="pocos">A collection of POCO objects that specifies the column values to be inserted. Assumes that every POCO is of the same type.</param>
/// <returns>An array of the auto allocated primary key of the new record, or null for non-auto-increment tables</returns>
public object BulkInsert(IEnumerable<object> pocos)
{
Sql sql;
IList<PocoColumn> columns = new List<PocoColumn>();
IList<object> parameters;
IList<object> inserted;
PocoData pd;
Type primaryKeyType;
object template;
string commandText;
string tableName;
string primaryKeyName;
bool autoIncrement;
int maxBulkInsert;
if (null == pocos)
{
return new object[] { };
}
template = pocos.First<object>();
if (null == template)
{
return null;
}
pd = PocoData.ForType(template.GetType());
tableName = pd.TableInfo.TableName;
primaryKeyName = pd.TableInfo.PrimaryKey;
autoIncrement = pd.TableInfo.AutoIncrement;
//Calculate the maximum chunk size
maxBulkInsert = 2100 / pd.Columns.Count;
IEnumerable<object> pacosToInsert = pocos.Take(maxBulkInsert);
IEnumerable<object> pacosremaining = pocos.Skip(maxBulkInsert);
try
{
OpenSharedConnection();
try
{
var names = new List<string>();
var values = new List<string>();
var index = 0;
foreach (var i in pd.Columns)
{
// Don't insert result columns
if (i.Value.ResultColumn)
continue;
// Don't insert the primary key (except under oracle where we need bring in the next sequence value)
if (autoIncrement && primaryKeyName != null && string.Compare(i.Key, primaryKeyName, true) == 0)
{
primaryKeyType = i.Value.PropertyInfo.PropertyType;
// Setup auto increment expression
string autoIncExpression = _dbType.GetAutoIncrementExpression(pd.TableInfo);
if (autoIncExpression != null)
{
names.Add(i.Key);
values.Add(autoIncExpression);
}
continue;
}
names.Add(_dbType.EscapeSqlIdentifier(i.Key));
values.Add(string.Format("{0}{1}", _paramPrefix, index++));
columns.Add(i.Value);
}
string outputClause = String.Empty;
if (autoIncrement)
{
outputClause = _dbType.GetInsertOutputClause(primaryKeyName);
}
commandText = string.Format("INSERT INTO {0} ({1}){2} VALUES",
_dbType.EscapeTableName(tableName),
string.Join(",", names.ToArray()),
outputClause
);
sql = new Sql(commandText);
parameters = new List<object>();
string valuesText = string.Concat("(", string.Join(",", values.ToArray()), ")");
bool isFirstPoco = true;
var parameterCounter = 0;
foreach (object poco in pacosToInsert)
{
parameterCounter++;
parameters.Clear();
foreach (PocoColumn column in columns)
{
parameters.Add(column.GetValue(poco));
}
sql.Append(valuesText, parameters.ToArray<object>());
if (isFirstPoco && pocos.Count() > 1)
{
valuesText = "," + valuesText;
isFirstPoco = false;
}
}
inserted = new List<object>();
using (var cmd = CreateCommand(_sharedConnection, sql.SQL, sql.Arguments))
{
if (!autoIncrement)
{
DoPreExecute(cmd);
cmd.ExecuteNonQuery();
OnExecutedCommand(cmd);
PocoColumn pkColumn;
if (primaryKeyName != null && pd.Columns.TryGetValue(primaryKeyName, out pkColumn))
{
foreach (object poco in pocos)
{
inserted.Add(pkColumn.GetValue(poco));
}
}
return inserted.ToArray<object>();
}
object id = _dbType.ExecuteInsert(this, cmd, primaryKeyName);
if (pacosremaining.Any())
{
return BulkInsert(pacosremaining);
}
return id;
//using (var reader = cmd.ExecuteReader())
//{
// while (reader.Read())
// {
// inserted.Add(reader[0]);
// }
//}
//object[] primaryKeys = inserted.ToArray<object>();
//// Assign the ID back to the primary key property
//if (primaryKeyName != null)
//{
// PocoColumn pc;
// if (pd.Columns.TryGetValue(primaryKeyName, out pc))
// {
// index = 0;
// foreach (object poco in pocos)
// {
// pc.SetValue(poco, pc.ChangeType(primaryKeys[index]));
// index++;
// }
// }
//}
//return primaryKeys;
}
}
finally
{
CloseSharedConnection();
}
}
catch (Exception x)
{
if (OnException(x))
throw;
return null;
}
}
Below is a BulkInsert method of PetaPoco that expands on taylonr's very clever idea to use the SQL technique of insert multiple rows via INSERT INTO tab(col1, col2) OUTPUT inserted.[ID] VALUES (#0, #1), (#2, 3), (#4, #5), ..., (#n-1, #n).
It also returns the auto-increment (identity) values of inserted records, which I don't believe happens in IvoTops' implementation.
NOTE: SQL Server 2012 (and below) has a limit of 2,100 parameters per query. (This is likely the source of the stack overflow exception referenced by Zelid's comment). You will need to manually split your batches up based on the number of columns that are not decorated as Ignore or Result. For example, a POCO with 21 columns should be sent in batch sizes of 99, or (2100 - 1) / 21. I may refactor this to dynamically split batches based on this limit for SQL Server; however, you will always see the best results by managing the batch size external to this method.
This method showed an approximate 50% gain in execution time over my previous technique of using a shared connection in a single transaction for all inserts.
This is one area where Massive really shines - Massive has a Save(params object[] things) that builds an array of IDbCommands, and executes each one on a shared connection. It works out of the box, and doesn't run into parameter limits.
/// <summary>
/// Performs an SQL Insert against a collection of pocos
/// </summary>
/// <param name="pocos">A collection of POCO objects that specifies the column values to be inserted. Assumes that every POCO is of the same type.</param>
/// <returns>An array of the auto allocated primary key of the new record, or null for non-auto-increment tables</returns>
/// <remarks>
/// NOTE: As of SQL Server 2012, there is a limit of 2100 parameters per query. This limitation does not seem to apply on other platforms, so
/// this method will allow more than 2100 parameters. See http://msdn.microsoft.com/en-us/library/ms143432.aspx
/// The name of the table, it's primary key and whether it's an auto-allocated primary key are retrieved from the attributes of the first POCO in the collection
/// </remarks>
public object[] BulkInsert(IEnumerable<object> pocos)
{
Sql sql;
IList<PocoColumn> columns = new List<PocoColumn>();
IList<object> parameters;
IList<object> inserted;
PocoData pd;
Type primaryKeyType;
object template;
string commandText;
string tableName;
string primaryKeyName;
bool autoIncrement;
if (null == pocos)
return new object[] {};
template = pocos.First<object>();
if (null == template)
return null;
pd = PocoData.ForType(template.GetType());
tableName = pd.TableInfo.TableName;
primaryKeyName = pd.TableInfo.PrimaryKey;
autoIncrement = pd.TableInfo.AutoIncrement;
try
{
OpenSharedConnection();
try
{
var names = new List<string>();
var values = new List<string>();
var index = 0;
foreach (var i in pd.Columns)
{
// Don't insert result columns
if (i.Value.ResultColumn)
continue;
// Don't insert the primary key (except under oracle where we need bring in the next sequence value)
if (autoIncrement && primaryKeyName != null && string.Compare(i.Key, primaryKeyName, true) == 0)
{
primaryKeyType = i.Value.PropertyInfo.PropertyType;
// Setup auto increment expression
string autoIncExpression = _dbType.GetAutoIncrementExpression(pd.TableInfo);
if (autoIncExpression != null)
{
names.Add(i.Key);
values.Add(autoIncExpression);
}
continue;
}
names.Add(_dbType.EscapeSqlIdentifier(i.Key));
values.Add(string.Format("{0}{1}", _paramPrefix, index++));
columns.Add(i.Value);
}
string outputClause = String.Empty;
if (autoIncrement)
{
outputClause = _dbType.GetInsertOutputClause(primaryKeyName);
}
commandText = string.Format("INSERT INTO {0} ({1}){2} VALUES",
_dbType.EscapeTableName(tableName),
string.Join(",", names.ToArray()),
outputClause
);
sql = new Sql(commandText);
parameters = new List<object>();
string valuesText = string.Concat("(", string.Join(",", values.ToArray()), ")");
bool isFirstPoco = true;
foreach (object poco in pocos)
{
parameters.Clear();
foreach (PocoColumn column in columns)
{
parameters.Add(column.GetValue(poco));
}
sql.Append(valuesText, parameters.ToArray<object>());
if (isFirstPoco)
{
valuesText = "," + valuesText;
isFirstPoco = false;
}
}
inserted = new List<object>();
using (var cmd = CreateCommand(_sharedConnection, sql.SQL, sql.Arguments))
{
if (!autoIncrement)
{
DoPreExecute(cmd);
cmd.ExecuteNonQuery();
OnExecutedCommand(cmd);
PocoColumn pkColumn;
if (primaryKeyName != null && pd.Columns.TryGetValue(primaryKeyName, out pkColumn))
{
foreach (object poco in pocos)
{
inserted.Add(pkColumn.GetValue(poco));
}
}
return inserted.ToArray<object>();
}
// BUG: the following line reportedly causes duplicate inserts; need to confirm
//object id = _dbType.ExecuteInsert(this, cmd, primaryKeyName);
using(var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
inserted.Add(reader[0]);
}
}
object[] primaryKeys = inserted.ToArray<object>();
// Assign the ID back to the primary key property
if (primaryKeyName != null)
{
PocoColumn pc;
if (pd.Columns.TryGetValue(primaryKeyName, out pc))
{
index = 0;
foreach(object poco in pocos)
{
pc.SetValue(poco, pc.ChangeType(primaryKeys[index]));
index++;
}
}
}
return primaryKeys;
}
}
finally
{
CloseSharedConnection();
}
}
catch (Exception x)
{
if (OnException(x))
throw;
return null;
}
}
Here is the code for BulkInsert that you can add to v5.01 PetaPoco.cs
You can paste it somewhere close the regular insert at line 1098
You give it an IEnumerable of Pocos and it will send it to the database
in batches of x together. The code is 90% from the regular insert.
I do not have performance comparison, let me know :)
/// <summary>
/// Bulk inserts multiple rows to SQL
/// </summary>
/// <param name="tableName">The name of the table to insert into</param>
/// <param name="primaryKeyName">The name of the primary key column of the table</param>
/// <param name="autoIncrement">True if the primary key is automatically allocated by the DB</param>
/// <param name="pocos">The POCO objects that specifies the column values to be inserted</param>
/// <param name="batchSize">The number of POCOS to be grouped together for each database rounddtrip</param>
public void BulkInsert(string tableName, string primaryKeyName, bool autoIncrement, IEnumerable<object> pocos, int batchSize = 25)
{
try
{
OpenSharedConnection();
try
{
using (var cmd = CreateCommand(_sharedConnection, ""))
{
var pd = PocoData.ForObject(pocos.First(), primaryKeyName);
// Create list of columnnames only once
var names = new List<string>();
foreach (var i in pd.Columns)
{
// Don't insert result columns
if (i.Value.ResultColumn)
continue;
// Don't insert the primary key (except under oracle where we need bring in the next sequence value)
if (autoIncrement && primaryKeyName != null && string.Compare(i.Key, primaryKeyName, true) == 0)
{
// Setup auto increment expression
string autoIncExpression = _dbType.GetAutoIncrementExpression(pd.TableInfo);
if (autoIncExpression != null)
{
names.Add(i.Key);
}
continue;
}
names.Add(_dbType.EscapeSqlIdentifier(i.Key));
}
var namesArray = names.ToArray();
var values = new List<string>();
int count = 0;
do
{
cmd.CommandText = "";
cmd.Parameters.Clear();
var index = 0;
foreach (var poco in pocos.Skip(count).Take(batchSize))
{
values.Clear();
foreach (var i in pd.Columns)
{
// Don't insert result columns
if (i.Value.ResultColumn) continue;
// Don't insert the primary key (except under oracle where we need bring in the next sequence value)
if (autoIncrement && primaryKeyName != null && string.Compare(i.Key, primaryKeyName, true) == 0)
{
// Setup auto increment expression
string autoIncExpression = _dbType.GetAutoIncrementExpression(pd.TableInfo);
if (autoIncExpression != null)
{
values.Add(autoIncExpression);
}
continue;
}
values.Add(string.Format("{0}{1}", _paramPrefix, index++));
AddParam(cmd, i.Value.GetValue(poco), i.Value.PropertyInfo);
}
string outputClause = String.Empty;
if (autoIncrement)
{
outputClause = _dbType.GetInsertOutputClause(primaryKeyName);
}
cmd.CommandText += string.Format("INSERT INTO {0} ({1}){2} VALUES ({3})", _dbType.EscapeTableName(tableName),
string.Join(",", namesArray), outputClause, string.Join(",", values.ToArray()));
}
// Are we done?
if (cmd.CommandText == "") break;
count += batchSize;
DoPreExecute(cmd);
cmd.ExecuteNonQuery();
OnExecutedCommand(cmd);
}
while (true);
}
}
finally
{
CloseSharedConnection();
}
}
catch (Exception x)
{
if (OnException(x))
throw;
}
}
/// <summary>
/// Performs a SQL Bulk Insert
/// </summary>
/// <param name="pocos">The POCO objects that specifies the column values to be inserted</param>
/// <param name="batchSize">The number of POCOS to be grouped together for each database rounddtrip</param>
public void BulkInsert(IEnumerable<object> pocos, int batchSize = 25)
{
if (!pocos.Any()) return;
var pd = PocoData.ForType(pocos.First().GetType());
BulkInsert(pd.TableInfo.TableName, pd.TableInfo.PrimaryKey, pd.TableInfo.AutoIncrement, pocos);
}
And in the same lines if you want BulkUpdate:
public void BulkUpdate<T>(string tableName, string primaryKeyName, IEnumerable<T> pocos, int batchSize = 25)
{
try
{
object primaryKeyValue = null;
OpenSharedConnection();
try
{
using (var cmd = CreateCommand(_sharedConnection, ""))
{
var pd = PocoData.ForObject(pocos.First(), primaryKeyName);
int count = 0;
do
{
cmd.CommandText = "";
cmd.Parameters.Clear();
var index = 0;
var cmdText = new StringBuilder();
foreach (var poco in pocos.Skip(count).Take(batchSize))
{
var sb = new StringBuilder();
var colIdx = 0;
foreach (var i in pd.Columns)
{
// Don't update the primary key, but grab the value if we don't have it
if (string.Compare(i.Key, primaryKeyName, true) == 0)
{
primaryKeyValue = i.Value.GetValue(poco);
continue;
}
// Dont update result only columns
if (i.Value.ResultColumn)
continue;
// Build the sql
if (colIdx > 0)
sb.Append(", ");
sb.AppendFormat("{0} = {1}{2}", _dbType.EscapeSqlIdentifier(i.Key), _paramPrefix,
index++);
// Store the parameter in the command
AddParam(cmd, i.Value.GetValue(poco), i.Value.PropertyInfo);
colIdx++;
}
// Find the property info for the primary key
PropertyInfo pkpi = null;
if (primaryKeyName != null)
{
pkpi = pd.Columns[primaryKeyName].PropertyInfo;
}
cmdText.Append(string.Format("UPDATE {0} SET {1} WHERE {2} = {3}{4};\n",
_dbType.EscapeTableName(tableName), sb.ToString(),
_dbType.EscapeSqlIdentifier(primaryKeyName), _paramPrefix,
index++));
AddParam(cmd, primaryKeyValue, pkpi);
}
if (cmdText.Length == 0) break;
if (_providerName.IndexOf("oracle", StringComparison.OrdinalIgnoreCase) >= 0)
{
cmdText.Insert(0, "BEGIN\n");
cmdText.Append("\n END;");
}
DoPreExecute(cmd);
cmd.CommandText = cmdText.ToString();
count += batchSize;
cmd.ExecuteNonQuery();
OnExecutedCommand(cmd);
} while (true);
}
}
finally
{
CloseSharedConnection();
}
}
catch (Exception x)
{
if (OnException(x))
throw;
}
}
Here's a nice 2018 update using FastMember from NuGet:
private static async Task SqlBulkCopyPocoAsync<T>(PetaPoco.Database db, IEnumerable<T> data)
{
var pd = PocoData.ForType(typeof(T), db.DefaultMapper);
using (var bcp = new SqlBulkCopy(db.ConnectionString))
using (var reader = ObjectReader.Create(data))
{
// set up a mapping from the property names to the column names
var propNames = typeof(T).GetProperties().Where(p => Attribute.IsDefined(p, typeof(ResultColumnAttribute)) == false).Select(propertyInfo => propertyInfo.Name).ToArray();
foreach (var propName in propNames)
{
bcp.ColumnMappings.Add(propName, "[" + pd.GetColumnName(propName) + "]");
}
bcp.DestinationTableName = pd.TableInfo.TableName;
await bcp.WriteToServerAsync(reader).ConfigureAwait(false);
}
}
You can just do a foreach on your records.
foreach (var record in records) {
db.Save(record);
}
Related
I am using MySQLClient with a local database. I wrote a method which returns a list of data about the user, where I specify the columns I want the data from and it generates the query dynamically.
However, the reader is only returning the column names rather than the actual data and I don't know why, since the same method works previously in the program when the user is logging in.
I am using parameterised queries to protect from SQL injection.
Here is my code. I have removed parts which are unrelated to the problem, but i can give full code if needed.
namespace Library_application
{
class MainProgram
{
public static Int32 user_id;
static void Main()
{
MySqlConnection conn = LoginProgram.Start();
//this is the login process and works perfectly fine so i won't show its code
if (conn != null)
{
//this is where things start to break
NewUser(conn);
}
Console.ReadLine();
}
static void NewUser(MySqlConnection conn)
{
//three types of users, currently only using student
string query = "SELECT user_role FROM Users WHERE user_id=#user_id";
Dictionary<string, string> vars = new Dictionary<string, string>
{
["#user_id"] = user_id.ToString()
};
MySqlDataReader reader = SQLControler.SqlQuery(conn, query, vars, 0);
if (reader.Read())
{
string user_role = reader["user_role"].ToString();
reader.Close();
//this works fine and it correctly identifies the role and creates a student
Student user = new Student(conn, user_id);
//later i will add the logic to detect and create the other users but i just need this to work first
}
else
{
throw new Exception($"no user_role for user_id - {user_id}");
}
}
}
class SQLControler
{
public static MySqlDataReader SqlQuery(MySqlConnection conn, string query, Dictionary<string, string> vars, int type)
{
MySqlCommand cmd = new MySqlCommand(query, conn);
int count = vars.Count();
MySqlParameter[] param = new MySqlParameter[count];
//adds the parameters to the command
for (int i = 0; i < count; i++)
{
string key = vars.ElementAt(i).Key;
param[i] = new MySqlParameter(key, vars[key]);
cmd.Parameters.Add(param[i]);
}
//runs this one
if (type == 0)
{
Console.WriteLine("------------------------------------");
return cmd.ExecuteReader();
//returns the reader so i can get the data later and keep this reusable
}
else if (type == 1)
{
cmd.ExecuteNonQuery();
return null;
}
else
{
throw new Exception("incorrect type value");
}
}
}
class User
{
public List<string> GetValues(MySqlConnection conn, List<string> vals, int user_id)
{
Dictionary<string, string> vars = new Dictionary<string, string> { };
//------------------------------------------------------------------------------------
//this section is generating the query and parameters
//using parameters to protect against sql injection, i know that it ins't essential in this scenario
//but it will be later, so if i fix it by simply removing the parameterisation then im just kicking the problem down the road
string args = "";
for (int i = 0; i < vals.Count(); i++)
{
args = args + "#" + vals[i];
vars.Add("#" + vals[i], vals[i]);
if ((i + 1) != vals.Count())
{
args = args + ", ";
}
}
string query = "SELECT " + args + " FROM Users WHERE user_id = #user_id";
Console.WriteLine(query);
vars.Add("#user_id", user_id.ToString());
//-------------------------------------------------------------------------------------
//sends the connection, query, parameters, and query type (0 means i use a reader (select), 1 means i use non query (delete etc..))
MySqlDataReader reader = SQLControler.SqlQuery(conn, query, vars, 0);
List<string> return_vals = new List<string>();
if (reader.Read())
{
//loops through the reader and adds the value to list
for (int i = 0; i < vals.Count(); i++)
{
//vals is a list of column names in the ame order they will be returned
//i think this is where it's breaking but im not certain
return_vals.Add(reader[vals[i]].ToString());
}
reader.Close();
return return_vals;
}
else
{
throw new Exception("no data");
}
}
}
class Student : User
{
public Student(MySqlConnection conn, int user_id)
{
Console.WriteLine("student created");
//list of the data i want to retrieve from the db
//must be the column names
List<string> vals = new List<string> { "user_forename", "user_surname", "user_role", "user_status"};
//should return a list with the values in the specified columns from the user with the matching id
List<string> return_vals = base.GetValues(conn, vals, user_id);
//for some reason i am getting back the column names rather than the values in the fields
foreach(var v in return_vals)
{
Console.WriteLine(v);
}
}
}
What i have tried:
- Using getstring
- Using index rather than column names
- Specifying a specific column name
- Using while (reader.Read)
- Requesting different number of columns
I have used this method during the login section and it works perfectly there (code below). I can't figure out why it doesnt work here (code above) aswell.
static Boolean Login(MySqlConnection conn)
{
Console.Write("Username: ");
string username = Console.ReadLine();
Console.Write("Password: ");
string password = Console.ReadLine();
string query = "SELECT user_id, username, password FROM Users WHERE username=#username";
Dictionary<string, string> vars = new Dictionary<string, string>
{
["#username"] = username
};
MySqlDataReader reader = SQLControler.SqlQuery(conn, query, vars, 0);
Boolean valid_login = ValidLogin(reader, password);
return (valid_login);
}
static Boolean ValidLogin(MySqlDataReader reader, string password)
{
Boolean return_val;
if (reader.Read())
{
//currently just returns the password as is, I will implement the hashing later
password = PasswordHash(password);
if (password == reader["password"].ToString())
{
MainProgram.user_id = Convert.ToInt32(reader["user_id"]);
return_val = true;
}
else
{
return_val = false;
}
}
else
{
return_val = false;
}
reader.Close();
return return_val;
}
The problem is here:
string args = "";
for (int i = 0; i < vals.Count(); i++)
{
args = args + "#" + vals[i];
vars.Add("#" + vals[i], vals[i]);
// ...
}
string query = "SELECT " + args + " FROM Users WHERE user_id = #user_id";
This builds a query that looks like:
SELECT #user_forename, #user_surname, #user_role, #user_status FROM Users WHERE user_id = #user_id;
Meanwhile, vars.Add("#" + vals[i], vals[i]); ends up mapping #user_forename to "user_forename" in the MySqlParameterCollection for the query. Your query ends up selecting the (constant) value of those parameters for each row in the database.
The solution is:
Don't prepend # to the column names you're selecting.
Don't add the column names as variables to the query.
You can do this by replacing that whole loop with:
string args = string.Join(", ", vals);
first I know this question has been asked but I really couldn't find an answer nor find the root of the problem so maybe a someone points me in the right direction.
I'm having the An entity object cannot be referenced by multiple instances of IEntityChangeTracker. error when trying to save into the log tables.
for the log table, I'm using
https://github.com/thepirat000/Audit.NET/tree/master/src/Audit.EntityFramework
so inside my DbContext class where I define the dbset, I have to override the onscopecreated function
the problem here is that when context.Savechanges run for the first audit record for each table it works but after first record, I get the multiple reference error.
so let's say I have the following tables
Languages table. with the following values
English,French,German
Countries Table with the following values
UK,France,Germany
for languages table, if I change English to English3 and save it works It records to the audit table but then for languages table, I can not do any changes at any records it's the same in every table
what am I missing?
private void SaveToLogTable(AuditScope auditScope)
{
foreach (var entry in ((AuditEventEntityFramework)auditScope.Event).EntityFrameworkEvent.Entries)
{
if(entry.Action is null) return;
if (TABLES.Any(x => x.T_TABLE_NAME.Equals(entry.Table)))
{
var newLog = new LOGS
{
LOG_ACTION = ACTIONS.FirstOrDefault(x => x.A_DESC == entry.Action)?.A_CODE,
LOG_DATE = DateTime.Now,
USERS = MyGlobalSettings.MyUser
};
if (entry.Changes != null)
{
foreach (var changes in entry.Changes)
{
var ch = new CHANGES
{
CH_COLUMN = changes.ColumnName,
CH_NEW_VALUE = changes.NewValue.ToString(),
CH_ORIGINAL_VALUE = changes.OriginalValue.ToString()
};
newLog.CHANGES.Add(ch);
}
}
if (entry.ColumnValues != null)
{
foreach (var kv in entry.ColumnValues)
{
var val = new VALUES
{
ColumnName = kv.Key,
ColumnValue = kv.Value.ToString()
};
newLog.VALUES.Add(val);
}
}
TABLES.First(x => x.T_TABLE_NAME.Equals(entry.Table)).LOGS.Add(newLog);
}
else
{
var table = new TABLES {T_TABLE_NAME = entry.Table};
var newLog = new LOGS
{
LOG_ACTION = ACTIONS.FirstOrDefault(x => x.A_DESC.Equals(entry.Action))?.A_CODE,
LOG_DATE = DateTime.Now,
LOG_USER_REFNO = MyGlobalSettings.MyUser.U_ROWID
//USERS = MyGlobalSettings.MyUser
};
if (entry.Changes != null)
{
foreach (var changes in entry.Changes)
{
var ch = new CHANGES
{
CH_COLUMN = changes.ColumnName,
CH_NEW_VALUE = changes.NewValue.ToString(),
CH_ORIGINAL_VALUE = changes.OriginalValue.ToString()
};
newLog.CHANGES.Add(ch);
}
}
if (entry.ColumnValues != null)
{
foreach (var kv in entry.ColumnValues)
{
var val = new VALUES
{
ColumnName = kv.Key,
ColumnValue = kv.Value is null? "": kv.Value.ToString()
};
newLog.VALUES.Add(val);
}
}
table.LOGS.Add(newLog);
//TABLES.Attach(table);
//TABLES.First(x => x.T_TABLE_NAME.Equals(entry.Table)).LOGS.Add(newLog);
TABLES.Add(table);
//TablesList.Add(table);
}
//entry.Entity
}
}
we have an application that does some processing at night. Simply put, it creates some statistics for every user (about 10.000).
Now we have noticed that this takes hours in the production environment and we have been able to simulate this using a backup of the production database.
And we see that when the foreach loop starts, it generally takes around 200 ms to generate the data and save it to the database for one user. After about a 1000 users, this gets up to 700 ms per user. And after about 2.000 users, it starts to just take longer and longer, all the way up to 2 seconds to generate the data and save it to the database per user.
Do you have any ideas why this may be? Here is the (simplified) code to show what happens:
var userService = IoC.GetInstance<IUserService>();
var users = userService.GetAll().Where(x => x.IsRegistered == true);
var statisticsService = IoC.GetInstance<IStatisticsService>();
foreach (var user in users.ToList())
{
var statistic1 = GetStatistic(1); // this returns an object
var statistic 2 = GetStatistic(2);
statisticsService.Add(statistic1);
statisticsService.Add(statistic2);
statisticsService.Commit(); /* this is essentially a dbContext.SaveChanges(); */
}
function Statistic GetStatistic(int statisticnumber)
{
var stat = new Statistic();
stat.Type = statisticnumber;
switch(statisticnumber) {
case 1:
stat.Value = /* query to count how many times they've logged in */
break;
case 2:
stat.Value = /* query to get their average score */
break;
...
}
return stat;
}
So far we have tried:
AsNoTracking: we select the users using AsNoTracking to make sure Entity Framework doesn't track any changes to the user itself (because there are none).
Clearing indexes on statistics table: before this starts we run a script that drops all indexes (except the clustered index). After generation we recreate these indexes
Does anyone have any additional things we can test/try ?
As you can see in comments you need to keep the context clean so you need to Dispose it every n records (usually, in my case, n < 1000).
This is a good solution in most cases. But there are some issues, the most important are:
1. When you need to insert a lot of records, running insert (and update) statements runs faster.
2. The entities you write (and related entities) must be all in the same context.
There are some other libraries around to make bulk operations but they works only with SQL Server and I think that a great added value of EF is that is DBMS independent without significant efforts.
When I need to insert several records (less than 1.000.000) and I want to keep EF advantages I use the following methods. They generates a DML statement starting from an entity.
public int ExecuteInsertCommand(object entityObject)
{
DbCommand command = GenerateInsertCommand(entityObject);
ConnectionState oldConnectionState = command.Connection.State;
try
{
if (oldConnectionState != ConnectionState.Open)
command.Connection.Open();
int result = command.ExecuteNonQuery();
return result;
}
finally
{
if (oldConnectionState != ConnectionState.Open)
command.Connection.Close();
}
}
public DbCommand GenerateInsertCommand(object entityObject)
{
ObjectContext objectContext = ((IObjectContextAdapter)Context).ObjectContext;
var metadataWorkspace = ((EntityConnection)objectContext.Connection).GetMetadataWorkspace();
IEnumerable<EntitySetMapping> entitySetMappingCollection = metadataWorkspace.GetItems<EntityContainerMapping>(DataSpace.CSSpace).Single().EntitySetMappings;
IEnumerable<AssociationSetMapping> associationSetMappingCollection = metadataWorkspace.GetItems<EntityContainerMapping>(DataSpace.CSSpace).Single().AssociationSetMappings;
var entitySetMappings = entitySetMappingCollection.First(o => o.EntityTypeMappings.Select(e => e.EntityType.Name).Contains(entityObject.GetType().Name));
var entityTypeMapping = entitySetMappings.EntityTypeMappings[0];
string tableName = entityTypeMapping.EntitySetMapping.EntitySet.Name;
MappingFragment mappingFragment = entityTypeMapping.Fragments[0];
string sqlColumns = string.Empty;
string sqlValues = string.Empty;
int paramCount = 0;
DbCommand command = Context.Database.Connection.CreateCommand();
foreach (PropertyMapping propertyMapping in mappingFragment.PropertyMappings)
{
if (((ScalarPropertyMapping)propertyMapping).Column.StoreGeneratedPattern != StoreGeneratedPattern.None)
continue;
string columnName = ((ScalarPropertyMapping)propertyMapping).Column.Name;
object columnValue = entityObject.GetType().GetProperty(propertyMapping.Property.Name).GetValue(entityObject, null);
string paramName = string.Format("#p{0}", paramCount);
if (paramCount != 0)
{
sqlColumns += ",";
sqlValues += ",";
}
sqlColumns += SqlQuote(columnName);
sqlValues += paramName;
DbParameter parameter = command.CreateParameter();
parameter.Value = columnValue;
parameter.ParameterName = paramName;
command.Parameters.Add(parameter);
paramCount++;
}
foreach (var navigationProperty in entityTypeMapping.EntityType.NavigationProperties)
{
PropertyInfo propertyInfo = entityObject.GetType().GetProperty(navigationProperty.Name);
if (typeof(System.Collections.IEnumerable).IsAssignableFrom(propertyInfo.PropertyType))
continue;
AssociationSetMapping associationSetMapping = associationSetMappingCollection.First(a => a.AssociationSet.ElementType.FullName == navigationProperty.RelationshipType.FullName);
EndPropertyMapping propertyMappings = associationSetMapping.AssociationTypeMapping.MappingFragment.PropertyMappings.Cast<EndPropertyMapping>().First(p => p.AssociationEnd.Name.EndsWith("_Target"));
object relatedObject = propertyInfo.GetValue(entityObject, null);
foreach (ScalarPropertyMapping propertyMapping in propertyMappings.PropertyMappings)
{
string columnName = propertyMapping.Column.Name;
string paramName = string.Format("#p{0}", paramCount);
object columnValue = relatedObject == null ?
null :
relatedObject.GetType().GetProperty(propertyMapping.Property.Name).GetValue(relatedObject, null);
if (paramCount != 0)
{
sqlColumns += ",";
sqlValues += ",";
}
sqlColumns += SqlQuote(columnName);
sqlValues += string.Format("#p{0}", paramCount);
DbParameter parameter = command.CreateParameter();
parameter.Value = columnValue;
parameter.ParameterName = paramName;
command.Parameters.Add(parameter);
paramCount++;
}
}
string sql = string.Format("INSERT INTO {0} ({1}) VALUES ({2})", tableName, sqlColumns, sqlValues);
command.CommandText = sql;
foreach (DbParameter parameter in command.Parameters)
{
if (parameter.Value == null)
parameter.Value = DBNull.Value;
}
return command;
}
public int ExecuteUpdateCommand(object entityObject)
{
DbCommand command = GenerateUpdateCommand(entityObject);
ConnectionState oldConnectionState = command.Connection.State;
try
{
if (oldConnectionState != ConnectionState.Open)
command.Connection.Open();
int result = command.ExecuteNonQuery();
return result;
}
finally
{
if (oldConnectionState != ConnectionState.Open)
command.Connection.Close();
}
}
public DbCommand GenerateUpdateCommand(object entityObject)
{
ObjectContext objectContext = ((IObjectContextAdapter)Context).ObjectContext;
var metadataWorkspace = ((EntityConnection)objectContext.Connection).GetMetadataWorkspace();
IEnumerable<EntitySetMapping> entitySetMappingCollection = metadataWorkspace.GetItems<EntityContainerMapping>(DataSpace.CSSpace).Single().EntitySetMappings;
IEnumerable<AssociationSetMapping> associationSetMappingCollection = metadataWorkspace.GetItems<EntityContainerMapping>(DataSpace.CSSpace).Single().AssociationSetMappings;
string entityTypeName;
if (!entityObject.GetType().Namespace.Contains("DynamicProxi"))
entityTypeName = entityObject.GetType().Name;
else
entityTypeName = entityObject.GetType().BaseType.Name;
var entitySetMappings = entitySetMappingCollection.First(o => o.EntityTypeMappings.Select(e => e.EntityType.Name).Contains(entityTypeName));
var entityTypeMapping = entitySetMappings.EntityTypeMappings[0];
string tableName = entityTypeMapping.EntitySetMapping.EntitySet.Name;
MappingFragment mappingFragment = entityTypeMapping.Fragments[0];
string sqlColumns = string.Empty;
int paramCount = 0;
DbCommand command = Context.Database.Connection.CreateCommand();
foreach (PropertyMapping propertyMapping in mappingFragment.PropertyMappings)
{
if (((ScalarPropertyMapping)propertyMapping).Column.StoreGeneratedPattern != StoreGeneratedPattern.None)
continue;
string columnName = ((ScalarPropertyMapping)propertyMapping).Column.Name;
if (entityTypeMapping.EntityType.KeyProperties.Select(_ => _.Name).Contains(columnName))
continue;
object columnValue = entityObject.GetType().GetProperty(propertyMapping.Property.Name).GetValue(entityObject, null);
string paramName = string.Format("#p{0}", paramCount);
if (paramCount != 0)
sqlColumns += ",";
sqlColumns += string.Format("{0} = {1}", SqlQuote(columnName), paramName);
DbParameter parameter = command.CreateParameter();
parameter.Value = columnValue ?? DBNull.Value;
parameter.ParameterName = paramName;
command.Parameters.Add(parameter);
paramCount++;
}
foreach (var navigationProperty in entityTypeMapping.EntityType.NavigationProperties)
{
PropertyInfo propertyInfo = entityObject.GetType().GetProperty(navigationProperty.Name);
if (typeof(System.Collections.IEnumerable).IsAssignableFrom(propertyInfo.PropertyType))
continue;
AssociationSetMapping associationSetMapping = associationSetMappingCollection.First(a => a.AssociationSet.ElementType.FullName == navigationProperty.RelationshipType.FullName);
EndPropertyMapping propertyMappings = associationSetMapping.AssociationTypeMapping.MappingFragment.PropertyMappings.Cast<EndPropertyMapping>().First(p => p.AssociationEnd.Name.EndsWith("_Target"));
object relatedObject = propertyInfo.GetValue(entityObject, null);
foreach (ScalarPropertyMapping propertyMapping in propertyMappings.PropertyMappings)
{
string columnName = propertyMapping.Column.Name;
string paramName = string.Format("#p{0}", paramCount);
object columnValue = relatedObject == null ?
null :
relatedObject.GetType().GetProperty(propertyMapping.Property.Name).GetValue(relatedObject, null);
if (paramCount != 0)
sqlColumns += ",";
sqlColumns += string.Format("{0} = {1}", SqlQuote(columnName), paramName);
DbParameter parameter = command.CreateParameter();
parameter.Value = columnValue ?? DBNull.Value;
parameter.ParameterName = paramName;
command.Parameters.Add(parameter);
paramCount++;
}
}
string sqlWhere = string.Empty;
bool first = true;
foreach (EdmProperty keyProperty in entityTypeMapping.EntityType.KeyProperties)
{
var propertyMapping = mappingFragment.PropertyMappings.First(p => p.Property.Name == keyProperty.Name);
string columnName = ((ScalarPropertyMapping)propertyMapping).Column.Name;
object columnValue = entityObject.GetType().GetProperty(propertyMapping.Property.Name).GetValue(entityObject, null);
string paramName = string.Format("#p{0}", paramCount);
if (first)
first = false;
else
sqlWhere += " AND ";
sqlWhere += string.Format("{0} = {1}", SqlQuote(columnName), paramName);
DbParameter parameter = command.CreateParameter();
parameter.Value = columnValue;
parameter.ParameterName = paramName;
command.Parameters.Add(parameter);
paramCount++;
}
string sql = string.Format("UPDATE {0} SET {1} WHERE {2}", tableName, sqlColumns, sqlWhere);
command.CommandText = sql;
return command;
}
Add Method
This method is getting slower and slower after every iteration. In fact, this method doesn't get slower but the DetectChanges methods that get called inside the Add method.
So more record the ChangeTracker contains, slower the DetectChanges method become.
At around 100,000 entities, it can get more than 200ms to simply add a new entity when it was taking 0ms when the first entity was added.
Solution
There are several solutions to fix this issue such as:
USE AddRange over Add
SET AutoDetectChanges to false
SPLIT SaveChanges in multiple batches
In your case, probably re-creating a new context every time you loop can be the best idea since it looks you want to save on every iteration.
foreach (var user in users.ToList())
{
var statisticsService = new Instance<IStatisticsService>();
var statistic1 = GetStatistic(1); // this returns an object
var statistic 2 = GetStatistic(2);
statisticsService.Add(statistic1);
statisticsService.Add(statistic2);
statisticsService.Commit(); /* this is essentially a dbContext.SaveChanges(); */
}
Once you get rid of the poor performance due to the DetectChanges method, you still have a performance issue caused by the number of database round-trip performed by the SaveChanges methods.
If you need to save 10,000 statistics, then SaveChanges will make 10,000 database round-trip which is INSANELY slow.
Disclaimer: I'm the owner of the project Entity Framework Extensions
This library allows you to perform all bulk operations:
BulkSaveChanges
BulkInsert
BulkUpdate
BulkDelete
BulkMerge
BulkSynchronize
It works will all major provider such:
SQL Server
SQL Compact
Oracle
MySQL
SQLite
PostgreSQL
Example:
// Easy to use
context.BulkSaveChanges();
// Easy to customize
context.BulkSaveChanges(bulk => bulk.BatchSize = 100);
// Perform Bulk Operations
context.BulkDelete(customers);
context.BulkInsert(customers);
context.BulkUpdate(customers);
// Customize Primary Key
context.BulkMerge(customers, operation => {
operation.ColumnPrimaryKeyExpression =
customer => customer.Code;
});
I'm comparing materialize time between Dapper and ADO.NET and Dapper. Ultimately, Dapper tend to faster than ADO.NET, though the first time a given fetch query was executed is slower than ADO.NET. a few result show that Dapper a little bit faster than ADO.NET(almost all of result show that it comparable though)
So I think I'm using inefficient approach to map result of SqlDataReader to object.
This is my code
var sql = "SELECT * FROM Sales.SalesOrderHeader WHERE SalesOrderID = #Id";
var conn = new SqlConnection(ConnectionString);
var stopWatch = new Stopwatch();
try
{
conn.Open();
var sqlCmd = new SqlCommand(sql, conn);
for (var i = 0; i < keys.GetLength(0); i++)
{
for (var r = 0; r < keys.GetLength(1); r++)
{
stopWatch.Restart();
sqlCmd.Parameters.Clear();
sqlCmd.Parameters.AddWithValue("#Id", keys[i, r]);
var reader = await sqlCmd.ExecuteReaderAsync();
SalesOrderHeaderSQLserver salesOrderHeader = null;
while (await reader.ReadAsync())
{
salesOrderHeader = new SalesOrderHeaderSQLserver();
salesOrderHeader.SalesOrderId = (int)reader["SalesOrderId"];
salesOrderHeader.SalesOrderNumber = reader["SalesOrderNumber"] as string;
salesOrderHeader.AccountNumber = reader["AccountNumber"] as string;
salesOrderHeader.BillToAddressID = (int)reader["BillToAddressID"];
salesOrderHeader.TotalDue = (decimal)reader["TotalDue"];
salesOrderHeader.Comment = reader["Comment"] as string;
salesOrderHeader.DueDate = (DateTime)reader["DueDate"];
salesOrderHeader.CurrencyRateID = reader["CurrencyRateID"] as int?;
salesOrderHeader.CustomerID = (int)reader["CustomerID"];
salesOrderHeader.SalesPersonID = reader["SalesPersonID"] as int?;
salesOrderHeader.CreditCardApprovalCode = reader["CreditCardApprovalCode"] as string;
salesOrderHeader.ShipDate = reader["ShipDate"] as DateTime?;
salesOrderHeader.Freight = (decimal)reader["Freight"];
salesOrderHeader.ModifiedDate = (DateTime)reader["ModifiedDate"];
salesOrderHeader.OrderDate = (DateTime)reader["OrderDate"];
salesOrderHeader.TerritoryID = reader["TerritoryID"] as int?;
salesOrderHeader.CreditCardID = reader["CreditCardID"] as int?;
salesOrderHeader.OnlineOrderFlag = (bool)reader["OnlineOrderFlag"];
salesOrderHeader.PurchaseOrderNumber = reader["PurchaseOrderNumber"] as string;
salesOrderHeader.RevisionNumber = (byte)reader["RevisionNumber"];
salesOrderHeader.Rowguid = (Guid)reader["Rowguid"];
salesOrderHeader.ShipMethodID = (int)reader["ShipMethodID"];
salesOrderHeader.ShipToAddressID = (int)reader["ShipToAddressID"];
salesOrderHeader.Status = (byte)reader["Status"];
salesOrderHeader.SubTotal = (decimal)reader["SubTotal"];
salesOrderHeader.TaxAmt = (decimal)reader["TaxAmt"];
}
stopWatch.Stop();
reader.Close();
await PrintTestFindByPKReport(stopWatch.ElapsedMilliseconds, salesOrderHeader.SalesOrderId.ToString());
}
I used as keyword to cast in nullable column, is that correct?
and this is code for Dapper.
using (var conn = new SqlConnection(ConnectionString))
{
conn.Open();
var stopWatch = new Stopwatch();
for (var i = 0; i < keys.GetLength(0); i++)
{
for (var r = 0; r < keys.GetLength(1); r++)
{
stopWatch.Restart();
var result = (await conn.QueryAsync<SalesOrderHeader>("SELECT * FROM Sales.SalesOrderHeader WHERE SalesOrderID = #Id", new { Id = keys[i, r] })).FirstOrDefault();
stopWatch.Stop();
await PrintTestFindByPKReport(stopWatch.ElapsedMilliseconds, result.ToString());
}
}
}
When in doubt regarding anything db or reflection, I ask myself, "what would Marc Gravell do?".
In this case, he would use FastMember! And you should too. It's the underpinning to the data conversions in Dapper, and can easily be used to map your own DataReader to an object (should you not want to use Dapper).
Below is an extension method converting a SqlDataReader into something of type T:
PLEASE NOTE: This code implies a dependency on FastMember and is written for .NET Core (though could easily be converted to .NET Framework/Standard compliant code).
public static T ConvertToObject<T>(this SqlDataReader rd) where T : class, new()
{
Type type = typeof(T);
var accessor = TypeAccessor.Create(type);
var members = accessor.GetMembers();
var t = new T();
for (int i = 0; i < rd.FieldCount; i++)
{
if (!rd.IsDBNull(i))
{
string fieldName = rd.GetName(i);
if (members.Any(m => string.Equals(m.Name, fieldName, StringComparison.OrdinalIgnoreCase)))
{
accessor[t, fieldName] = rd.GetValue(i);
}
}
}
return t;
}
2022 update
Now that we have .NET 5 and .NET 6 available, which include Source Generators - an amazing Roslyn-based feature, that, basically, allows your code to... generate more code at compile time. It's basically "AOT Reflection" (ahead-of-time) that allows you to generate lightning-fast mapping code that has zero overhead. This thing will revolutionize the ORM world for sure.
Now, back to the question - the fastest way to map an IDataReader would be to use Source Generators. We started experimenting with this feature and we love it.
Here's a library we're working on, that does exactly that (maps DataReader to objects), feel free to "steal" some code examples: https://github.com/jitbit/MapDataReader
Previous answer that is still 100% valid
The most upvoted answer mentions #MarkGravel and his FastMember. But if you're already using Dapper, which is also a component of his, you can use Dapper's GetRowParser like this:
var parser = reader.GetRowParser<MyObject>(typeof(MyObject));
while (reader.Read())
{
var myObject = parser(reader);
}
Here's a way to make your ADO.NET code faster.
When you do your select, list out the fields that you are selecting rather than using select *. This will let you ensure the order that the fields are coming back even if that order changes in the database.Then when getting those fields from the Reader, get them by index rather than by name. Using and index is faster.
Also, I'd recommend not making string database fields nullable unless there is a strong business reason. Then just store a blank string in the database if there is no value. Finally I'd recommend using the Get methods on the DataReader to get your fields in the type they are so that casting isn't needed in your code. So for example instead of casting the DataReader[index++] value as an int use DataReader.GetInt(index++)
So for example, this code:
salesOrderHeader = new SalesOrderHeaderSQLserver();
salesOrderHeader.SalesOrderId = (int)reader["SalesOrderId"];
salesOrderHeader.SalesOrderNumber = reader["SalesOrderNumber"] as string;
salesOrderHeader.AccountNumber = reader["AccountNumber"] as string;
becomes
int index = 0;
salesOrderHeader = new SalesOrderHeaderSQLserver();
salesOrderHeader.SalesOrderId = reader.GetInt(index++);
salesOrderHeader.SalesOrderNumber = reader.GetString(index++);
salesOrderHeader.AccountNumber = reader.GetString(index++);
Give that a whirl and see how it does for you.
Modified #HouseCat's solution to be case insensitive:
/// <summary>
/// Maps a SqlDataReader record to an object. Ignoring case.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="dataReader"></param>
/// <param name="newObject"></param>
/// <remarks>https://stackoverflow.com/a/52918088</remarks>
public static void MapDataToObject<T>(this SqlDataReader dataReader, T newObject)
{
if (newObject == null) throw new ArgumentNullException(nameof(newObject));
// Fast Member Usage
var objectMemberAccessor = TypeAccessor.Create(newObject.GetType());
var propertiesHashSet =
objectMemberAccessor
.GetMembers()
.Select(mp => mp.Name)
.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
for (int i = 0; i < dataReader.FieldCount; i++)
{
var name = propertiesHashSet.FirstOrDefault(a => a.Equals(dataReader.GetName(i), StringComparison.InvariantCultureIgnoreCase));
if (!String.IsNullOrEmpty(name))
{
objectMemberAccessor[newObject, name]
= dataReader.IsDBNull(i) ? null : dataReader.GetValue(i);
}
}
}
EDIT: This does not work for List<T> or multiple tables in the results.
EDIT2: Changing the calling function to this works for lists. I am just going to return a list of objects no matter what and get the first index if I was expecting a single object. I haven't looked into multiple tables yet but I will.
public static void MapDataToObject<T>(this SqlDataReader dataReader, T newObject)
{
if (newObject == null) throw new ArgumentNullException(nameof(newObject));
// Fast Member Usage
var objectMemberAccessor = TypeAccessor.Create(newObject.GetType());
var propertiesHashSet =
objectMemberAccessor
.GetMembers()
.Select(mp => mp.Name)
.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
for (int i = 0; i < dataReader.FieldCount; i++)
{
var name = propertiesHashSet.FirstOrDefault(a => a.Equals(dataReader.GetName(i), StringComparison.InvariantCultureIgnoreCase));
if (!String.IsNullOrEmpty(name))
{
//Attention! if you are getting errors here, then double check that your model and sql have matching types for the field name.
//Check api.log for error message!
objectMemberAccessor[newObject, name]
= dataReader.IsDBNull(i) ? null : dataReader.GetValue(i);
}
}
}
EDIT 3: Updated to show sample calling function.
public async Task<List<T>> ExecuteReaderAsync<T>(string storedProcedureName, SqlParameter[] sqlParameters = null) where T : class, new()
{
var newListObject = new List<T>();
using (var conn = new SqlConnection(_connectionString))
{
using (SqlCommand sqlCommand = GetSqlCommand(conn, storedProcedureName, sqlParameters))
{
await conn.OpenAsync();
using (var dataReader = await sqlCommand.ExecuteReaderAsync(CommandBehavior.Default))
{
if (dataReader.HasRows)
{
while (await dataReader.ReadAsync())
{
var newObject = new T();
dataReader.MapDataToObject(newObject);
newListObject.Add(newObject);
}
}
}
}
}
return newListObject;
}
Took the method from pimbrouwers' answer and optimized it slightly. Reduce LINQ calls.
Maps only properties found in both the object and data field names. Handles DBNull. Other assumption made is your domain model properties absolutely equals table column/field names.
/// <summary>
/// Maps a SqlDataReader record to an object.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="dataReader"></param>
/// <param name="newObject"></param>
public static void MapDataToObject<T>(this SqlDataReader dataReader, T newObject)
{
if (newObject == null) throw new ArgumentNullException(nameof(newObject));
// Fast Member Usage
var objectMemberAccessor = TypeAccessor.Create(newObject.GetType());
var propertiesHashSet =
objectMemberAccessor
.GetMembers()
.Select(mp => mp.Name)
.ToHashSet();
for (int i = 0; i < dataReader.FieldCount; i++)
{
if (propertiesHashSet.Contains(dataReader.GetName(i)))
{
objectMemberAccessor[newObject, dataReader.GetName(i)]
= dataReader.IsDBNull(i) ? null : dataReader.GetValue(i);
}
}
}
Sample Usage:
public async Task<T> GetAsync<T>(string storedProcedureName, SqlParameter[] sqlParameters = null) where T : class, new()
{
using (var conn = new SqlConnection(_connString))
{
var sqlCommand = await GetSqlCommandAsync(storedProcedureName, conn, sqlParameters);
var dataReader = await sqlCommand.ExecuteReaderAsync(CommandBehavior.CloseConnection);
if (dataReader.HasRows)
{
var newObject = new T();
if (await dataReader.ReadAsync())
{ dataReader.MapDataToObject(newObject); }
return newObject;
}
else
{ return null; }
}
}
You can install the package DbDataReaderMapper with the command Install-Package DbDataReaderMapper or using your IDE's package manager.
You can then create your data access object (I will choose a shorter example than the one you provided):
class EmployeeDao
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int? Age { get; set; }
}
To do the automatic mapping you can call the extension method MapToObject<T>()
var reader = await sqlCmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var employeeObj = reader.MapToObject<EmployeeDao>();
}
and you will get rid of tens of lines of unreadable and hardly-maintainable code.
Step-by-step example here: https://github.com/LucaMozzo/DbDataReaderMapper
Perhaps the approach I will present isn't the most efficient but gets the job done with very little coding effort. The main benefit I see here is that you don't have to deal with data structure other than building a compatible (mapable) object.
If you convert the SqlDataReader to DataTable then serialize it using JsonConvert.SerializeObject you can then deserialize it to a known object type using JsonConvert.DeserializeObject
Here is an example of implementation:
SqlDataReader reader = null;
SqlConnection myConnection = new SqlConnection();
myConnection.ConnectionString = ConfigurationManager.ConnectionStrings["DatabaseConnection"].ConnectionString;
SqlCommand sqlCmd = new SqlCommand();
sqlCmd.CommandType = CommandType.Text;
sqlCmd.CommandText = "SELECT * FROM MyTable";
sqlCmd.Connection = myConnection;
myConnection.Open();
reader = sqlCmd.ExecuteReader();
var dataTable = new DataTable();
dataTable.Load(reader);
List<MyObject> myObjects = new List<MyObject>();
if (dataTable.Rows.Count > 0)
{
var serializedMyObjects = JsonConvert.SerializeObject(dataTable);
// Here you get the object
myObjects = (List<MyObject>)JsonConvert.DeserializeObject(serializedMyObjects, typeof(List<MyObject>));
}
myConnection.Close();
List<T> result = new List<T>();
SqlDataReader reader = com.ExecuteReader();
while(reader.Read())
{
Type type = typeof(T);
T obj = (T)Activator.CreateInstance(type);
PropertyInfo[] properties = type.GetProperties();
foreach (PropertyInfo property in properties)
{
try
{
var value = reader[property.Name];
if (value != null)
property.SetValue(obj, Convert.ChangeType(value.ToString(), property.PropertyType));
}
catch{}
}
result.Add(obj);
}
There is a SqlDataReader Mapper library in NuGet which helps you to map SqlDataReader to an object. Here is how it can be used (from GitHub documentation):
var mappedObject = new SqlDataReaderMapper<DTOObject>(reader)
.Build();
Or, if you want a more advanced mapping:
var mappedObject = new SqlDataReaderMapper<DTOObject>(reader)
.NameTransformers("_", "")
.ForMember<int>("CurrencyId")
.ForMember("CurrencyCode", "Code")
.ForMember<string>("CreatedByUser", "User").Trim()
.ForMemberManual("CountryCode", val => val.ToString().Substring(0, 10))
.ForMemberManual("ZipCode", val => val.ToString().Substring(0, 5), "ZIP")
.Build();
Advanced mapping allows you to use name transformers, change types, map fields manually or even apply functions to the object's data so that you can easily map objects even if they differ with a reader.
I took both pimbrouwers and HouseCat's answers and come up with me. In my scenario, the column name in database has snake case format.
public static T ConvertToObject<T>(string query) where T : class, new()
{
using (var conn = new SqlConnection(AutoConfig.ConnectionString))
{
conn.Open();
var cmd = new SqlCommand(query) {Connection = conn};
var rd = cmd.ExecuteReader();
var mappedObject = new T();
if (!rd.HasRows) return mappedObject;
var accessor = TypeAccessor.Create(typeof(T));
var members = accessor.GetMembers();
if (!rd.Read()) return mappedObject;
for (var i = 0; i < rd.FieldCount; i++)
{
var columnNameFromDataTable = rd.GetName(i);
var columnValueFromDataTable = rd.GetValue(i);
var splits = columnNameFromDataTable.Split('_');
var columnName = new StringBuilder("");
foreach (var split in splits)
{
columnName.Append(CultureInfo.InvariantCulture.TextInfo.ToTitleCase(split.ToLower()));
}
var mappedColumnName = members.FirstOrDefault(x =>
string.Equals(x.Name, columnName.ToString(), StringComparison.OrdinalIgnoreCase));
if(mappedColumnName == null) continue;
var columnType = mappedColumnName.Type;
if (columnValueFromDataTable != DBNull.Value)
{
accessor[mappedObject, columnName.ToString()] = Convert.ChangeType(columnValueFromDataTable, columnType);
}
}
return mappedObject;
}
}
We use the following class to execute a SQL query and automatically map the rows to objects. You can easily adjust the class to fit to your needs. Beware that our approach depends on FastMember, but you could easily modify the code to use reflection.
/// <summary>
/// Mapping configuration for a specific sql table to a specific class.
/// </summary>
/// <param name="Accessor">Used to access the target class properties.</param>
/// <param name="PropToRowIdxDict">Target class property name -> database reader row idx dictionary.</param>
internal record RowMapper(TypeAccessor Accessor, IDictionary<string, int> PropToRowIdxDict);
public class RawSqlHelperService
{
/// <summary>
/// Create a new mapper for the conversion of a <see cref="DbDataReader"/> row -> <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">Target class to use.</typeparam>
/// <param name="reader">Data reader to obtain column information from.</param>
/// <returns>Row mapper object for <see cref="DbDataReader"/> row -> <typeparamref name="T"/>.</returns>
private RowMapper GetRowMapper<T>(DbDataReader reader) where T : class, new()
{
var accessor = TypeAccessor.Create(typeof(T));
var members = accessor.GetMembers();
// Column name -> column idx dict
var columnIdxDict = Enumerable.Range(0, reader.FieldCount).ToDictionary(idx => reader.GetName(idx), idx => idx);
var propToRowIdxDict = members
.Where(m => m.GetAttribute(typeof(NotMappedAttribute), false) == null)
.Select(m =>
{
var columnAttr = m.GetAttribute(typeof(ColumnAttribute), false) as ColumnAttribute;
var columnName = columnAttr == null
? m.Name
: columnAttr.Name;
return (PropertyName: m.Name, ColumnName: columnName);
})
.ToDictionary(x => x.PropertyName, x => columnIdxDict[x.ColumnName]);
return new RowMapper(accessor, propToRowIdxDict);
}
/// <summary>
/// Read <see cref="DbDataReader"/> current row as object <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The class to map to.</typeparam>
/// <param name="reader">Data reader to read the current row from.</param>
/// <param name="mapper">Mapping configuration to use to perform the mapping operation.</param>
/// <returns>Resulting object of the mapping operation.</returns>
private T ReadRowAsObject<T>(DbDataReader reader, RowMapper mapper) where T : class, new()
{
var (accessor, propToRowIdxDict) = mapper;
var t = new T();
foreach (var (propertyName, columnIdx) in propToRowIdxDict)
accessor[t, propertyName] = reader.GetValue(columnIdx);
return t;
}
/// <summary>
/// Execute the specified <paramref name="sql"/> query and automatically map the resulting rows to <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">Target class to map to.</typeparam>
/// <param name="dbContext">Database context to perform the operation on.</param>
/// <param name="sql">SQL query to execute.</param>
/// <param name="parameters">Additional list of parameters to use for the query.</param>
/// <returns>Result of the SQL query mapped to a list of <typeparamref name="T"/>.</returns>
public async Task<IEnumerable<T>> ExecuteSql<T>(DbContext dbContext, string sql, IEnumerable<DbParameter> parameters = null) where T : class, new()
{
var con = dbContext.Database.GetDbConnection();
await con.OpenAsync();
var cmd = con.CreateCommand() as OracleCommand;
cmd.BindByName = true;
cmd.CommandText = sql;
cmd.Parameters.AddRange(parameters?.ToArray() ?? new DbParameter[0]);
var reader = await cmd.ExecuteReaderAsync();
var records = new List<T>();
var mapper = GetRowMapper<T>(reader);
while (await reader.ReadAsync())
{
records.Add(ReadRowAsObject<T>(reader, mapper));
}
await con.CloseAsync();
return records;
}
}
Mapping Attributes Supported
I implemented support for the attributes NotMapped and Column used also by the entity framework.
NotMapped Attribute
Properties decorated with this attribute will be ignored by the mapper.
Column Attribute
With this attribute the column name can be customized. Without this attribute the property name is assumed to be the column name.
Example Class
private class Test
{
[Column("SDAT")]
public DateTime StartDate { get; set; } // Column name = "SDAT"
public DateTime EDAT { get; set; } // Column name = "EDAT"
[NotMapped]
public int IWillBeIgnored { get; set; }
}
Comparision to Reflection
I also compared the approach with FastMember to using plain reflection.
For the comparision I queried two date columns from a table with 1000000 rows, here are the results:
Approach
Duration in seconds
FastMember
~1.6 seconds
Reflection
~2 seconds
Credits to user pim for inspiration.
This is based on the other answers but I used standard reflection to read the properties of the class you want to instantiate and fill it from the dataReader. You could also store the properties using a dictionary persisted b/w reads.
Initialize a dictionary containing the properties from the type with their names as the keys.
var type = typeof(Foo);
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var propertyDictionary = new Dictionary<string,PropertyInfo>();
foreach(var property in properties)
{
if (!property.CanWrite) continue;
propertyDictionary.Add(property.Name, property);
}
The method to set a new instance of the type from the DataReader would be like:
var foo = new Foo();
//retrieve the propertyDictionary for the type
for (var i = 0; i < dataReader.FieldCount; i++)
{
var n = dataReader.GetName(i);
PropertyInfo prop;
if (!propertyDictionary.TryGetValue(n, out prop)) continue;
var val = dataReader.IsDBNull(i) ? null : dataReader.GetValue(i);
prop.SetValue(foo, val, null);
}
return foo;
If you want to write an efficient generic class dealing with multiple types you could store each dictionary in a global dictionary>.
This kinda works
public static object PopulateClass(object o, SQLiteDataReader dr, Type T)
{
Type type = o.GetType();
PropertyInfo[] properties = type.GetProperties();
foreach (PropertyInfo property in properties)
{
T.GetProperty(property.Name).SetValue(o, dr[property.Name],null);
}
return o;
}
Note I'm using SQlite here but the concept is the same. As an example I'm filling a Game object by calling the above like this-
g = PopulateClass(g, dr, typeof(Game)) as Game;
Note you have to have your class match up with datareader 100%, so adjust your query to suit or pass in some sort of list to skip fields. With a SQLDataReader talking to a SQL Server DB you have a pretty good type match between .net and the database. With SQLite you have to declare your ints in your class as Int64s for this to work and watch sending nulls to strings. But the above concept seems to work so it should get you going. I think this is what the Op was after.
I have to do a lot of SQL inserts without using stored procedures.
For big classes, the insert strings get huge so I was thinking of building a generalized insert function to handle it when passing in an object. What I've written below works but it's not ideal because (1) I have to specify all possible data types and (2) I have to convert all values back to strings to build the insert string, I'd rather be using parameters with values.
I just want a function where I pass it an object class and the function inserts the object's values into the database (given all the column names in the table matches the property names of the object)
Any ideas would be greatly appreciated, thanks.
public static IEnumerable<KeyValuePair<string, T>> PropertiesOfType<T>(object obj)
{
return from p in obj.GetType().GetProperties()
where p.PropertyType == typeof(T)
select new KeyValuePair<string, T>(p.Name, (T)p.GetValue(obj, null));
}
public string InsertString(string _table, object _class)
{
Dictionary<string, string> returnDict = new Dictionary<string, string>();
StringBuilder sb = new StringBuilder();
foreach (var property in PropertiesOfType<DateTime>(_class))
returnDict.Add(property.Key, property.Value.ToString("yyyy-MM-dd HH:mm:ss"));
foreach (var property in PropertiesOfType<string>(_class))
returnDict.Add(property.Key, property.Value);
foreach (var property in PropertiesOfType<int>(_class))
{
if (property.Key == "Key")
continue;
returnDict.Add(property.Key, property.Value.ToString());
}
foreach (var property in PropertiesOfType<bool>(_class))
{
if (property.Value)
returnDict.Add(property.Key, "1");
else
returnDict.Add(property.Key, "0");
}
foreach (var property in PropertiesOfType<decimal>(_class))
returnDict.Add(property.Key, property.Value.ToString());
foreach (var property in PropertiesOfType<long>(_class))
returnDict.Add(property.Key, property.Value.ToString());
if (returnDict.Count == 1)
{
sb.Append(string.Format("INSERT INTO [{0}] ({1}) VALUES ('{2}')", _table, returnDict.ElementAt(0).Key, returnDict.ElementAt(0).Value));
}
else
{
for (int i = 0; i < returnDict.Count; i++)
{
if (i == 0)
sb.Append(string.Format("INSERT INTO [{0}] ({1}, ", _table, returnDict.ElementAt(i).Key));
else if (i == returnDict.Count - 1)
sb.Append(string.Format("{0}) ", returnDict.ElementAt(i).Key));
else
sb.Append(string.Format("{0}, ", returnDict.ElementAt(i).Key));
}
for (int i = 0; i < returnDict.Count; i++)
{
if (i == 0)
sb.Append(string.Format("VALUES ('{0}', ", returnDict.ElementAt(i).Value));
else if (i == returnDict.Count - 1)
sb.Append(string.Format("'{0}')", returnDict.ElementAt(i).Value));
else
sb.Append(string.Format("'{0}', ", returnDict.ElementAt(i).Value));
}
}
return sb.ToString();
}
string query = InsertString(_table, _obj);
I've managed to find a way to do this that I'm pretty happy about that doesn't require any external libraries or frameworks.
Basing on #HardikParmar's suggestion I built a new process on converting a class object into a datatable, this will then store all the relevant datatypes as columns.
Then add a row into the structured datatable using the class object.
Now what you have a datatable with one row of values.
Then I create a PARAMATERIZED insert statement. Then in my command text I add the values to the parameters.
Almost clean, always room for improvement.
//this function creates the datatable from a specified class type, you may exclude properties such as primary keys
public DataTable ClassToDataTable<T>(List<string> _excludeList = null)
{
Type classType = typeof(T);
List<PropertyInfo> propertyList = classType.GetProperties().ToList();
DataTable result = new DataTable(classType.Name);
foreach (PropertyInfo prop in propertyList)
{
if (_excludeList != null)
{
bool toContinue = false;
foreach (string excludeName in _excludeList)
{
if (excludeName == prop.Name)
{
toContinue = true;
break;
}
}
if (toContinue)
continue;
}
result.Columns.Add(prop.Name, prop.PropertyType);
}
return result;
}
//add data to the table
public void AddRow(ref DataTable table, object data)
{
Type classType = data.GetType();
string className = classType.Name;
if (!table.TableName.Equals(className))
{
throw new Exception("DataTableConverter.AddRow: " +
"TableName not equal to className.");
}
DataRow row = table.NewRow();
List<PropertyInfo> propertyList = classType.GetProperties().ToList();
foreach (PropertyInfo prop in propertyList)
{
foreach (DataColumn col in table.Columns)
{
if (col.ColumnName == prop.Name)
{
if (table.Columns[prop.Name] == null)
{
throw new Exception("DataTableConverter.AddRow: " +
"Column name does not exist: " + prop.Name);
}
row[prop.Name] = prop.GetValue(data, null);
}
}
}
table.Rows.Add(row);
}
//creates the insert string
public string MakeInsertParamString(string _tableName, DataTable _dt, string _condition=null)
{
StringBuilder sb = new StringBuilder();
sb.Append(string.Format("INSERT INTO [{0}] (", _tableName));
for (int i = 0; i < _dt.Columns.Count; i++)
{
sb.Append(string.Format("{0}", _dt.Columns[i].ColumnName));
if (i < _dt.Columns.Count - 1)
sb.Append(", ");
}
sb.Append(") VALUES (");
for (int i = 0; i < _dt.Columns.Count; i++)
{
sb.Append(string.Format("#{0}", _dt.Columns[i].ColumnName));
if (i < _dt.Columns.Count - 1)
sb.Append(", ");
}
sb.Append(")");
if (!string.IsNullOrEmpty(_condition))
sb.Append(" WHERE " + _condition);
return sb.ToString();
}
//inserts into the database
public string InsertUsingDataRow(string _tableName, DataTable _dt, string _condition = null)
{
try
{
using (SQLiteConnection conn = new SQLiteConnection(_dbPath))
{
string query = MakeInsertParamString(_tableName, _dt, _condition);
SQLiteCommand cmd = new SQLiteCommand(query, conn);
foreach (DataColumn col in _dt.Columns)
{
var objectValue = _dt.Rows[0][col.ColumnName];
cmd.Parameters.AddWithValue("#" + col.ColumnName, objectValue);
}
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();
}
//return MakeInsertParamString(_tableName, _dt, _condition);
return "Success";
}
catch (Exception ex) { return ex.Message; }
}