diff --git a/README.md b/README.md index d977654..ceaecd4 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ # NReco.Data -Lightweight data access components for generating SQL commands, mapping results to strongly typed POCO models or dictionaries, schema-less CRUD-operations. +Lightweight data access components for generating SQL commands, mapping results to strongly typed POCO models or dictionaries, schema-less CRUD-operations with RecordSet. * abstract DB-independent [Query structure](https://github.com/nreco/data/wiki/Query) (no need to compose raw SQL) * DbCommandBuilder for generating SELECT, INSERT, UPDATE and DELETE commands -* DbBatchCommandBuilder for generating several SQL statements into one IDbCommand (batch inserts, updates, multiple recordsets) +* DbBatchCommandBuilder for generating several SQL statements into one IDbCommand (batch inserts, updates, select multiple recordsets) * [RecordSet model](https://github.com/nreco/data/wiki/RecordSet) for in-memory data records (lightweight and efficient replacement for DataTable/DataRow) -* DbDataAdapter for CRUD-operations, can map query results to POCO models, dictionaries and RecordSet (full async support) +* DbDataAdapter for CRUD-operations: + * supports annotated POCO models (like EF Core entity models) + * schema-less data access API (dictionaries / RecordSet) + * async support for all methods * application-level data views (complex SQL queries) that accessed like simple read-only tables (DbDataView) -* best for schema-less DB access, dynamic DB queries, user-defined filters, reporting applications +* best for schema-less DB access, dynamic DB queries, user-defined filters; DAL can be used in addition to EF Core * fills the gap between minimalistic .NET Core (corefx) System.Data and EF Core * parser/builder for compact string query representation: [relex](https://github.com/nreco/data/wiki/Relex) expressions * can be used with any existing ADO.NET data provider (MsSql, PostgreSql, Sqlite, MySql etc) @@ -54,8 +57,10 @@ dbAdapter.Update( {"FirstName", "Bruce" }, {"LastName", "Wayne" } }); +// insert by model +dbAdapter.Insert( "Employees", new { FirstName = "John", LastName = "Smith" } ); ``` -**[RecordSet](https://github.com/nreco/data/wiki/RecordSet)** - efficient replacement for DataTable/DataRow (API is very similar): +**[RecordSet](https://github.com/nreco/data/wiki/RecordSet)** - efficient replacement for DataTable/DataRow with very similar API: ``` var rs = dbAdapter.Select(new Query("Employees")).ToRecordSet(); rs.SetPrimaryKey("EmployeeID"); @@ -65,6 +70,7 @@ foreach (var row in rs) { row.Delete(); } dbAdapter.Update(rs); +var rsReader = new RecordSetReader(rs); // DbDataReader for in-memory rows ``` **[Relex](https://github.com/nreco/data/wiki/Relex)** - compact relational query expressions: ``` diff --git a/src/NReco.Data.Tests/DbDataAdapterTests.cs b/src/NReco.Data.Tests/DbDataAdapterTests.cs index bb42fa3..f1f11bb 100644 --- a/src/NReco.Data.Tests/DbDataAdapterTests.cs +++ b/src/NReco.Data.Tests/DbDataAdapterTests.cs @@ -125,6 +125,34 @@ public void InsertUpdateDelete_Dictionary() { Assert.Equal(1, DbAdapter.Delete( norwayCompanyQ ) ); } + [Fact] + public async Task InsertUpdateDelete_DictionaryAsync() { + // insert + DbAdapter.Connection.Open(); + Assert.Equal(1, + await DbAdapter.InsertAsync("companies", new Dictionary() { + {"title", "Test Inc"}, + {"country", "Norway"} + }).ConfigureAwait(false) ); + object recordId = DbAdapter.CommandBuilder.DbFactory.GetInsertId(DbAdapter.Connection); + DbAdapter.Connection.Close(); + + // update + Assert.Equal(1, + await DbAdapter.UpdateAsync( new Query("companies", (QField)"id"==new QConst(recordId) ), + new Dictionary() { + {"title", "Megacorp Inc"} + } + ).ConfigureAwait(false) ); + + var norwayCompanyQ = new Query("companies", (QField)"country"==(QConst)"Norway" ); + + Assert.Equal("Megacorp Inc", DbAdapter.Select(norwayCompanyQ).ToDictionary()["title"]); + + // cleanup + Assert.Equal(1, await DbAdapter.DeleteAsync( norwayCompanyQ ).ConfigureAwait(false) ); + } + [Fact] public void InsertUpdateDelete_RecordSet() { @@ -209,6 +237,28 @@ public void InsertUpdateDelete_PocoModel() { Assert.Equal(1, DbAdapter.Delete( newCompany ) ); } + [Fact] + public async Task InsertUpdateDelete_PocoModelAsync() { + // insert + var newCompany = new CompanyModelAnnotated(); + newCompany.Id = 5000; // should be ignored + newCompany.Name = "Test Super Corp"; + newCompany.registered = false; // should be ignored + Assert.Equal(1, await DbAdapter.InsertAsync(newCompany).ConfigureAwait(false) ); + + Assert.True(newCompany.Id.HasValue); + Assert.NotEqual(5000, newCompany.Id.Value); + + Assert.Equal("Test Super Corp", DbAdapter.Select(new Query("companies", (QField)"id"==(QConst)newCompany.Id.Value).Select("title") ).Single() ); + + newCompany.Name = "Super Corp updated"; + Assert.Equal(1, await DbAdapter.UpdateAsync( newCompany).ConfigureAwait(false) ); + + Assert.Equal(newCompany.Name, DbAdapter.Select(new Query("companies", (QField)"id"==(QConst)newCompany.Id.Value).Select("title") ).Single() ); + + Assert.Equal(1, await DbAdapter.DeleteAsync( newCompany ).ConfigureAwait(false) ); + } + public class ContactModel { public int? id { get; set; } diff --git a/src/NReco.Data/DbDataAdapter.cs b/src/NReco.Data/DbDataAdapter.cs index 8af96ab..990f1b1 100644 --- a/src/NReco.Data/DbDataAdapter.cs +++ b/src/NReco.Data/DbDataAdapter.cs @@ -92,10 +92,23 @@ public SelectQuery Select(string sql, params object[] parameters) { } int InsertInternal(string tableName, IEnumerable> data) { - using (var insertCmd = CommandBuilder.GetInsertCommand(tableName, data)) { - SetupCmd(insertCmd); - return ExecuteNonQuery(insertCmd); - } + if (tableName==null) + throw new ArgumentNullException($"{nameof(tableName)}"); + return ExecuteNonQuery( CommandBuilder.GetInsertCommand(tableName, data) ); + } + + Task InsertInternalAsync(string tableName, IEnumerable> data) { + if (tableName==null) + throw new ArgumentNullException($"{nameof(tableName)}"); + return ExecuteNonQueryAsync( CommandBuilder.GetInsertCommand(tableName, data), CancellationToken.None ); + } + + DataMapper.ColumnMapping FindAutoIncrementColumn(object pocoModel) { + var schema = DataMapper.Instance.GetSchema(pocoModel.GetType()); + foreach (var colMapping in schema.Columns) + if (colMapping.IsIdentity) + return colMapping; + return null; } /// @@ -105,11 +118,22 @@ int InsertInternal(string tableName, IEnumerabledictonary with new record data (column -> value) /// Number of inserted data records. public int Insert(string tableName, IDictionary data) { + if (data==null) + throw new ArgumentNullException($"{nameof(data)}"); return InsertInternal(tableName, DataHelper.GetChangeset(data) ); } /// - /// Executes INSERT statement generated by specified table name and POCO model. + /// Asynchronously executes INSERT statement generated by specified table name and dictionary values. + /// + public Task InsertAsync(string tableName, IDictionary data) { + if (data==null) + throw new ArgumentNullException($"{nameof(data)}"); + return InsertInternalAsync(tableName, DataHelper.GetChangeset(data) ); + } + + /// + /// Executes INSERT statement generated by specified table name and annotated POCO model. /// /// table name /// POCO model with public properties that match table columns. @@ -120,18 +144,47 @@ public int Insert(string tableName, object pocoModel) { int affected = 0; DataHelper.EnsureConnectionOpen(Connection, () => { affected = InsertInternal(tableName, DataHelper.GetChangeset( pocoModel, null) ); - var schema = DataMapper.Instance.GetSchema(pocoModel.GetType()); - foreach (var colMapping in schema.Columns) - if (colMapping.IsIdentity) { - var insertedId = CommandBuilder.DbFactory.GetInsertId(Connection); - if (insertedId!=null) - colMapping.SetValue(pocoModel, insertedId); - break; - } + var autoIncrementCol = FindAutoIncrementColumn(pocoModel); + if (autoIncrementCol!=null) { + var insertedId = CommandBuilder.DbFactory.GetInsertId(Connection); + if (insertedId!=null) + autoIncrementCol.SetValue(pocoModel, insertedId); + } }); return affected; } + /// + /// Asynchronously executes INSERT statement generated by specified table name and POCO model. + /// + public async Task InsertAsync(string tableName, object pocoModel) { + if (pocoModel==null) + throw new ArgumentNullException($"{nameof(pocoModel)}"); + if (tableName==null) + throw new ArgumentNullException($"{nameof(tableName)}"); + + CancellationToken cancel = CancellationToken.None; + var isClosedConn = Connection.State == ConnectionState.Closed; + if (isClosedConn) { + await Connection.OpenAsync(cancel).ConfigureAwait(false); + } + int affected = 0; + try { + affected = await InsertInternalAsync(tableName, DataHelper.GetChangeset( pocoModel, null) ).ConfigureAwait(false); + var autoIncrementCol = FindAutoIncrementColumn(pocoModel); + if (autoIncrementCol!=null) { + var insertedId = await CommandBuilder.DbFactory.GetInsertIdAsync(Connection, cancel).ConfigureAwait(false); + if (insertedId!=null) + autoIncrementCol.SetValue(pocoModel, insertedId); + } + } finally { + if (isClosedConn) + Connection.Close(); + } + + return affected; + } + /// /// Executes INSERT statement generated by annotated POCO model. /// @@ -143,32 +196,69 @@ public int Insert(object pocoModel) { var schema = DataMapper.Instance.GetSchema(pocoModel.GetType()); return Insert(schema.TableName, pocoModel); } - - int UpdateInternal(Query q, IEnumerable> data) { - using (var updateCmd = CommandBuilder.GetUpdateCommand(q, data)) { - SetupCmd(updateCmd); - return ExecuteNonQuery(updateCmd); - } + + /// + /// Asynchronously executes INSERT statement generated by annotated POCO model. + /// + public Task InsertAsync(object pocoModel) { + if (pocoModel==null) + throw new ArgumentNullException($"{nameof(pocoModel)}"); + var schema = DataMapper.Instance.GetSchema(pocoModel.GetType()); + return InsertAsync(schema.TableName, pocoModel); + } + + int UpdateInternal(Query query, IEnumerable> data) { + if (query==null) + throw new ArgumentNullException($"{nameof(query)}"); + return ExecuteNonQuery( CommandBuilder.GetUpdateCommand(query, data) ); + } + + Task UpdateInternalAsync(Query query, IEnumerable> data) { + if (query==null) + throw new ArgumentNullException($"{nameof(query)}"); + return ExecuteNonQueryAsync( CommandBuilder.GetUpdateCommand(query, data), CancellationToken.None ); } /// /// Executes UPDATE statement generated by specified and dictionary values. /// - /// query that determines data records to update. + /// query that determines data records to update. /// dictonary with changeset data (column -> value) /// Number of updated data records. - public int Update(Query q, IDictionary data) { - return UpdateInternal(q, DataHelper.GetChangeset(data) ); + public int Update(Query query, IDictionary data) { + if (data==null) + throw new ArgumentNullException($"{nameof(data)}"); + return UpdateInternal(query, DataHelper.GetChangeset(data) ); + } + + /// + /// Asynchronously executes UPDATE statement generated by specified and dictionary values. + /// + public Task UpdateAsync(Query query, IDictionary data) { + if (data==null) + throw new ArgumentNullException($"{nameof(data)}"); + return UpdateInternalAsync(query, DataHelper.GetChangeset(data) ); } /// /// Executes UPDATE statement generated by specified and POCO model. /// - /// query that determines data records to update. + /// query that determines data records to update. /// POCO model with public properties that match table columns. /// Number of updated data records. - public int Update(Query q, object pocoModel) { - return UpdateInternal(q, DataHelper.GetChangeset( pocoModel, null) ); + public int Update(Query query, object pocoModel) { + if (pocoModel==null) + throw new ArgumentNullException($"{nameof(pocoModel)}"); + return UpdateInternal(query, DataHelper.GetChangeset( pocoModel, null) ); + } + + /// + /// Asynchronously executes UPDATE statement generated by specified and POCO model. + /// + public Task UpdateAsync(Query query, object pocoModel) { + if (pocoModel==null) + throw new ArgumentNullException($"{nameof(pocoModel)}"); + return UpdateInternalAsync(query, DataHelper.GetChangeset( pocoModel, null) ); } /// @@ -182,6 +272,15 @@ public int Update(object pocoModel) { return Update( GetQueryByKey(pocoModel), pocoModel); } + /// + /// Asynchronously executes UPDATE statement generated by annotated POCO model. + /// + public Task UpdateAsync(object pocoModel) { + if (pocoModel==null) + throw new ArgumentNullException($"{nameof(pocoModel)}"); + return UpdateAsync( GetQueryByKey(pocoModel), pocoModel); + } + Query GetQueryByKey(object pocoModel) { var schema = DataMapper.Instance.GetSchema(pocoModel.GetType()); if (schema.Key.Length==0) @@ -202,7 +301,6 @@ void EnsurePrimaryKey(RecordSet recordSet) { throw new NotSupportedException("Update operation can be performed only for RecordSet with PrimaryKey"); } - RecordSet IRecordSetAdapter.Select(Query q) { return Select(q).ToRecordSet(); } @@ -269,10 +367,7 @@ public async Task UpdateAsync(string tableName, RecordSet recordSet, Cancel /// query that determines data records to delete. /// Number of actually deleted records. public int Delete(Query q) { - using (var deleteCmd = CommandBuilder.GetDeleteCommand(q)) { - SetupCmd(deleteCmd); - return ExecuteNonQuery(deleteCmd); - } + return ExecuteNonQuery( CommandBuilder.GetDeleteCommand(q) ); } /// @@ -286,6 +381,15 @@ public int Delete(object pocoModel) { return Delete( GetQueryByKey(pocoModel) ); } + /// + /// Asynchronously executes DELETE statement generated by annotated POCO model. + /// + public Task DeleteAsync(object pocoModel) { + if (pocoModel==null) + throw new ArgumentNullException($"{nameof(pocoModel)}"); + return DeleteAsync( GetQueryByKey(pocoModel) ); + } + /// /// Asynchronously executes DELETE statement generated by specified . /// @@ -296,32 +400,39 @@ public Task DeleteAsync(Query q) { /// /// Asynchronously executes DELETE statement generated by specified . /// - public async Task DeleteAsync(Query q, CancellationToken cancel) { + public Task DeleteAsync(Query q, CancellationToken cancel) { + return ExecuteNonQueryAsync( CommandBuilder.GetDeleteCommand(q), cancel ); + } + + private int ExecuteNonQuery(IDbCommand cmd) { + int affectedRecords = 0; + SetupCmd(cmd); + using (cmd) { + DataHelper.EnsureConnectionOpen(Connection, () => { + affectedRecords = cmd.ExecuteNonQuery(); + }); + } + return affectedRecords; + } + + private async Task ExecuteNonQueryAsync(IDbCommand cmd, CancellationToken cancel) { int affected = 0; - using (var deleteCmd = CommandBuilder.GetDeleteCommand(q)) { - SetupCmd(deleteCmd); - var isClosedConn = deleteCmd.Connection.State == ConnectionState.Closed; + using (cmd) { + SetupCmd(cmd); + var isClosedConn = cmd.Connection.State == ConnectionState.Closed; if (isClosedConn) { - await deleteCmd.Connection.OpenAsync(cancel).ConfigureAwait(false); + await cmd.Connection.OpenAsync(cancel).ConfigureAwait(false); } try { - affected = await deleteCmd.ExecuteNonQueryAsync(cancel).ConfigureAwait(false); + affected = await cmd.ExecuteNonQueryAsync(cancel).ConfigureAwait(false); } finally { if (isClosedConn) - deleteCmd.Connection.Close(); + cmd.Connection.Close(); } } return affected; } - private int ExecuteNonQuery(IDbCommand cmd) { - int affectedRecords = 0; - DataHelper.EnsureConnectionOpen(Connection, () => { - affectedRecords = cmd.ExecuteNonQuery(); - }); - return affectedRecords; - } - public void Dispose() { Dispose(true); } diff --git a/src/NReco.Data/DbFactory.cs b/src/NReco.Data/DbFactory.cs index 4b3c6e0..4011b9c 100644 --- a/src/NReco.Data/DbFactory.cs +++ b/src/NReco.Data/DbFactory.cs @@ -16,6 +16,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; +using System.Threading.Tasks; using System.Data; using System.Data.Common; @@ -92,6 +94,19 @@ public virtual object GetInsertId(IDbConnection connection) { } } + public async Task GetInsertIdAsync(IDbConnection connection, CancellationToken cancel) { + if (String.IsNullOrEmpty(LastInsertIdSelectText)) { + return null; + } + if (connection.State != ConnectionState.Open) + throw new InvalidOperationException("GetInsertId requires opened connection"); + using (var cmd = CreateCommand()) { + cmd.CommandText = LastInsertIdSelectText; + cmd.Connection = connection; + return await cmd.ExecuteScalarAsync(cancel); + } + } + protected virtual string GetCmdParameterPlaceholder(string paramName) { if (ParamPlaceholderFormat == null) return paramName; diff --git a/src/NReco.Data/IDbFactory.cs b/src/NReco.Data/IDbFactory.cs index cefbc89..5fc4e7f 100644 --- a/src/NReco.Data/IDbFactory.cs +++ b/src/NReco.Data/IDbFactory.cs @@ -15,6 +15,8 @@ using System; using System.Data; using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; using System.Collections.Generic; namespace NReco.Data @@ -46,9 +48,14 @@ public interface IDbFactory { ISqlExpressionBuilder CreateSqlBuilder(IDbCommand dbCommand, Func buildSubquery); /// - /// Get ID of last inserted record + /// Gets ID of last inserted record /// object GetInsertId(IDbConnection connection); + + /// + /// Asynchronously gets ID of last inserted record + /// + Task GetInsertIdAsync(IDbConnection connection, CancellationToken cancel); } public sealed class CommandParameter { diff --git a/src/NReco.Data/Internal/DbCommandAsyncExt.cs b/src/NReco.Data/Internal/DbCommandAsyncExt.cs index 0ad6913..e3b1404 100644 --- a/src/NReco.Data/Internal/DbCommandAsyncExt.cs +++ b/src/NReco.Data/Internal/DbCommandAsyncExt.cs @@ -32,6 +32,14 @@ internal static Task ExecuteNonQueryAsync(this IDbCommand cmd, Cancellation } } + internal static Task ExecuteScalarAsync(this IDbCommand cmd, CancellationToken cancel) { + if (cmd is DbCommand) { + return ((DbCommand)cmd).ExecuteScalarAsync(cancel); + } else { + return Task.FromResult( cmd.ExecuteScalar() ); + } + } + } diff --git a/src/NReco.Data/Properties/AssemblyInfo.cs b/src/NReco.Data/Properties/AssemblyInfo.cs index 7062195..4a74927 100644 --- a/src/NReco.Data/Properties/AssemblyInfo.cs +++ b/src/NReco.Data/Properties/AssemblyInfo.cs @@ -7,7 +7,7 @@ // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("NReco.Data")] -[assembly: AssemblyDescription("Lightweight data access components for generating SQL commands by db-independent queries.")] +[assembly: AssemblyDescription("Lightweight data access components for generating SQL commands by db-independent queries, CRUD operations.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("NReco.Data")] diff --git a/src/NReco.Data/project.json b/src/NReco.Data/project.json index b84b96c..0a1c69a 100644 --- a/src/NReco.Data/project.json +++ b/src/NReco.Data/project.json @@ -1,7 +1,7 @@ { "version": "1.0.0-alpha6", "title": "NReco.Data", - "description": "Lightweight db-independent data access library: generates SQL commands by abstract queries (command builder), schema-less CRUD operations (data adapter), POCO mapping, RecordSet (replacement for DataTable).", + "description": "Lightweight db-independent data access library: generates SQL commands by abstract queries (command builder), schema-less CRUD operations (data adapter), annotated POCO mapping, RecordSet (replacement for DataTable).", "authors": [ "Vitalii Fedorchenko" ], "copyright": "Copyright (c) 2016 Vitalii Fedorchenko", "packOptions": {