Commit 4f3a04cf authored by Marc Gravell's avatar Marc Gravell

Merge pull request #412 from StackExchange/QueryFirstOrDefault

Add {Query|Read}FirstOrDefault methods with optimized paths
parents 337af552 5708d498
...@@ -78,7 +78,7 @@ public static partial class SqlMapperExtensions ...@@ -78,7 +78,7 @@ public static partial class SqlMapperExtensions
/// <param name="transaction">The transaction to run under, null (the defualt) if none</param> /// <param name="transaction">The transaction to run under, null (the defualt) if none</param>
/// <param name="commandTimeout">Number of seconds before command execution timeout</param> /// <param name="commandTimeout">Number of seconds before command execution timeout</param>
/// <returns>Entity of T</returns> /// <returns>Entity of T</returns>
public static async Task<IEnumerable<T>> GetAllAsync<T>(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class public static Task<IEnumerable<T>> GetAllAsync<T>(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class
{ {
var type = typeof(T); var type = typeof(T);
var cacheType = typeof(List<T>); var cacheType = typeof(List<T>);
...@@ -95,9 +95,12 @@ public static partial class SqlMapperExtensions ...@@ -95,9 +95,12 @@ public static partial class SqlMapperExtensions
if (!type.IsInterface()) if (!type.IsInterface())
{ {
return await connection.QueryAsync<T>(sql, null, transaction, commandTimeout); return connection.QueryAsync<T>(sql, null, transaction, commandTimeout);
} }
return GetAllAsyncImpl<T>(connection, transaction, commandTimeout, sql, type);
}
private static async Task<IEnumerable<T>> GetAllAsyncImpl<T>(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string sql, Type type) where T : class
{
var result = await connection.QueryAsync(sql); var result = await connection.QueryAsync(sql);
var list = new List<T>(); var list = new List<T>();
foreach (IDictionary<string, object> res in result) foreach (IDictionary<string, object> res in result)
...@@ -124,7 +127,7 @@ public static partial class SqlMapperExtensions ...@@ -124,7 +127,7 @@ public static partial class SqlMapperExtensions
/// <param name="commandTimeout">Number of seconds before command execution timeout</param> /// <param name="commandTimeout">Number of seconds before command execution timeout</param>
/// <param name="sqlAdapter">The specific ISqlAdapter to use, auto-detected based on connection if null</param> /// <param name="sqlAdapter">The specific ISqlAdapter to use, auto-detected based on connection if null</param>
/// <returns>Identity of inserted entity</returns> /// <returns>Identity of inserted entity</returns>
public static async Task<int> InsertAsync<T>(this IDbConnection connection, T entityToInsert, IDbTransaction transaction = null, public static Task<int> InsertAsync<T>(this IDbConnection connection, T entityToInsert, IDbTransaction transaction = null,
int? commandTimeout = null, ISqlAdapter sqlAdapter = null) where T : class int? commandTimeout = null, ISqlAdapter sqlAdapter = null) where T : class
{ {
var type = typeof(T); var type = typeof(T);
...@@ -164,13 +167,13 @@ public static partial class SqlMapperExtensions ...@@ -164,13 +167,13 @@ public static partial class SqlMapperExtensions
if (!isList) //single entity if (!isList) //single entity
{ {
return await sqlAdapter.InsertAsync(connection, transaction, commandTimeout, name, sbColumnList.ToString(), return sqlAdapter.InsertAsync(connection, transaction, commandTimeout, name, sbColumnList.ToString(),
sbParameterList.ToString(), keyProperties, entityToInsert); sbParameterList.ToString(), keyProperties, entityToInsert);
} }
//insert list of entities //insert list of entities
var cmd = $"INSERT INTO {name} ({sbColumnList}) values ({sbParameterList})"; var cmd = $"INSERT INTO {name} ({sbColumnList}) values ({sbParameterList})";
return await connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout); return connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout);
} }
/// <summary> /// <summary>
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0", "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0",
"summary": "A high performance Micro-ORM", "summary": "A high performance Micro-ORM",
"description": "A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc..", "description": "A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc..",
"version": "1.50-beta3", "version": "1.50-beta4",
"title": "Dapper dot net (strong named)", "title": "Dapper dot net (strong named)",
"tags": [ "orm", "sql", "micro-orm" ], "tags": [ "orm", "sql", "micro-orm" ],
"copyright": "2015 Stack Exchange, Inc.", "copyright": "2015 Stack Exchange, Inc.",
......
...@@ -130,10 +130,13 @@ public void Run(int iterations) ...@@ -130,10 +130,13 @@ public void Run(int iterations)
var mapperConnection = TestSuite.GetOpenConnection(); var mapperConnection = TestSuite.GetOpenConnection();
tests.Add(id => mapperConnection.Query<Post>("select * from Posts where Id = @Id", new { Id = id }, buffered: true).First(), "Mapper Query (buffered)"); tests.Add(id => mapperConnection.Query<Post>("select * from Posts where Id = @Id", new { Id = id }, buffered: true).First(), "Mapper Query (buffered)");
tests.Add(id => mapperConnection.Query<Post>("select * from Posts where Id = @Id", new { Id = id }, buffered: false).First(), "Mapper Query (non-buffered)"); tests.Add(id => mapperConnection.Query<Post>("select * from Posts where Id = @Id", new { Id = id }, buffered: false).First(), "Mapper Query (non-buffered)");
tests.Add(id => mapperConnection.QueryFirstOrDefault<Post>("select * from Posts where Id = @Id", new { Id = id }), "Mapper QueryFirstOrDefault");
var mapperConnection2 = TestSuite.GetOpenConnection(); var mapperConnection2 = TestSuite.GetOpenConnection();
tests.Add(id => mapperConnection2.Query("select * from Posts where Id = @Id", new { Id = id }, buffered: true).First(), "Dynamic Mapper Query (buffered)"); tests.Add(id => mapperConnection2.Query("select * from Posts where Id = @Id", new { Id = id }, buffered: true).First(), "Dynamic Mapper Query (buffered)");
tests.Add(id => mapperConnection2.Query("select * from Posts where Id = @Id", new { Id = id }, buffered: false).First(), "Dynamic Mapper Query (non-buffered)"); tests.Add(id => mapperConnection2.Query("select * from Posts where Id = @Id", new { Id = id }, buffered: false).First(), "Dynamic Mapper Query (non-buffered)");
tests.Add(id => mapperConnection2.QueryFirstOrDefault("select * from Posts where Id = @Id", new { Id = id }), "Dynamic Mapper QueryQueryFirstOrDefault");
// dapper.contrib // dapper.contrib
var mapperConnection3 = TestSuite.GetOpenConnection(); var mapperConnection3 = TestSuite.GetOpenConnection();
......
...@@ -156,6 +156,19 @@ public async Task TestMultiAsync() ...@@ -156,6 +156,19 @@ public async Task TestMultiAsync()
} }
} }
[Fact]
public async Task TestMultiAsyncViaFirstOrDefault()
{
using (SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select 1; select 2; select 3; select 4; select 5"))
{
multi.ReadFirstOrDefaultAsync<int>().Result.IsEqualTo(1);
multi.ReadAsync<int>().Result.Single().IsEqualTo(2);
multi.ReadFirstOrDefaultAsync<int>().Result.IsEqualTo(3);
multi.ReadAsync<int>().Result.Single().IsEqualTo(4);
multi.ReadFirstOrDefaultAsync<int>().Result.IsEqualTo(5);
}
}
[Fact] [Fact]
public async Task TestMultiClosedConnAsync() public async Task TestMultiClosedConnAsync()
{ {
...@@ -166,6 +179,19 @@ public async Task TestMultiClosedConnAsync() ...@@ -166,6 +179,19 @@ public async Task TestMultiClosedConnAsync()
} }
} }
[Fact]
public async Task TestMultiClosedConnAsyncViaFirstOrDefault()
{
using (SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select 1; select 2; select 3; select 4; select 5;"))
{
multi.ReadFirstOrDefaultAsync<int>().Result.IsEqualTo(1);
multi.ReadAsync<int>().Result.Single().IsEqualTo(2);
multi.ReadFirstOrDefaultAsync<int>().Result.IsEqualTo(3);
multi.ReadAsync<int>().Result.Single().IsEqualTo(4);
multi.ReadFirstOrDefaultAsync<int>().Result.IsEqualTo(5);
}
}
#if EXTERNALS #if EXTERNALS
[Fact] [Fact]
public async Task ExecuteReaderOpenAsync() public async Task ExecuteReaderOpenAsync()
...@@ -337,6 +363,19 @@ public async Task TypeBasedViaTypeAsync() ...@@ -337,6 +363,19 @@ public async Task TypeBasedViaTypeAsync()
b.IsEqualTo("abc"); b.IsEqualTo("abc");
} }
[Fact]
public async Task TypeBasedViaTypeAsyncFirstOrDefault()
{
Type type = GetSomeType();
dynamic actual = (await marsConnection.QueryFirstOrDefaultAsync(type, "select @A as [A], @B as [B]", new { A = 123, B = "abc" }));
((object)actual).GetType().IsEqualTo(type);
int a = actual.A;
string b = actual.B;
a.IsEqualTo(123);
b.IsEqualTo("abc");
}
[Fact] [Fact]
public async Task Issue22_ExecuteScalarAsync() public async Task Issue22_ExecuteScalarAsync()
{ {
...@@ -562,7 +601,26 @@ public async Task TestSubsequentQueriesSuccessAsync() ...@@ -562,7 +601,26 @@ public async Task TestSubsequentQueriesSuccessAsync()
class AsyncFoo0 { public int Id { get; set; } } class AsyncFoo0 { public int Id { get; set; } }
class AsyncFoo1 { public int Id { get; set; } } class AsyncFoo1 { public int Id { get; set; } }
class AsyncFoo2 { public int Id { get; set; } } class AsyncFoo2 { public int Id { get; set; } }
[Fact]
public async Task TestSchemaChangedViaFirstOrDefaultAsync()
{
await connection.ExecuteAsync("create table #dog(Age int, Name nvarchar(max)) insert #dog values(1, 'Alf')");
try
{
var d = await connection.QueryFirstOrDefaultAsync<Dog>("select * from #dog");
d.Name.IsEqualTo("Alf");
d.Age.IsEqualTo(1);
connection.Execute("alter table #dog drop column Name");
d = await connection.QueryFirstOrDefaultAsync<Dog>("select * from #dog");
d.Name.IsNull();
d.Age.IsEqualTo(1);
}
finally
{
await connection.ExecuteAsync("drop table #dog");
}
}
[Fact] [Fact]
public async Task TestMultiMapArbitraryMapsAsync() public async Task TestMultiMapArbitraryMapsAsync()
......
...@@ -258,6 +258,26 @@ public void TestSchemaChanged() ...@@ -258,6 +258,26 @@ public void TestSchemaChanged()
} }
} }
[Fact]
public void TestSchemaChangedViaFirstOrDefault()
{
connection.Execute("create table #dog(Age int, Name nvarchar(max)) insert #dog values(1, 'Alf')");
try
{
var d = connection.QueryFirstOrDefault<Dog>("select * from #dog");
d.Name.IsEqualTo("Alf");
d.Age.IsEqualTo(1);
connection.Execute("alter table #dog drop column Name");
d = connection.QueryFirstOrDefault<Dog>("select * from #dog");
d.Name.IsNull();
d.Age.IsEqualTo(1);
}
finally
{
connection.Execute("drop table #dog");
}
}
[Fact] [Fact]
public void TestSchemaChangedMultiMap() public void TestSchemaChangedMultiMap()
{ {
...@@ -845,6 +865,8 @@ public void TestFastExpandoSupportsIDictionary() ...@@ -845,6 +865,8 @@ public void TestFastExpandoSupportsIDictionary()
public void TestDapperSetsPrivates() public void TestDapperSetsPrivates()
{ {
connection.Query<PrivateDan>("select 'one' ShadowInDB").First().Shadow.IsEqualTo(1); connection.Query<PrivateDan>("select 'one' ShadowInDB").First().Shadow.IsEqualTo(1);
connection.QueryFirstOrDefault<PrivateDan>("select 'one' ShadowInDB").Shadow.IsEqualTo(1);
} }
class PrivateDan class PrivateDan
...@@ -1631,6 +1653,25 @@ public void TestChangingDefaultStringTypeMappingToAnsiString() ...@@ -1631,6 +1653,25 @@ public void TestChangingDefaultStringTypeMappingToAnsiString()
Dapper.SqlMapper.AddTypeMap(typeof(string), DbType.String); // Restore Default to Unicode String Dapper.SqlMapper.AddTypeMap(typeof(string), DbType.String); // Restore Default to Unicode String
} }
[Fact]
public void TestChangingDefaultStringTypeMappingToAnsiStringFirstOrDefault()
{
var sql = "SELECT SQL_VARIANT_PROPERTY(CONVERT(sql_variant, @testParam),'BaseType') AS BaseType";
var param = new { testParam = "TestString" };
var result01 = connection.QueryFirstOrDefault<string>(sql, param);
result01.IsEqualTo("nvarchar");
Dapper.SqlMapper.PurgeQueryCache();
Dapper.SqlMapper.AddTypeMap(typeof(string), DbType.AnsiString); // Change Default String Handling to AnsiString
var result02 = connection.QueryFirstOrDefault<string>(sql, param);
result02.IsEqualTo("varchar");
Dapper.SqlMapper.PurgeQueryCache();
Dapper.SqlMapper.AddTypeMap(typeof(string), DbType.String); // Restore Default to Unicode String
}
#if COREFX #if COREFX
class TransactedConnection : IDbConnection class TransactedConnection : IDbConnection
{ {
......
This diff is collapsed.
...@@ -32,6 +32,15 @@ public Task<IEnumerable<dynamic>> ReadAsync(bool buffered = true) ...@@ -32,6 +32,15 @@ public Task<IEnumerable<dynamic>> ReadAsync(bool buffered = true)
return ReadAsyncImpl<dynamic>(typeof(DapperRow), buffered); return ReadAsyncImpl<dynamic>(typeof(DapperRow), buffered);
} }
/// <summary>
/// Read the first row of the next grid of results, returned as a dynamic object
/// </summary>
/// <remarks>Note: the row can be accessed via "dynamic", or by casting to an IDictionary&lt;string,object&gt;</remarks>
public Task<dynamic> ReadFirstOrDefaultAsync()
{
return ReadFirstOrDefaultAsyncImpl<dynamic>(typeof(DapperRow));
}
/// <summary> /// <summary>
/// Read the next grid of results /// Read the next grid of results
/// </summary> /// </summary>
...@@ -40,6 +49,16 @@ public Task<IEnumerable<object>> ReadAsync(Type type, bool buffered = true) ...@@ -40,6 +49,16 @@ public Task<IEnumerable<object>> ReadAsync(Type type, bool buffered = true)
if (type == null) throw new ArgumentNullException(nameof(type)); if (type == null) throw new ArgumentNullException(nameof(type));
return ReadAsyncImpl<object>(type, buffered); return ReadAsyncImpl<object>(type, buffered);
} }
/// <summary>
/// Read the first row of the next grid of results
/// </summary>
public Task<object> ReadFirstOrDefaultAsync(Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return ReadFirstOrDefaultAsyncImpl<object>(type);
}
/// <summary> /// <summary>
/// Read the next grid of results /// Read the next grid of results
/// </summary> /// </summary>
...@@ -48,6 +67,14 @@ public Task<IEnumerable<T>> ReadAsync<T>(bool buffered = true) ...@@ -48,6 +67,14 @@ public Task<IEnumerable<T>> ReadAsync<T>(bool buffered = true)
return ReadAsyncImpl<T>(typeof(T), buffered); return ReadAsyncImpl<T>(typeof(T), buffered);
} }
/// <summary>
/// Read the first row of the next grid of results
/// </summary>
public Task<T> ReadFirstOrDefaultAsync<T>()
{
return ReadFirstOrDefaultAsyncImpl<T>(typeof(T));
}
private async Task NextResultAsync() private async Task NextResultAsync()
{ {
if (await ((DbDataReader)reader).NextResultAsync(cancel).ConfigureAwait(false)) if (await ((DbDataReader)reader).NextResultAsync(cancel).ConfigureAwait(false))
...@@ -94,6 +121,42 @@ private Task<IEnumerable<T>> ReadAsyncImpl<T>(Type type, bool buffered) ...@@ -94,6 +121,42 @@ private Task<IEnumerable<T>> ReadAsyncImpl<T>(Type type, bool buffered)
} }
} }
private Task<T> ReadFirstOrDefaultAsyncImpl<T>(Type type)
{
var dbReader = reader as DbDataReader;
if (dbReader != null) return ReadFirstOrDefaultAsyncImplImpl<T>(dbReader, type);
// no async API available; use non-async and fake it
return Task.FromResult<T>(ReadFirstOrDefaultImpl<T>(type));
}
private async Task<T> ReadFirstOrDefaultAsyncImplImpl<T>(DbDataReader reader, Type type)
{
if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed");
if (IsConsumed) throw new InvalidOperationException("Query results must be consumed in the correct order, and each result can only be consumed once");
IsConsumed = true;
T result = default(T);
if (await reader.ReadAsync(cancel).ConfigureAwait(false))
{
var typedIdentity = identity.ForGrid(type, gridIndex);
CacheInfo cache = GetCacheInfo(typedIdentity, null, addToCache);
var deserializer = cache.Deserializer;
int hash = GetColumnHash(reader);
if (deserializer.Func == null || deserializer.Hash != hash)
{
deserializer = new DeserializerState(hash, GetDeserializer(type, reader, 0, -1, false));
cache.Deserializer = deserializer;
}
result = (T)deserializer.Func(reader);
while (await reader.ReadAsync(cancel).ConfigureAwait(false)) { }
}
await NextResultAsync().ConfigureAwait(false);
return result;
}
private async Task<IEnumerable<T>> ReadBufferedAsync<T>(int index, Func<IDataReader, object> deserializer, Identity typedIdentity) private async Task<IEnumerable<T>> ReadBufferedAsync<T>(int index, Func<IDataReader, object> deserializer, Identity typedIdentity)
{ {
try try
......
...@@ -39,6 +39,15 @@ public IEnumerable<dynamic> Read(bool buffered = true) ...@@ -39,6 +39,15 @@ public IEnumerable<dynamic> Read(bool buffered = true)
return ReadImpl<dynamic>(typeof(DapperRow), buffered); return ReadImpl<dynamic>(typeof(DapperRow), buffered);
} }
/// <summary>
/// Read the first row of the next grid of results, returned as a dynamic object
/// </summary>
/// <remarks>Note: the row can be accessed via "dynamic", or by casting to an IDictionary&lt;string,object&gt;</remarks>
public dynamic ReadFirstOrDefault()
{
return ReadFirstOrDefaultImpl<dynamic>(typeof(DapperRow));
}
/// <summary> /// <summary>
/// Read the next grid of results /// Read the next grid of results
/// </summary> /// </summary>
...@@ -47,6 +56,14 @@ public IEnumerable<T> Read<T>(bool buffered = true) ...@@ -47,6 +56,14 @@ public IEnumerable<T> Read<T>(bool buffered = true)
return ReadImpl<T>(typeof(T), buffered); return ReadImpl<T>(typeof(T), buffered);
} }
/// <summary>
/// Read the first row of the next grid of results
/// </summary>
public T ReadFirstOrDefault<T>()
{
return ReadFirstOrDefaultImpl<T>(typeof(T));
}
/// <summary> /// <summary>
/// Read the next grid of results /// Read the next grid of results
/// </summary> /// </summary>
...@@ -56,6 +73,15 @@ public IEnumerable<object> Read(Type type, bool buffered = true) ...@@ -56,6 +73,15 @@ public IEnumerable<object> Read(Type type, bool buffered = true)
return ReadImpl<object>(type, buffered); return ReadImpl<object>(type, buffered);
} }
/// <summary>
/// Read the first row of the next grid of results
/// </summary>
public object ReadFirstOrDefault(Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return ReadFirstOrDefaultImpl<object>(type);
}
private IEnumerable<T> ReadImpl<T>(Type type, bool buffered) private IEnumerable<T> ReadImpl<T>(Type type, bool buffered)
{ {
if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed"); if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed");
...@@ -75,6 +101,32 @@ private IEnumerable<T> ReadImpl<T>(Type type, bool buffered) ...@@ -75,6 +101,32 @@ private IEnumerable<T> ReadImpl<T>(Type type, bool buffered)
return buffered ? result.ToList() : result; return buffered ? result.ToList() : result;
} }
private T ReadFirstOrDefaultImpl<T>(Type type)
{
if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed");
if (IsConsumed) throw new InvalidOperationException("Query results must be consumed in the correct order, and each result can only be consumed once");
IsConsumed = true;
T result = default(T);
if(reader.Read())
{
var typedIdentity = identity.ForGrid(type, gridIndex);
CacheInfo cache = GetCacheInfo(typedIdentity, null, addToCache);
var deserializer = cache.Deserializer;
int hash = GetColumnHash(reader);
if (deserializer.Func == null || deserializer.Hash != hash)
{
deserializer = new DeserializerState(hash, GetDeserializer(type, reader, 0, -1, false));
cache.Deserializer = deserializer;
}
result = (T) deserializer.Func(reader);
while (reader.Read()) { }
}
NextResult();
return result;
}
private IEnumerable<TReturn> MultiReadInternal<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn>(Delegate func, string splitOn) private IEnumerable<TReturn> MultiReadInternal<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn>(Delegate func, string splitOn)
{ {
......
This diff is collapsed.
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0", "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0",
"summary": "A high performance Micro-ORM", "summary": "A high performance Micro-ORM",
"description": "A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc..", "description": "A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc..",
"version": "1.50-beta3", "version": "1.50-beta4",
"title": "Dapper dot net", "title": "Dapper dot net",
"tags": [ "orm", "sql", "micro-orm" ], "tags": [ "orm", "sql", "micro-orm" ],
"copyright": "2015 Stack Exchange, Inc.", "copyright": "2015 Stack Exchange, Inc.",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment