Commit 432ae180 authored by Sam Saffron's avatar Sam Saffron

reorganised the multi mapper

added support for schema changes - schema can now change and dapper will re-generate the deserializers.
parent a1d6b265
...@@ -115,8 +115,6 @@ class CacheInfo ...@@ -115,8 +115,6 @@ class CacheInfo
public void RecordHit() { Interlocked.Increment(ref hitCount); } public void RecordHit() { Interlocked.Increment(ref hitCount); }
} }
private static int totalErrorCount = 0;
private const int PurgeCacheAfterNErrors = 20;
public static event EventHandler QueryCachePurged; public static event EventHandler QueryCachePurged;
private static void OnQueryCachePurged() private static void OnQueryCachePurged()
{ {
...@@ -135,29 +133,10 @@ private static bool TryGetQueryCache(Identity key, out CacheInfo value) ...@@ -135,29 +133,10 @@ private static bool TryGetQueryCache(Identity key, out CacheInfo value)
{ {
lock (_queryCache) { return _queryCache.TryGetValue(key, out value); } lock (_queryCache) { return _queryCache.TryGetValue(key, out value); }
} }
private static void PurgeQueryCache(Identity key)
{
bool purged = false;
lock (_queryCache)
{
if (++totalErrorCount >= PurgeCacheAfterNErrors)
{
totalErrorCount = 0;
_queryCache.Clear();
purged = true;
}
else
{
_queryCache.Remove(key);
}
}
if(purged) OnQueryCachePurged();
}
public static void PurgeQueryCache() public static void PurgeQueryCache()
{ {
lock (_queryCache) lock (_queryCache)
{ {
totalErrorCount = 0;
_queryCache.Clear(); _queryCache.Clear();
} }
OnQueryCachePurged(); OnQueryCachePurged();
...@@ -205,20 +184,10 @@ private static bool TryGetQueryCache(Identity key, out CacheInfo value) ...@@ -205,20 +184,10 @@ private static bool TryGetQueryCache(Identity key, out CacheInfo value)
value = null; value = null;
return false; return false;
} }
private static void PurgeQueryCache(Identity key)
{
if(Interlocked.Increment(ref totalErrorCount) >= PurgeCacheAfterNErrors)
{
PurgeQueryCache();
} else {
CacheInfo info;
_queryCache.TryRemove(key, out info);
}
}
public static void PurgeQueryCache() public static void PurgeQueryCache()
{ {
_queryCache.Clear(); _queryCache.Clear();
Interlocked.Exchange(ref totalErrorCount, 0);
OnQueryCachePurged(); OnQueryCachePurged();
} }
...@@ -532,36 +501,41 @@ private static IEnumerable<T> QueryInternal<T>(this IDbConnection cnn, string sq ...@@ -532,36 +501,41 @@ private static IEnumerable<T> QueryInternal<T>(this IDbConnection cnn, string sq
{ {
var identity = new Identity(sql, commandType, cnn, typeof(T), param == null ? null : param.GetType(), null); var identity = new Identity(sql, commandType, cnn, typeof(T), param == null ? null : param.GetType(), null);
var info = GetCacheInfo(identity); var info = GetCacheInfo(identity);
bool clean = true;
try using (var cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, param, commandTimeout, commandType))
{ {
using (var cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, param, commandTimeout, commandType)) using (var reader = cmd.ExecuteReader())
{ {
using (var reader = cmd.ExecuteReader()) Func<Func<IDataReader, T>> cacheDeserializer = () =>
{ {
if (info.Deserializer == null) info.Deserializer = GetDeserializer<T>(reader, 0, -1, false);
{ SetQueryCache(identity, info);
info.Deserializer = GetDeserializer<T>(reader, 0, -1, false); return (Func<IDataReader, T>)info.Deserializer;
SetQueryCache(identity, info); };
}
var deserializer = (Func<IDataReader, T>)info.Deserializer; if (info.Deserializer == null)
{
cacheDeserializer();
}
var deserializer = (Func<IDataReader, T>)info.Deserializer;
while (reader.Read()) while (reader.Read())
{
T next;
try
{
next = deserializer(reader);
}
catch (DataException)
{ {
clean = false; // give it another shot, in case the underlying schema changed
var next = deserializer(reader); deserializer = cacheDeserializer();
clean = true; next = deserializer(reader);
yield return next;
} }
yield return next;
} }
}
}
finally
{ // throw away query plan on failure - could
if (!clean)
{
PurgeQueryCache(identity);
} }
} }
} }
...@@ -626,6 +600,7 @@ class DontMap { } ...@@ -626,6 +600,7 @@ class DontMap { }
return buffered ? results.ToList() : results; return buffered ? results.ToList() : results;
} }
static IEnumerable<TReturn> MultiMapImpl<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(this IDbConnection cnn, string sql, object map, object param, IDbTransaction transaction, string splitOn, int? commandTimeout, CommandType? commandType, IDataReader reader, Identity identity) static IEnumerable<TReturn> MultiMapImpl<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(this IDbConnection cnn, string sql, object map, object param, IDbTransaction transaction, string splitOn, int? commandTimeout, CommandType? commandType, IDataReader reader, Identity identity)
{ {
identity = identity ?? new Identity(sql, commandType, cnn, typeof(TFirst), (object)param == null ? null : ((object)param).GetType(), new[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth) }); identity = identity ?? new Identity(sql, commandType, cnn, typeof(TFirst), (object)param == null ? null : ((object)param).GetType(), new[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth) });
...@@ -642,177 +617,178 @@ class DontMap { } ...@@ -642,177 +617,178 @@ class DontMap { }
ownedReader = ownedCommand.ExecuteReader(); ownedReader = ownedCommand.ExecuteReader();
reader = ownedReader; reader = ownedReader;
} }
object deserializer; object deserializer = null;
object[] otherDeserializers; object[] otherDeserializers = null;
Action cacheDeserializers = () =>
{
var deserializers = GenerateDeserializers(new Type[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth)}, splitOn, reader);
deserializer = cinfo.Deserializer = deserializers[0];
otherDeserializers = cinfo.OtherDeserializers = deserializers.Skip(1).ToArray();
SetQueryCache(identity, cinfo);
};
if ((deserializer = cinfo.Deserializer) == null || (otherDeserializers = cinfo.OtherDeserializers) == null) if ((deserializer = cinfo.Deserializer) == null || (otherDeserializers = cinfo.OtherDeserializers) == null)
{ {
int current = 0; cacheDeserializers();
}
var splits = splitOn.Split(',').ToArray(); Func<IDataReader, TReturn> mapIt = GenerateMapper<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(deserializer, otherDeserializers, map);
var splitIndex = 0;
Func<Type,int> nextSplit = type => if (mapIt != null)
{
while (reader.Read())
{ {
var currentSplit = splits[splitIndex]; TReturn next;
if (splits.Length > splitIndex + 1) try
{
splitIndex++;
}
bool skipFirst = false;
int startingPos = current + 1;
// if our current type has the split, skip the first time you see it.
if (type != typeof(Object))
{ {
var props = GetSettableProps(type); next = mapIt(reader);
var fields = GetSettableFields(type);
foreach (var name in props.Select(p => p.Name).Concat(fields.Select(f => f.Name)))
{
if (string.Equals(name, currentSplit, StringComparison.OrdinalIgnoreCase))
{
skipFirst = true;
startingPos = current;
break;
}
}
} }
catch (DataException)
int pos;
for (pos = startingPos; pos < reader.FieldCount; pos++)
{ {
// some people like ID some id ... assuming case insensitive splits for now cacheDeserializers();
if (splitOn == "*") mapIt = GenerateMapper<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(deserializer, otherDeserializers, map);
{ next = mapIt(reader);
break;
}
if (string.Equals(reader.GetName(pos), currentSplit, StringComparison.OrdinalIgnoreCase))
{
if (skipFirst)
{
skipFirst = false;
}
else
{
break;
}
}
} }
current = pos; yield return next;
return pos;
};
var otherDeserializer = new List<object>();
int split = nextSplit(typeof(TFirst));
deserializer = cinfo.Deserializer = GetDeserializer<TFirst>(reader, 0, split, false);
if (typeof(TSecond) != typeof(DontMap))
{
var next = nextSplit(typeof(TSecond));
otherDeserializer.Add(GetDeserializer<TSecond>(reader, split, next - split, true));
split = next;
}
if (typeof(TThird) != typeof(DontMap))
{
var next = nextSplit(typeof(TThird));
otherDeserializer.Add(GetDeserializer<TThird>(reader, split, next - split, true));
split = next;
} }
if (typeof(TFourth) != typeof(DontMap)) }
}
finally
{
try
{
if (ownedReader != null)
{ {
var next = nextSplit(typeof(TFourth)); ownedReader.Dispose();
otherDeserializer.Add(GetDeserializer<TFourth>(reader, split, next - split, true));
split = next;
} }
if (typeof(TFifth) != typeof(DontMap)) }
finally
{
if (ownedCommand != null)
{ {
var next = nextSplit(typeof(TFifth)); ownedCommand.Dispose();
otherDeserializer.Add(GetDeserializer<TFifth>(reader, split, next - split, true));
} }
otherDeserializers = cinfo.OtherDeserializers = otherDeserializer.ToArray();
SetQueryCache(identity, cinfo);
} }
}
}
var rootDeserializer = (Func<IDataReader, TFirst>)deserializer; private static Func<IDataReader, TReturn> GenerateMapper<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(object deserializer, object[] otherDeserializers, object map)
var deserializer2 = (Func<IDataReader, TSecond>)otherDeserializers[0]; {
var rootDeserializer = (Func<IDataReader, TFirst>)deserializer;
var deserializer2 = (Func<IDataReader, TSecond>)otherDeserializers[0];
Func<IDataReader, TReturn> mapIt = null;
Func<IDataReader, TReturn> mapIt = null; if (otherDeserializers.Length == 1)
{
mapIt = r => ((Func<TFirst, TSecond, TReturn>)map)(rootDeserializer(r), deserializer2(r));
}
if (otherDeserializers.Length == 1) if (otherDeserializers.Length > 1)
{
var deserializer3 = (Func<IDataReader, TThird>)otherDeserializers[1];
if (otherDeserializers.Length == 2)
{ {
mapIt = r => ((Func<TFirst, TSecond, TReturn>)map)(rootDeserializer(r), deserializer2(r)); mapIt = r => ((Func<TFirst, TSecond, TThird, TReturn>)map)(rootDeserializer(r), deserializer2(r), deserializer3(r));
} }
if (otherDeserializers.Length > 2)
if (otherDeserializers.Length > 1)
{ {
var deserializer3 = (Func<IDataReader, TThird>)otherDeserializers[1]; var deserializer4 = (Func<IDataReader, TFourth>)otherDeserializers[2];
if (otherDeserializers.Length == 3)
if (otherDeserializers.Length == 2)
{ {
mapIt = r => ((Func<TFirst, TSecond, TThird, TReturn>)map)(rootDeserializer(r), deserializer2(r), deserializer3(r)); mapIt = r => ((Func<TFirst, TSecond, TThird, TFourth, TReturn>)map)(rootDeserializer(r), deserializer2(r), deserializer3(r), deserializer4(r));
} }
if (otherDeserializers.Length > 2)
{
var deserializer4 = (Func<IDataReader, TFourth>)otherDeserializers[2];
if (otherDeserializers.Length == 3)
{
mapIt = r => ((Func<TFirst, TSecond, TThird, TFourth, TReturn>)map)(rootDeserializer(r), deserializer2(r), deserializer3(r), deserializer4(r));
}
if (otherDeserializers.Length > 3) if (otherDeserializers.Length > 3)
{ {
#if CSHARP30 #if CSHARP30
throw new NotSupportedException(); throw new NotSupportedException();
#else #else
var deserializer5 = (Func<IDataReader, TFifth>)otherDeserializers[3]; var deserializer5 = (Func<IDataReader, TFifth>)otherDeserializers[3];
mapIt = r => ((Func<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>)map)(rootDeserializer(r), deserializer2(r), deserializer3(r), deserializer4(r), deserializer5(r)); mapIt = r => ((Func<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>)map)(rootDeserializer(r), deserializer2(r), deserializer3(r), deserializer4(r), deserializer5(r));
#endif #endif
}
} }
} }
}
if (mapIt != null) return mapIt;
}
private static object[] GenerateDeserializers(Type[] types, string splitOn, IDataReader reader)
{
int current = 0;
var splits = splitOn.Split(',').ToArray();
var splitIndex = 0;
Func<Type, int> nextSplit = type =>
{
var currentSplit = splits[splitIndex];
if (splits.Length > splitIndex + 1)
{ {
bool clean = true; splitIndex++;
try }
bool skipFirst = false;
int startingPos = current + 1;
// if our current type has the split, skip the first time you see it.
if (type != typeof(Object))
{
var props = GetSettableProps(type);
var fields = GetSettableFields(type);
foreach (var name in props.Select(p => p.Name).Concat(fields.Select(f => f.Name)))
{ {
while (reader.Read()) if (string.Equals(name, currentSplit, StringComparison.OrdinalIgnoreCase))
{ {
clean = false; skipFirst = true;
TReturn next = mapIt(reader); startingPos = current;
clean = true; break;
yield return next;
} }
} }
finally
{
if (!clean) PurgeQueryCache(identity);
}
} }
}
finally int pos;
{ for (pos = startingPos; pos < reader.FieldCount; pos++)
try
{ {
if (ownedReader != null) // some people like ID some id ... assuming case insensitive splits for now
if (splitOn == "*")
{ {
ownedReader.Dispose(); break;
} }
} if (string.Equals(reader.GetName(pos), currentSplit, StringComparison.OrdinalIgnoreCase))
finally
{
if (ownedCommand != null)
{ {
ownedCommand.Dispose(); if (skipFirst)
{
skipFirst = false;
}
else
{
break;
}
} }
} }
current = pos;
return pos;
};
var deserializers = new List<object>();
int split = 0;
bool first = true;
foreach (var type in types)
{
if (type != typeof(DontMap))
{
int next = nextSplit(type);
deserializers.Add(GetDeserializer(type, reader, split, next - split, /* returnNullIfFirstMissing: */ !first));
first = false;
split = next;
}
} }
return deserializers.ToArray();
} }
private static CacheInfo GetCacheInfo(Identity identity) private static CacheInfo GetCacheInfo(Identity identity)
...@@ -837,6 +813,35 @@ private static CacheInfo GetCacheInfo(Identity identity) ...@@ -837,6 +813,35 @@ private static CacheInfo GetCacheInfo(Identity identity)
return info; return info;
} }
static MethodInfo getDeserializerMethodInfo;
private static object GetDeserializer(Type t, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing)
{
if (getDeserializerMethodInfo == null)
{
foreach (var mi in typeof(SqlMapper).GetMethods(BindingFlags.NonPublic | BindingFlags.Static))
{
if (mi.Name == "GetDeserializer" && mi.IsGenericMethodDefinition)
{
getDeserializerMethodInfo = mi;
}
}
}
var method = getDeserializerMethodInfo.MakeGenericMethod(t);
try
{
return method.Invoke(null, new object[] { reader, startBound, length, returnNullIfFirstMissing });
}
catch (TargetInvocationException ex)
{
if (ex.InnerException != null && ex.InnerException.GetType() == typeof(ArgumentException))
{
throw ex.InnerException;
}
throw;
}
}
private static Func<IDataReader, T> GetDeserializer<T>(IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing) private static Func<IDataReader, T> GetDeserializer<T>(IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing)
{ {
Type type = typeof(T); Type type = typeof(T);
...@@ -1583,13 +1588,20 @@ public IEnumerable<T> Read<T>() ...@@ -1583,13 +1588,20 @@ public IEnumerable<T> Read<T>()
var typedIdentity = identity.ForGrid(typeof(T), gridIndex); var typedIdentity = identity.ForGrid(typeof(T), gridIndex);
CacheInfo cache = GetCacheInfo(typedIdentity); CacheInfo cache = GetCacheInfo(typedIdentity);
var deserializer = (Func<IDataReader, T>)cache.Deserializer; var deserializer = (Func<IDataReader, T>)cache.Deserializer;
if (deserializer == null)
Func<Func<IDataReader, T>> deserializerGenerator = () =>
{ {
deserializer = GetDeserializer<T>(reader, 0, -1, false); deserializer = GetDeserializer<T>(reader, 0, -1, false);
cache.Deserializer = deserializer; cache.Deserializer = deserializer;
return deserializer;
};
if (deserializer == null)
{
deserializer = deserializerGenerator();
} }
consumed = true; consumed = true;
return ReadDeferred(gridIndex, deserializer, typedIdentity); return ReadDeferred(gridIndex, deserializer, typedIdentity, deserializerGenerator);
} }
private IEnumerable<TReturn> MultiReadInternal<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(object func, string splitOn) private IEnumerable<TReturn> MultiReadInternal<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(object func, string splitOn)
...@@ -1650,26 +1662,27 @@ public IEnumerable<T> Read<T>() ...@@ -1650,26 +1662,27 @@ public IEnumerable<T> Read<T>()
} }
#endif #endif
private IEnumerable<T> ReadDeferred<T>(int index, Func<IDataReader, T> deserializer, Identity typedIdentity) private IEnumerable<T> ReadDeferred<T>(int index, Func<IDataReader, T> deserializer, Identity typedIdentity, Func<Func<IDataReader, T>> deserializerGenerator)
{ {
bool clean = true;
try try
{ {
while (index == gridIndex && reader.Read()) while (index == gridIndex && reader.Read())
{ {
clean = false; T next;
T next = deserializer(reader); try
clean = true; {
next = deserializer(reader);
}
catch (DataException)
{
deserializer = deserializerGenerator();
next = deserializer(reader);
}
yield return next; yield return next;
} }
} }
finally // finally so that First etc progresses things even when multiple rows finally // finally so that First etc progresses things even when multiple rows
{ {
if (!clean)
{
PurgeQueryCache(typedIdentity);
}
if (index == gridIndex) if (index == gridIndex)
{ {
NextResult(); NextResult();
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
using System.IO; using System.IO;
using System.Data; using System.Data;
using System.Collections; using System.Collections;
using System.Reflection;
namespace SqlMapper namespace SqlMapper
{ {
...@@ -36,6 +37,40 @@ public void PassInEmptyIntArray() ...@@ -36,6 +37,40 @@ public void PassInEmptyIntArray()
.IsSequenceEqualTo(new int[0]); .IsSequenceEqualTo(new int[0]);
} }
public void TestSchemaChanged()
{
connection.Execute("create table #dog(Age int, Name nvarchar(max)) insert #dog values(1, 'Alf')");
var d = connection.Query<Dog>("select * from #dog").Single();
d.Name.IsEqualTo("Alf");
d.Age.IsEqualTo(1);
connection.Execute("alter table #dog drop column Name");
d = connection.Query<Dog>("select * from #dog").Single();
d.Name.IsNull();
d.Age.IsEqualTo(1);
connection.Execute("drop table #dog");
}
public void TestSchemaChangedMultiMap()
{
connection.Execute("create table #dog(Age int, Name nvarchar(max)) insert #dog values(1, 'Alf')");
var tuple = connection.Query<Dog,Dog,Tuple<Dog, Dog>>("select * from #dog d1 join #dog d2 on 1=1", (d1,d2) => Tuple.Create(d1, d2), splitOn: "Age").Single();
tuple.Item1.Name.IsEqualTo("Alf");
tuple.Item1.Age.IsEqualTo(1);
tuple.Item2.Name.IsEqualTo("Alf");
tuple.Item2.Age.IsEqualTo(1);
connection.Execute("alter table #dog drop column Name");
tuple = connection.Query<Dog, Dog,Tuple<Dog, Dog>>("select * from #dog d1 join #dog d2 on 1=1", (d1, d2) => Tuple.Create(d1, d2), splitOn: "Age").Single();
tuple.Item1.Name.IsNull();
tuple.Item1.Age.IsEqualTo(1);
tuple.Item2.Name.IsNull();
tuple.Item2.Age.IsEqualTo(1);
connection.Execute("drop table #dog");
}
public void TestReadMultipleIntegersWithSplitOnAny() public void TestReadMultipleIntegersWithSplitOnAny()
{ {
connection.Query<int, int, int, Tuple<int, int, int>>( connection.Query<int, int, int, Tuple<int, int, int>>(
......
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