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())
{ {
if (info.Deserializer == null) Func<Func<IDataReader, T>> cacheDeserializer = () =>
{ {
info.Deserializer = GetDeserializer<T>(reader, 0, -1, false); info.Deserializer = GetDeserializer<T>(reader, 0, -1, false);
SetQueryCache(identity, info); SetQueryCache(identity, info);
return (Func<IDataReader, T>)info.Deserializer;
};
if (info.Deserializer == null)
{
cacheDeserializer();
} }
var deserializer = (Func<IDataReader, T>)info.Deserializer; var deserializer = (Func<IDataReader, T>)info.Deserializer;
while (reader.Read()) while (reader.Read())
{ {
clean = false; T next;
var next = deserializer(reader); try
clean = true; {
yield return next; next = deserializer(reader);
}
} }
catch (DataException)
{
// give it another shot, in case the underlying schema changed
deserializer = cacheDeserializer();
next = deserializer(reader);
} }
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,106 +617,67 @@ class DontMap { } ...@@ -642,106 +617,67 @@ class DontMap { }
ownedReader = ownedCommand.ExecuteReader(); ownedReader = ownedCommand.ExecuteReader();
reader = ownedReader; reader = ownedReader;
} }
object deserializer; object deserializer = null;
object[] otherDeserializers; object[] otherDeserializers = null;
if ((deserializer = cinfo.Deserializer) == null || (otherDeserializers = cinfo.OtherDeserializers) == null)
{
int current = 0;
var splits = splitOn.Split(',').ToArray(); Action cacheDeserializers = () =>
var splitIndex = 0;
Func<Type,int> nextSplit = type =>
{ {
var currentSplit = splits[splitIndex]; var deserializers = GenerateDeserializers(new Type[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth)}, splitOn, reader);
if (splits.Length > splitIndex + 1) deserializer = cinfo.Deserializer = deserializers[0];
otherDeserializers = cinfo.OtherDeserializers = deserializers.Skip(1).ToArray();
SetQueryCache(identity, cinfo);
};
if ((deserializer = cinfo.Deserializer) == null || (otherDeserializers = cinfo.OtherDeserializers) == null)
{ {
splitIndex++; cacheDeserializers();
} }
bool skipFirst = false; Func<IDataReader, TReturn> mapIt = GenerateMapper<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(deserializer, otherDeserializers, map);
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))) if (mapIt != null)
{ {
if (string.Equals(name, currentSplit, StringComparison.OrdinalIgnoreCase)) while (reader.Read())
{ {
skipFirst = true; TReturn next;
startingPos = current; try
break; {
next = mapIt(reader);
} }
catch (DataException)
{
cacheDeserializers();
mapIt = GenerateMapper<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(deserializer, otherDeserializers, map);
next = mapIt(reader);
} }
yield return next;
} }
int pos;
for (pos = startingPos; pos < reader.FieldCount; pos++)
{
// some people like ID some id ... assuming case insensitive splits for now
if (splitOn == "*")
{
break;
} }
if (string.Equals(reader.GetName(pos), currentSplit, StringComparison.OrdinalIgnoreCase)) }
finally
{ {
if (skipFirst) try
{ {
skipFirst = false; if (ownedReader != null)
}
else
{ {
break; ownedReader.Dispose();
}
} }
} }
current = pos; finally
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)); if (ownedCommand != null)
otherDeserializer.Add(GetDeserializer<TSecond>(reader, split, next - split, true));
split = next;
}
if (typeof(TThird) != typeof(DontMap))
{ {
var next = nextSplit(typeof(TThird)); ownedCommand.Dispose();
otherDeserializer.Add(GetDeserializer<TThird>(reader, split, next - split, true));
split = next;
} }
if (typeof(TFourth) != typeof(DontMap))
{
var next = nextSplit(typeof(TFourth));
otherDeserializer.Add(GetDeserializer<TFourth>(reader, split, next - split, true));
split = next;
} }
if (typeof(TFifth) != typeof(DontMap))
{
var next = nextSplit(typeof(TFifth));
otherDeserializer.Add(GetDeserializer<TFifth>(reader, split, next - split, true));
} }
otherDeserializers = cinfo.OtherDeserializers = otherDeserializer.ToArray();
SetQueryCache(identity, cinfo);
} }
private static Func<IDataReader, TReturn> GenerateMapper<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(object deserializer, object[] otherDeserializers, object map)
{
var rootDeserializer = (Func<IDataReader, TFirst>)deserializer; var rootDeserializer = (Func<IDataReader, TFirst>)deserializer;
var deserializer2 = (Func<IDataReader, TSecond>)otherDeserializers[0]; var deserializer2 = (Func<IDataReader, TSecond>)otherDeserializers[0];
Func<IDataReader, TReturn> mapIt = null; Func<IDataReader, TReturn> mapIt = null;
if (otherDeserializers.Length == 1) if (otherDeserializers.Length == 1)
...@@ -777,42 +713,82 @@ class DontMap { } ...@@ -777,42 +713,82 @@ class DontMap { }
} }
} }
if (mapIt != null) return mapIt;
}
private static object[] GenerateDeserializers(Type[] types, string splitOn, IDataReader reader)
{ {
bool clean = true; int current = 0;
try var splits = splitOn.Split(',').ToArray();
var splitIndex = 0;
Func<Type, int> nextSplit = type =>
{ {
while (reader.Read()) var currentSplit = splits[splitIndex];
if (splits.Length > splitIndex + 1)
{ {
clean = false; splitIndex++;
TReturn next = mapIt(reader);
clean = true;
yield return next;
}
} }
finally
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))
{ {
if (!clean) PurgeQueryCache(identity); var props = GetSettableProps(type);
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;
} }
} }
} }
finally
int pos;
for (pos = startingPos; pos < reader.FieldCount; pos++)
{ {
try // some people like ID some id ... assuming case insensitive splits for now
if (splitOn == "*")
{ {
if (ownedReader != null) break;
}
if (string.Equals(reader.GetName(pos), currentSplit, StringComparison.OrdinalIgnoreCase))
{ {
ownedReader.Dispose(); if (skipFirst)
{
skipFirst = false;
} }
else
{
break;
} }
finally }
}
current = pos;
return pos;
};
var deserializers = new List<object>();
int split = 0;
bool first = true;
foreach (var type in types)
{ {
if (ownedCommand != null) if (type != typeof(DontMap))
{ {
ownedCommand.Dispose(); 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