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
public void RecordHit() { Interlocked.Increment(ref hitCount); }
}
private static int totalErrorCount = 0;
private const int PurgeCacheAfterNErrors = 20;
public static event EventHandler QueryCachePurged;
private static void OnQueryCachePurged()
{
......@@ -135,29 +133,10 @@ private static bool TryGetQueryCache(Identity key, out CacheInfo 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()
{
lock (_queryCache)
{
totalErrorCount = 0;
_queryCache.Clear();
}
OnQueryCachePurged();
......@@ -205,20 +184,10 @@ private static bool TryGetQueryCache(Identity key, out CacheInfo value)
value = null;
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()
{
_queryCache.Clear();
Interlocked.Exchange(ref totalErrorCount, 0);
OnQueryCachePurged();
}
......@@ -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 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);
SetQueryCache(identity, info);
return (Func<IDataReader, T>)info.Deserializer;
};
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;
var next = deserializer(reader);
clean = true;
yield return next;
// 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 { }
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)
{
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 { }
ownedReader = ownedCommand.ExecuteReader();
reader = ownedReader;
}
object deserializer;
object[] otherDeserializers;
object deserializer = null;
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)
{
int current = 0;
cacheDeserializers();
}
var splits = splitOn.Split(',').ToArray();
var splitIndex = 0;
Func<IDataReader, TReturn> mapIt = GenerateMapper<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(deserializer, otherDeserializers, map);
Func<Type,int> nextSplit = type =>
if (mapIt != null)
{
while (reader.Read())
{
var currentSplit = splits[splitIndex];
if (splits.Length > splitIndex + 1)
{
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))
TReturn next;
try
{
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;
}
}
next = mapIt(reader);
}
int pos;
for (pos = startingPos; pos < reader.FieldCount; pos++)
catch (DataException)
{
// some people like ID some id ... assuming case insensitive splits for now
if (splitOn == "*")
{
break;
}
if (string.Equals(reader.GetName(pos), currentSplit, StringComparison.OrdinalIgnoreCase))
{
if (skipFirst)
{
skipFirst = false;
}
else
{
break;
}
}
cacheDeserializers();
mapIt = GenerateMapper<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(deserializer, otherDeserializers, map);
next = mapIt(reader);
}
current = pos;
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;
yield return next;
}
if (typeof(TFourth) != typeof(DontMap))
}
}
finally
{
try
{
if (ownedReader != null)
{
var next = nextSplit(typeof(TFourth));
otherDeserializer.Add(GetDeserializer<TFourth>(reader, split, next - split, true));
split = next;
ownedReader.Dispose();
}
if (typeof(TFifth) != typeof(DontMap))
}
finally
{
if (ownedCommand != null)
{
var next = nextSplit(typeof(TFifth));
otherDeserializer.Add(GetDeserializer<TFifth>(reader, split, next - split, true));
ownedCommand.Dispose();
}
otherDeserializers = cinfo.OtherDeserializers = otherDeserializer.ToArray();
SetQueryCache(identity, cinfo);
}
}
}
var rootDeserializer = (Func<IDataReader, TFirst>)deserializer;
var deserializer2 = (Func<IDataReader, TSecond>)otherDeserializers[0];
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 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 > 1)
if (otherDeserializers.Length > 2)
{
var deserializer3 = (Func<IDataReader, TThird>)otherDeserializers[1];
if (otherDeserializers.Length == 2)
var deserializer4 = (Func<IDataReader, TFourth>)otherDeserializers[2];
if (otherDeserializers.Length == 3)
{
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
throw new NotSupportedException();
#else
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));
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));
#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;
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);
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;
TReturn next = mapIt(reader);
clean = true;
yield return next;
skipFirst = true;
startingPos = current;
break;
}
}
finally
{
if (!clean) PurgeQueryCache(identity);
}
}
}
finally
{
try
int pos;
for (pos = startingPos; pos < reader.FieldCount; pos++)
{
if (ownedReader != null)
// some people like ID some id ... assuming case insensitive splits for now
if (splitOn == "*")
{
ownedReader.Dispose();
break;
}
}
finally
{
if (ownedCommand != null)
if (string.Equals(reader.GetName(pos), currentSplit, StringComparison.OrdinalIgnoreCase))
{
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)
......@@ -837,6 +813,35 @@ private static CacheInfo GetCacheInfo(Identity identity)
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)
{
Type type = typeof(T);
......@@ -1583,13 +1588,20 @@ public IEnumerable<T> Read<T>()
var typedIdentity = identity.ForGrid(typeof(T), gridIndex);
CacheInfo cache = GetCacheInfo(typedIdentity);
var deserializer = (Func<IDataReader, T>)cache.Deserializer;
if (deserializer == null)
Func<Func<IDataReader, T>> deserializerGenerator = () =>
{
deserializer = GetDeserializer<T>(reader, 0, -1, false);
cache.Deserializer = deserializer;
return deserializer;
};
if (deserializer == null)
{
deserializer = deserializerGenerator();
}
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)
......@@ -1650,26 +1662,27 @@ public IEnumerable<T> Read<T>()
}
#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
{
while (index == gridIndex && reader.Read())
{
clean = false;
T next = deserializer(reader);
clean = true;
T next;
try
{
next = deserializer(reader);
}
catch (DataException)
{
deserializer = deserializerGenerator();
next = deserializer(reader);
}
yield return next;
}
}
finally // finally so that First etc progresses things even when multiple rows
{
if (!clean)
{
PurgeQueryCache(typedIdentity);
}
if (index == gridIndex)
{
NextResult();
......
......@@ -7,6 +7,7 @@
using System.IO;
using System.Data;
using System.Collections;
using System.Reflection;
namespace SqlMapper
{
......@@ -36,6 +37,40 @@ public void PassInEmptyIntArray()
.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()
{
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