Dapper完美兼容Oracle,执行存储过程,并返回结果集。
这个问题,困扰了我整整两天。
刚刚用到Dapper的时候,感觉非常牛掰。特别是配合.net 4.0新特性dynamic,让我生成泛型集合,再转json一气呵成。
不过,各种ORM总有让人吐槽的地方。。。
比如,我之前在SqlServer上写测试,搞封装,没有任何问题。CURD、批量操作、存储过程、事物等。
可是以转到Oracle上,就出问题了【喂~不是说好的支持Oracle的么】
在写Dapper+Oracle单元测试的前期,是没有问题的,也就是说普通的Sql操作是没有任何问题的。
然后,我写到存储过程的单元测试的时候,就蛋疼了。
因为原版采用的DbType数据类型枚举。Sqlserver返回结果集并没有输出游标。
但是Oracle输出结果集,就需要用游标了。那么,这里问题就来了。给OracleParameter设置参数类型,DbType并没有Cursor游标类型。
关于Dapper的文档也是不多,而且大部分都集中在SqlServer上,可能应为服务于.Net平台,比较侧重于微软的配套数据库。
好吧,问题来了,那就解决。反正是开源的。源代码都有。
先根据问题来搜索【我不喜欢用百度,因为百度搜出来一大堆不相关的东西,铜臭味太重。google在国内有无法访问,我就选择了Bing,结果效果还不错。】
经过网上搜集,发现Dapper确实是支持Oracle的,但是对于调用Oracle存储过程的内容却没有。
好吧,没有的话,先自己分析分析。
既然是参数类型不支持,那么换成支持的不就成了?
原版的是这样的:
1 DynamicParameters dp = new DynamicParameters();
2 dp.Add("RoleId", "1");
3 dp.Add("RoleName", "", DbType.String, ParameterDirection.Output);
这是Dapper原版中,声明parameter的部分,上面代码红色部分,就是指定参数类型。
在system.data.oracleclient 中,有OracleType这个枚举有Cursor类型。
然后,去查看 DynamicParameters 类,如下图:
可以看到,这个类,是实现了一个接口的。说明,原作者给我们预留了接口去自己实现其他内容。
继续看看接口:
接口的内容很简单,就是一个AddParameters方法。
那么,可以确定,上面的猜测是对的。
我们直接扩展实现这个接口就可以了。如图:
自己去创建一个实现了IDynamicParameters的类OracleDynamicParameters。
然后参照原作者提供的DynamicParameters类来实现这个接口。
最终修改版如下(代码多,展开了直接复制代码贴到你的文件里面):
1 /*
2 License: http://www.apache.org/licenses/LICENSE-2.0
3 Home page: http://code.google.com/p/dapper-dot-net/
4
5 Note: to build on C# 3.0 + .NET 3.5, include the CSHARP30 compiler symbol (and yes,
6 I know the difference between language and runtime versions; this is a compromise).
7 *
8 * 增加Oracle存储过程支持
9 * 李科笠 2015年10月13日 17:43:54
10 */
11 using System;
12 using System.Collections;
13 using System.Collections.Generic;
14 using System.ComponentModel;
15 using System.Data;
16 using System.Linq;
17 using System.Reflection;
18 using System.Reflection.Emit;
19 using System.Text;
20 using System.Threading;
21 using System.Text.RegularExpressions;
22 using Oracle.DataAccess.Client;
23
24 namespace Dapper
25 {
26 public static partial class SqlMapper
27 {
28 public interface IDynamicParameters
29 {
30 void AddParameters(IDbCommand command, Identity identity);
31 }
32 static Link> bindByNameCache;
33 static Action GetBindByName(Type commandType)
34 {
35 if (commandType == null) return null; // GIGO
36 Action action;
37 if (Link>.TryGet(bindByNameCache, commandType, out action))
38 {
39 return action;
40 }
41 var prop = commandType.GetProperty("BindByName", BindingFlags.Public | BindingFlags.Instance);
42 action = null;
43 ParameterInfo[] indexers;
44 MethodInfo setter;
45 if (prop != null && prop.CanWrite && prop.PropertyType == typeof(bool)
46 && ((indexers = prop.GetIndexParameters()) == null || indexers.Length == 0)
47 && (setter = prop.GetSetMethod()) != null
48 )
49 {
50 var method = new DynamicMethod(commandType.Name + "_BindByName", null, new Type[] { typeof(IDbCommand), typeof(bool) });
51 var il = method.GetILGenerator();
52 il.Emit(OpCodes.Ldarg_0);
53 il.Emit(OpCodes.Castclass, commandType);
54 il.Emit(OpCodes.Ldarg_1);
55 il.EmitCall(OpCodes.Callvirt, setter, null);
56 il.Emit(OpCodes.Ret);
57 action = (Action)method.CreateDelegate(typeof(Action));
58 }
59 // cache it
60 Link>.TryAdd(ref bindByNameCache, commandType, ref action);
61 return action;
62 }
63 ///
64 /// This is a micro-cache; suitable when the number of terms is controllable (a few hundred, for example),
65 /// and strictly append-only; you cannot change existing values. All key matches are on **REFERENCE**
66 /// equality. The type is fully thread-safe.
67 ///
68 class Link where TKey : class
69 {
70 public static bool TryGet(Link link, TKey key, out TValue value)
71 {
72 while (link != null)
73 {
74 if ((object)key == (object)link.Key)
75 {
76 value = link.Value;
77 return true;
78 }
79 link = link.Tail;
80 }
81 value = default(TValue);
82 return false;
83 }
84 public static bool TryAdd(ref Link head, TKey key, ref TValue value)
85 {
86 bool tryAgain;
87 do
88 {
89 var snapshot = Interlocked.CompareExchange(ref head, null, null);
90 TValue found;
91 if (TryGet(snapshot, key, out found))
92 { // existing match; report the existing value instead
93 value = found;
94 return false;
95 }
96 var newNode = new Link(key, value, snapshot);
97 // did somebody move our cheese?
98 tryAgain = Interlocked.CompareExchange(ref head, newNode, snapshot) != snapshot;
99 } while (tryAgain);
100 return true;
101 }
102 private Link(TKey key, TValue value, Link tail)
103 {
104 Key = key;
105 Value = value;
106 Tail = tail;
107 }
108 public TKey Key { get; private set; }
109 public TValue Value { get; private set; }
110 public Link Tail { get; private set; }
111 }
112 class CacheInfo
113 {
114 public Func Deserializer { get; set; }
115 public Func[] OtherDeserializers { get; set; }
116 public Action ParamReader { get; set; }
117 private int hitCount;
118 public int GetHitCount() { return Interlocked.CompareExchange(ref hitCount, 0, 0); }
119 public void RecordHit() { Interlocked.Increment(ref hitCount); }
120 }
121
122 public static event EventHandler QueryCachePurged;
123 private static void OnQueryCachePurged()
124 {
125 var handler = QueryCachePurged;
126 if (handler != null) handler(null, EventArgs.Empty);
127 }
128 #if CSHARP30
129 private static readonly Dictionary _queryCache = new Dictionary();
130 // note: conflicts between readers and writers are so short-lived that it isn't worth the overhead of
131 // ReaderWriterLockSlim etc; a simple lock is faster
132 private static void SetQueryCache(Identity key, CacheInfo value)
133 {
134 lock (_queryCache) { _queryCache[key] = value; }
135 }
136 private static bool TryGetQueryCache(Identity key, out CacheInfo value)
137 {
138 lock (_queryCache) { return _queryCache.TryGetValue(key, out value); }
139 }
140 public static void PurgeQueryCache()
141 {
142 lock (_queryCache)
143 {
144 _queryCache.Clear();
145 }
146 OnQueryCachePurged();
147 }
148 #else
149 static readonly System.Collections.Concurrent.ConcurrentDictionary _queryCache = new System.Collections.Concurrent.ConcurrentDictionary();
150 private static void SetQueryCache(Identity key, CacheInfo value)
151 {
152 if (Interlocked.Increment(ref collect) == COLLECT_PER_ITEMS)
153 {
154 CollectCacheGarbage();
155 }
156 _queryCache[key] = value;
157 }
158
159 private static void CollectCacheGarbage()
160 {
161 try
162 {
163 foreach (var pair in _queryCache)
164 {
165 if (pair.Value.GetHitCount() <= COLLECT_HIT_COUNT_MIN)
166 {
167 CacheInfo cache;
168 _queryCache.TryRemove(pair.Key, out cache);
169 }
170 }
171 }
172
173 finally
174 {
175 Interlocked.Exchange(ref collect, 0);
176 }
177 }
178
179 private const int COLLECT_PER_ITEMS = 1000, COLLECT_HIT_COUNT_MIN = 0;
180 private static int collect;
181 private static bool TryGetQueryCache(Identity key, out CacheInfo value)
182 {
183 if (_queryCache.TryGetValue(key, out value))
184 {
185 value.RecordHit();
186 return true;
187 }
188 value = null;
189 return false;
190 }
191
192 public static void PurgeQueryCache()
193 {
194 _queryCache.Clear();
195 OnQueryCachePurged();
196 }
197
198 public static int GetCachedSQLCount()
199 {
200 return _queryCache.Count;
201 }
202
203
204 public static IEnumerable> GetCachedSQL(int ignoreHitCountAbove = int.MaxValue)
205 {
206 var data = _queryCache.Select(pair => Tuple.Create(pair.Key.connectionString, pair.Key.sql, pair.Value.GetHitCount()));
207 if (ignoreHitCountAbove < int.MaxValue) data = data.Where(tuple => tuple.Item3 <= ignoreHitCountAbove);
208 return data;
209 }
210
211 public static IEnumerable> GetHashCollissions()
212 {
213 var counts = new Dictionary();
214 foreach (var key in _queryCache.Keys)
215 {
216 int count;
217 if (!counts.TryGetValue(key.hashCode, out count))
218 {
219 counts.Add(key.hashCode, 1);
220 }
221 else
222 {
223 counts[key.hashCode] = count + 1;
224 }
225 }
226 return from pair in counts
227 where pair.Value > 1
228 select Tuple.Create(pair.Key, pair.Value);
229
230 }
231 #endif
232
233
234 static readonly Dictionary typeMap;
235
236 static SqlMapper()
237 {
238 typeMap = new Dictionary();
239 typeMap[typeof(byte)] = DbType.Byte;
240 typeMap[typeof(sbyte)] = DbType.SByte;
241 typeMap[typeof(short)] = DbType.Int16;
242 typeMap[typeof(ushort)] = DbType.UInt16;
243 typeMap[typeof(int)] = DbType.Int32;
244 typeMap[typeof(uint)] = DbType.UInt32;
245 typeMap[typeof(long)] = DbType.Int64;
246 typeMap[typeof(ulong)] = DbType.UInt64;
247 typeMap[typeof(float)] = DbType.Single;
248 typeMap[typeof(double)] = DbType.Double;
249 typeMap[typeof(decimal)] = DbType.Decimal;
250 typeMap[typeof(bool)] = DbType.Boolean;
251 typeMap[typeof(string)] = DbType.String;
252 typeMap[typeof(char)] = DbType.StringFixedLength;
253 typeMap[typeof(Guid)] = DbType.Guid;
254 typeMap[typeof(DateTime)] = DbType.DateTime;
255 typeMap[typeof(DateTimeOffset)] = DbType.DateTimeOffset;
256 typeMap[typeof(byte[])] = DbType.Binary;
257 typeMap[typeof(byte?)] = DbType.Byte;
258 typeMap[typeof(sbyte?)] = DbType.SByte;
259 typeMap[typeof(short?)] = DbType.Int16;
260 typeMap[typeof(ushort?)] = DbType.UInt16;
261 typeMap[typeof(int?)] = DbType.Int32;
262 typeMap[typeof(uint?)] = DbType.UInt32;
263 typeMap[typeof(long?)] = DbType.Int64;
264 typeMap[typeof(ulong?)] = DbType.UInt64;
265 typeMap[typeof(float?)] = DbType.Single;
266 typeMap[typeof(double?)] = DbType.Double;
267 typeMap[typeof(decimal?)] = DbType.Decimal;
268 typeMap[typeof(bool?)] = DbType.Boolean;
269 typeMap[typeof(char?)] = DbType.StringFixedLength;
270 typeMap[typeof(Guid?)] = DbType.Guid;
271 typeMap[typeof(DateTime?)] = DbType.DateTime;
272 typeMap[typeof(DateTimeOffset?)] = DbType.DateTimeOffset;
273 typeMap[typeof(System.Data.Linq.Binary)] = DbType.Binary;
274 }
275
276 private static DbType LookupDbType(Type type, string name)
277 {
278 DbType dbType;
279 var nullUnderlyingType = Nullable.GetUnderlyingType(type);
280 if (nullUnderlyingType != null) type = nullUnderlyingType;
281 if (type.IsEnum)
282 {
283 type = Enum.GetUnderlyingType(type);
284 }
285 if (typeMap.TryGetValue(type, out dbType))
286 {
287 return dbType;
288 }
289 if (typeof(IEnumerable).IsAssignableFrom(type))
290 {
291 // use xml to denote its a list, hacky but will work on any DB
292 return DbType.Xml;
293 }
294
295
296 throw new NotSupportedException(string.Format("The member {0} of type {1} cannot be used as a parameter value", name, type));
297 }
298
299 public class Identity : IEquatable
300 {
301 internal Identity ForGrid(Type primaryType, int gridIndex)
302 {
303 return new Identity(sql, commandType, connectionString, primaryType, parametersType, null, gridIndex);
304 }
305
306 internal Identity ForGrid(Type primaryType, Type[] otherTypes, int gridIndex)
307 {
308 return new Identity(sql, commandType, connectionString, primaryType, parametersType, otherTypes, gridIndex);
309 }
310
311 public Identity ForDynamicParameters(Type type)
312 {
313 return new Identity(sql, commandType, connectionString, this.type, type, null, -1);
314 }
315
316 internal Identity(string sql, CommandType? commandType, IDbConnection connection, Type type, Type parametersType, Type[] otherTypes)
317 : this(sql, commandType, connection.ConnectionString, type, parametersType, otherTypes, 0)
318 { }
319 private Identity(string sql, CommandType? commandType, string connectionString, Type type, Type parametersType, Type[] otherTypes, int gridIndex)
320 {
321 this.sql = sql;
322 this.commandType = commandType;
323 this.connectionString = connectionString;
324 this.type = type;
325 this.parametersType = parametersType;
326 this.gridIndex = gridIndex;
327 unchecked
328 {
329 hashCode = 17; // we *know* we are using this in a dictionary, so pre-compute this
330 hashCode = hashCode * 23 + commandType.GetHashCode();
331 hashCode = hashCode * 23 + gridIndex.GetHashCode();
332 hashCode = hashCode * 23 + (sql == null ? 0 : sql.GetHashCode());
333 hashCode = hashCode * 23 + (type == null ? 0 : type.GetHashCode());
334 if (otherTypes != null)
335 {
336 foreach (var t in otherTypes)
337 {
338 hashCode = hashCode * 23 + (t == null ? 0 : t.GetHashCode());
339 }
340 }
341 hashCode = hashCode * 23 + (connectionString == null ? 0 : connectionString.GetHashCode());
342 hashCode = hashCode * 23 + (parametersType == null ? 0 : parametersType.GetHashCode());
343 }
344 }
345 public override bool Equals(object obj)
346 {
347 return Equals(obj as Identity);
348 }
349 public readonly string sql;
350 public readonly CommandType? commandType;
351 public readonly int hashCode, gridIndex;
352 private readonly Type type;
353 public readonly string connectionString;
354 public readonly Type parametersType;
355 public override int GetHashCode()
356 {
357 return hashCode;
358 }
359 public bool Equals(Identity other)
360 {
361 return
362 other != null &&
363 gridIndex == other.gridIndex &&
364 type == other.type &&
365 sql == other.sql &&
366 commandType == other.commandType &&
367 connectionString == other.connectionString &&
368 parametersType == other.parametersType;
369 }
370 }
371
372 #if CSHARP30
373 ///
374 /// Execute parameterized SQL
375 ///
376 /// Number of rows affected
377 public static int Execute(this IDbConnection cnn, string sql, object param)
378 {
379 return Execute(cnn, sql, param, null, null, null);
380 }
381 ///
382 /// Executes a query, returning the data typed as per T
383 ///
384 /// the dynamic param may seem a bit odd, but this works around a major usability issue in vs, if it is Object vs completion gets annoying. Eg type new get new object
385 /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is
386 /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive).
387 ///
388 public static IEnumerable Query(this IDbConnection cnn, string sql, object param)
389 {
390 return Query(cnn, sql, param, null, true, null, null);
391 }
392
393 #endif
394 ///
395 /// Execute parameterized SQL
396 ///
397 /// Number of rows affected
398 public static int Execute(
399 #if CSHARP30
400 this IDbConnection cnn, string sql, object param, IDbTransaction transaction, int? commandTimeout, CommandType? commandType
401 #else
402 this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null
403 #endif
404 )
405 {
406 IEnumerable multiExec = (object)param as IEnumerable;
407 Identity identity;
408 CacheInfo info = null;
409 if (multiExec != null && !(multiExec is string))
410 {
411 bool isFirst = true;
412 int total = 0;
413 using (var cmd = SetupCommand(cnn, transaction, sql, null, null, commandTimeout, commandType))
414 {
415
416 string masterSql = null;
417 foreach (var obj in multiExec)
418 {
419 if (isFirst)
420 {
421 masterSql = cmd.CommandText;
422 isFirst = false;
423 identity = new Identity(sql, cmd.CommandType, cnn, null, obj.GetType(), null);
424 info = GetCacheInfo(identity);
425 }
426 else
427 {
428 cmd.CommandText = masterSql; // because we do magic replaces on "in" etc
429 cmd.Parameters.Clear(); // current code is Add-tastic
430 }
431 info.ParamReader(cmd, obj);
432 total += cmd.ExecuteNonQuery();
433 }
434 }
435 return total;
436 }
437
438 // nice and simple
439 identity = new Identity(sql, commandType, cnn, null, (object)param == null ? null : ((object)param).GetType(), null);
440 info = GetCacheInfo(identity);
441 return ExecuteCommand(cnn, transaction, sql, info.ParamReader, (object)param, commandTimeout, commandType);
442 }
443 #if !CSHARP30
444 ///
445 /// Return a list of dynamic objects, reader is closed after the call
446 ///
447 public static IEnumerable Query(this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null)
448 {
449 return Query(cnn, sql, param as object, transaction, buffered, commandTimeout, commandType);
450 }
451 #endif
452
453 ///
454 /// Executes a query, returning the data typed as per T
455 ///
456 /// the dynamic param may seem a bit odd, but this works around a major usability issue in vs, if it is Object vs completion gets annoying. Eg type new get new object
457 /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is
458 /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive).
459 ///
460 public static IEnumerable Query(
461 #if CSHARP30
462 this IDbConnection cnn, string sql, object param, IDbTransaction transaction, bool buffered, int? commandTimeout, CommandType? commandType
463 #else
464 this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null
465 #endif
466 )
467 {
468 var data = QueryInternal(cnn, sql, param as object, transaction, commandTimeout, commandType);
469 return buffered ? data.ToList() : data;
470 }
471
472 ///
473 /// Execute a command that returns multiple result sets, and access each in turn
474 ///
475 public static GridReader QueryMultiple(
476 #if CSHARP30
477 this IDbConnection cnn, string sql, object param, IDbTransaction transaction, int? commandTimeout, CommandType? commandType
478 #else
479 this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null
480 #endif
481 )
482 {
483 Identity identity = new Identity(sql, commandType, cnn, typeof(GridReader), (object)param == null ? null : ((object)param).GetType(), null);
484 CacheInfo info = GetCacheInfo(identity);
485
486 IDbCommand cmd = null;
487 IDataReader reader = null;
488 try
489 {
490 cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, (object)param, commandTimeout, commandType);
491 reader = cmd.ExecuteReader();
492 return new GridReader(cmd, reader, identity);
493 }
494 catch
495 {
496 if (reader != null) reader.Dispose();
497 if (cmd != null) cmd.Dispose();
498 throw;
499 }
500 }
501
502 ///
503 /// Return a typed list of objects, reader is closed after the call
504 ///
505 private static IEnumerable QueryInternal(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, int? commandTimeout, CommandType? commandType)
506 {
507 var identity = new Identity(sql, commandType, cnn, typeof(T), param == null ? null : param.GetType(), null);
508 var info = GetCacheInfo(identity);
509
510 using (var cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, param, commandTimeout, commandType))
511 {
512 using (var reader = cmd.ExecuteReader())
513 {
514 Func> cacheDeserializer = () =>
515 {
516 info.Deserializer = GetDeserializer(typeof(T), reader, 0, -1, false);
517 SetQueryCache(identity, info);
518 return info.Deserializer;
519 };
520
521 if (info.Deserializer == null)
522 {
523 cacheDeserializer();
524 }
525
526 var deserializer = info.Deserializer;
527
528 while (reader.Read())
529 {
530 object next;
531 try
532 {
533 next = deserializer(reader);
534 }
535 catch (DataException)
536 {
537 // give it another shot, in case the underlying schema changed
538 deserializer = cacheDeserializer();
539 next = deserializer(reader);
540 }
541 yield return (T)next;
542 }
543
544 }
545 }
546 }
547
548 ///
549 /// Maps a query to objects
550 ///
551 /// The return type
552 ///
553 ///
554 ///
555 ///
556 ///
557 ///
558 ///
559 /// The Field we should split and read the second object from (default: id)
560 /// Number of seconds before command execution timeout
561 ///
562 public static IEnumerable Query(
563 #if CSHARP30
564 this IDbConnection cnn, string sql, Func map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType
565 #else
566 this IDbConnection cnn, string sql, Func map, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null
567 #endif
568 )
569 {
570 return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType);
571 }
572
573 public static IEnumerable Query(
574 #if CSHARP30
575 this IDbConnection cnn, string sql, Func map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType
576 #else
577 this IDbConnection cnn, string sql, Func map, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null
578 #endif
579 )
580 {
581 return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType);
582 }
583
584 public static IEnumerable Query(
585 #if CSHARP30
586 this IDbConnection cnn, string sql, Func map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType
587 #else
588 this IDbConnection cnn, string sql, Func map, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null
589 #endif
590 )
591 {
592 return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType);
593 }
594 #if !CSHARP30
595 public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
596 {
597 return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType);
598 }
599 #endif
600 class DontMap { }
601 static IEnumerable MultiMap(
602 this IDbConnection cnn, string sql, object map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType)
603 {
604 var results = MultiMapImpl(cnn, sql, map, param, transaction, splitOn, commandTimeout, commandType, null, null);
605 return buffered ? results.ToList() : results;
606 }
607
608
609 static IEnumerable MultiMapImpl(this IDbConnection cnn, string sql, object map, object param, IDbTransaction transaction, string splitOn, int? commandTimeout, CommandType? commandType, IDataReader reader, Identity identity)
610 {
611 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) });
612 CacheInfo cinfo = GetCacheInfo(identity);
613
614 IDbCommand ownedCommand = null;
615 IDataReader ownedReader = null;
616
617 try
618 {
619 if (reader == null)
620 {
621 ownedCommand = SetupCommand(cnn, transaction, sql, cinfo.ParamReader, (object)param, commandTimeout, commandType);
622 ownedReader = ownedCommand.ExecuteReader();
623 reader = ownedReader;
624 }
625 Func deserializer = null;
626 Func[] otherDeserializers = null;
627
628 Action cacheDeserializers = () =>
629 {
630 var deserializers = GenerateDeserializers(new Type[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth) }, splitOn, reader);
631 deserializer = cinfo.Deserializer = deserializers[0];
632 otherDeserializers = cinfo.OtherDeserializers = deserializers.Skip(1).ToArray();
633 SetQueryCache(identity, cinfo);
634 };
635
636 if ((deserializer = cinfo.Deserializer) == null || (otherDeserializers = cinfo.OtherDeserializers) == null)
637 {
638 cacheDeserializers();
639 }
640
641 Func mapIt = GenerateMapper(deserializer, otherDeserializers, map);
642
643 if (mapIt != null)
644 {
645 while (reader.Read())
646 {
647 TReturn next;
648 try
649 {
650 next = mapIt(reader);
651 }
652 catch (DataException)
653 {
654 cacheDeserializers();
655 mapIt = GenerateMapper(deserializer, otherDeserializers, map);
656 next = mapIt(reader);
657 }
658 yield return next;
659 }
660 }
661 }
662 finally
663 {
664 try
665 {
666 if (ownedReader != null)
667 {
668 ownedReader.Dispose();
669 }
670 }
671 finally
672 {
673 if (ownedCommand != null)
674 {
675 ownedCommand.Dispose();
676 }
677 }
678 }
679 }
680
681 private static Func GenerateMapper(Func deserializer, Func[] otherDeserializers, object map)
682 {
683 switch (otherDeserializers.Length)
684 {
685 case 1:
686 return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r));
687 case 2:
688 return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r));
689 case 3:
690 return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r));
691 #if !CSHARP30
692 case 4:
693 return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r), (TFifth)otherDeserializers[3](r));
694 #endif
695 default:
696 throw new NotSupportedException();
697 }
698 }
699
700 private static Func[] GenerateDeserializers(Type[] types, string splitOn, IDataReader reader)
701 {
702 int current = 0;
703 var splits = splitOn.Split(',').ToArray();
704 var splitIndex = 0;
705
706 Func nextSplit = type =>
707 {
708 var currentSplit = splits[splitIndex];
709 if (splits.Length > splitIndex + 1)
710 {
711 splitIndex++;
712 }
713
714 bool skipFirst = false;
715 int startingPos = current + 1;
716 // if our current type has the split, skip the first time you see it.
717 if (type != typeof(Object))
718 {
719 var props = GetSettableProps(type);
720 var fields = GetSettableFields(type);
721
722 foreach (var name in props.Select(p => p.Name).Concat(fields.Select(f => f.Name)))
723 {
724 if (string.Equals(name, currentSplit, StringComparison.OrdinalIgnoreCase))
725 {
726 skipFirst = true;
727 startingPos = current;
728 break;
729 }
730 }
731
732 }
733
734 int pos;
735 for (pos = startingPos; pos < reader.FieldCount; pos++)
736 {
737 // some people like ID some id ... assuming case insensitive splits for now
738 if (splitOn == "*")
739 {
740 break;
741 }
742 if (string.Equals(reader.GetName(pos), currentSplit, StringComparison.OrdinalIgnoreCase))
743 {
744 if (skipFirst)
745 {
746 skipFirst = false;
747 }
748 else
749 {
750 break;
751 }
752 }
753 }
754 current = pos;
755 return pos;
756 };
757
758 var deserializers = new List>();
759 int split = 0;
760 bool first = true;
761 foreach (var type in types)
762 {
763 if (type != typeof(DontMap))
764 {
765 int next = nextSplit(type);
766 deserializers.Add(GetDeserializer(type, reader, split, next - split, /* returnNullIfFirstMissing: */ !first));
767 first = false;
768 split = next;
769 }
770 }
771
772 return deserializers.ToArray();
773 }
774
775 private static CacheInfo GetCacheInfo(Identity identity)
776 {
777 CacheInfo info;
778 if (!TryGetQueryCache(identity, out info))
779 {
780 info = new CacheInfo();
781 if (identity.parametersType != null)
782 {
783 if (typeof(IDynamicParameters).IsAssignableFrom(identity.parametersType))
784 {
785 info.ParamReader = (cmd, obj) => { (obj as IDynamicParameters).AddParameters(cmd, identity); };
786 }
787 else
788 {
789 info.ParamReader = CreateParamInfoGenerator(identity);
790 }
791 }
792 SetQueryCache(identity, info);
793 }
794 return info;
795 }
796
797 private static Func GetDeserializer(Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing)
798 {
799 #if !CSHARP30
800 // dynamic is passed in as Object ... by c# design
801 if (type == typeof(object)
802 || type == typeof(FastExpando))
803 {
804 return GetDynamicDeserializer(reader, startBound, length, returnNullIfFirstMissing);
805 }
806 #endif
807
808 if (type.IsClass && type != typeof(string) && type != typeof(byte[]) && type != typeof(System.Data.Linq.Binary))
809 {
810 return GetClassDeserializer(type, reader, startBound, length, returnNullIfFirstMissing);
811 }
812 return GetStructDeserializer(type, startBound);
813
814 }
815 #if !CSHARP30
816 private class FastExpando : System.Dynamic.DynamicObject, IDictionary
817 {
818 IDictionary data;
819
820 public static FastExpando Attach(IDictionary data)
821 {
822 return new FastExpando { data = data };
823 }
824
825 public override bool TrySetMember(System.Dynamic.SetMemberBinder binder, object value)
826 {
827 data[binder.Name] = value;
828 return true;
829 }
830
831 public override bool TryGetMember(System.Dynamic.GetMemberBinder binder, out object result)
832 {
833 return data.TryGetValue(binder.Name, out result);
834 }
835
836 #region IDictionary Members
837
838 void IDictionary.Add(string key, object value)
839 {
840 throw new NotImplementedException();
841 }
842
843 bool IDictionary.ContainsKey(string key)
844 {
845 return data.ContainsKey(key);
846 }
847
848 ICollection IDictionary.Keys
849 {
850 get { return data.Keys; }
851 }
852
853 bool IDictionary.Remove(string key)
854 {
855 throw new NotImplementedException();
856 }
857
858 bool IDictionary.TryGetValue(string key, out object value)
859 {
860 return data.TryGetValue(key, out value);
861 }
862
863 ICollection
View Code
ok,扩展写完了,来一个单元测试,试一试:
1 ///
2 /// 执行带参数存储过程,并返回结果
3 ///
4 public static void ExectPro()
5 {
6 var p = new OracleDynamicParameters();
7 p.Add("beginTime", 201501);
8 p.Add("endTime", 201512);
9 p.Add("targetColumn", "tax");
10 p.Add("vCur", OracleDbType.RefCursor, ParameterDirection.Output);
11 using (IDbConnection conn = new OracleConnection(SqlConnOdp))
12 {
13 conn.Open();
14 var aa = conn.Query("p_123c", param: p, commandType: CommandType.StoredProcedure).ToList();
15 aa.ForEach(m => Console.WriteLine(m.C_NAME));
16 }
17 Console.ReadLine();
18 }
结果执行通过,并打印了首列的所有值。
那么,Dapper的简单扩展就完成了。
写在后面
补充说明: 我用的Oracle驱动是ODP.NET,.net是4.0
这个ODP.NET的Oracle.DataAccess.dll推荐从你的目标服务器,复制回来,不要用本地的,反正我用本地的,就提示外部程序错误。猜测是版本问题或者是位数问题。
相关参考文章
http://stackoverflow.com/questions/6212992/using-dapper-with-oracle
https://stackoverflow.com/questions/15943389/using-dapper-with-oracle-user-defined-types
http://stackoverflow.com/questions/7390015/using-dapper-with-oracle-stored-procedures-which-return-cursors