ようこそ。睡眠不足なプログラマのチラ裏です。

型なしDataSetと型付DataSet、そして片想いORマッピング的な何か。

ネタ元
山本大@クロノスの日記 - アンチ型付DataSet派の自作ORマッピングより

なぜ型付DataSetを使うのか、型なしDataSetは使わないのか

型付DataSetについては、嫌な思い出も少なくないので不満がないわけでもないのだけど、
常識的に考えて「型付DataSetを使うべきだよね。*1」というのは、もちろん知識として知っているつもり。
その理由は大きく3つあって、まずIDEのインテリセンスによってプログラミングの効率化が期待できること。
そして、コンパイル時に型チェックが行われるため、実行時の型変換エラーを未然に防げること。
また、コンパイル時に型が決定されているため実行時の列へのアクセスパフォーマンスが向上すること。
いずれも、システム開発をする上では生唾モノのおいしいメリットであるから、部分的に型なしDataSetを利用することはあるものの、
なるべく型付DataSetを利用したいというのは、健全な技術者の正論でしょう。

    / ̄ ̄\
  /   _ノ  \
  |    ( ●)(●)
 . |     (__人__)   おまいらなんで型なしDataSetなんて使ってんの?
   |     ` ⌒´ノ    型付DataSet使うしかないだろ、常識的に考えて…
 .  |         }
 .  ヽ        }
    ヽ     ノ        \
    /    く  \        \
    |     \   \         \
     |    |ヽ、二⌒)、          \

 

しかし、開発期間や開発メンバのスキルなど様々な事情により、型なしDataSetを標準的に使うことを選択する場合もある。
そのような場合、id:iad_otomamayさんの記事のアイディアにあるようにリフレクションを利用することで、
IDEのインテリセンスを有効にすることができ、イイ感じに型なしDataSetのデータを扱うことができる。
ただ、DataSetのデータを扱う場合、どうしても「DBNull.ValueとNullable<>型の扱いはどーすんのさ」という話がついてまわる。
というわけで、せっかくなので、それについても出来れば対応しておきたい。


参考:(MSDN).NET Framework 開発者ガイド NULL 値の処理 (ADO.NET)


DataRowに片想いORマッピングするDataRowTypeMapperクラス

で、やってみた。
Nullable型のFieldおよびPropertyに対してマッピングすることができる。
なお、サンプルはC#3.0で書いてますが、LINQを使わなければC#2.0でも可能な内容です。

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;

namespace ClassLibrary1
{
    /// <summary>
    /// DataRowTypeMapper
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class DataRowTypeMapper<T> where T : new()
    {
        private DataTable _dt;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="dt"></param>
        public DataRowTypeMapper(DataTable dt)
        {
            this._dt = dt;
        }

        /// <summary>
        /// バージョントレラントの有無を取得または設定します。
        /// </summary>
        public bool IsVersionTolerant { get; set; }

        /// <summary>
        /// Nullable型ではないマッピング対象について、DBNull.Valueを許容するか否かを取得または設定します。
        /// 通常、DBNull.Valueを受け入れるマッピング対象はNullable型を指定してください。
        /// 
        /// ※ DBNull.Valueを許容する場合、DBNull.Valueは対象の型の既定値(default)として扱われます。
        /// ※ DBNull.Valueを許容しない場合、例外をThrowします。
        /// </summary>
        public bool IsDBNullTolerant { get; set; }

        /// <summary>
        /// マッピングしたRowsを取得します。
        /// </summary>
        public IEnumerable<T> Rows
        { 
            get
            {
                foreach (DataRow r in _dt.Rows) yield return GetMappingRow(r);
            }
        }

        /// <summary>
        /// インデクサ
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public T this[int index]
        {
            get{ return GetMappingRow(_dt.Rows[index]); }
        }

        /// <summary>
        /// DataTableのDataRowを、T型にマッピングします。
        /// </summary>
        /// <param name="row"></param>
        /// <returns></returns>
        public T GetMappingRow(DataRow row)
        {
            T mapRow = new T();
            Type t = mapRow.GetType();

            foreach (FieldInfo fi in GetTypeMappingFieldInfo(t))
            {
                if (IsVersionTolerant)
                {
                    if (_dt.Columns.Contains(fi.Name)) 
                    {
                        fi.SetValue(mapRow, GetSqlTypeToType(fi.FieldType, row[fi.Name]));
                    }
                    continue;
                }
                fi.SetValue(mapRow, GetSqlTypeToType(fi.FieldType, row[fi.Name]));
            }

            foreach (PropertyInfo prp in GetTypeMappingPropertyInfo(t))
            {
                if (IsVersionTolerant)
                {
                    if (_dt.Columns.Contains(prp.Name)) 
                    {
                        prp.SetValue(mapRow, GetSqlTypeToType(prp.PropertyType, row[prp.Name]), null);
                    }
                    continue;
                }
                prp.SetValue(mapRow, GetSqlTypeToType(prp.PropertyType, row[prp.Name]), null);
            }

            return mapRow;
        }

        /// <summary>
        /// マッピング対象Fieldメタデータを取得します。
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private IEnumerable<FieldInfo> GetTypeMappingFieldInfo(Type t)
        {
            return from f in t.GetFields(BindingFlags.Public
                                       | BindingFlags.Instance)
                   where f.FieldType.IsPrimitive
                       | f.FieldType.IsValueType 
                       | f.FieldType == typeof(string)
                   where !f.IsLiteral
                   where f.GetCustomAttributes(typeof(DataRowTypeMappingAttribute), false).Count() != 0
                   select f;        
        }

        /// <summary>
        /// マッピング対象Propertyメタデータを取得します。
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private IEnumerable<PropertyInfo> GetTypeMappingPropertyInfo(Type t)
        {
            return from p in t.GetProperties(BindingFlags.Public
                                           | BindingFlags.Instance
                                           | BindingFlags.SetProperty)
                   where p.PropertyType.IsPrimitive
                       | p.PropertyType.IsValueType
                       | p.PropertyType == typeof(string)
                   where p.GetCustomAttributes(typeof(DataRowTypeMappingAttribute), false).Count() != 0
                   select p;
        }

        /// <summary>
        /// SqlTypeに対応するTypeを取得します。。(DBNull.ValueとNullable型の扱い)
        /// </summary>
        /// <param name="t"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public object GetSqlTypeToType(Type t, object value)
        {
            if (Utility.IsNullable(t))
            {
                if (value == DBNull.Value) return null;
                var ga = t.GetGenericArguments();
                if (ga[0].IsEnum) return Enum.ToObject(ga[0], value);
                return value;
            }

            if (t.IsClass)
            {
                if (value == DBNull.Value) return null;
                return value;
            }

            if (!IsDBNullTolerant && value == DBNull.Value)
                throw new InvalidCastException("無効なキャストです。Nullable型ではないマッピング対象には、DBNull.Value値を設定することはできません。");

            if (value == DBNull.Value) return Activator.CreateInstance(t);
            if (t.IsEnum)
            {
                return Enum.ToObject(t, value);
            }
            return value;
        }
    }
}
using System;

namespace ClassLibrary1
{
    /// <summary>
    /// DataRowTypeMapperのマッピング対象とするFieldまたはPropertyに設定します。
    /// FieldおよびProperty名は、DataColumn名と同じにしてください。
    /// 
    /// 
    /// ※ DataRowTypeMapperのIsVersionTolerantプロパティがtrueの場合、
    ///    マッピング対象と同じ名前のDataColumnが存在しないことを許容します。
    /// 
    /// ※ DataRowTypeMapperのIsDBNullTolerantプロパティがtrueの場合、
    ///    DBNull.Valueは、対象の型の既定値(default)として扱われます。
    /// </summary>
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
    public sealed class DataRowTypeMappingAttribute : Attribute
    {
        public DataRowTypeMappingAttribute() { }
    }
}
using System;

namespace ClassLibrary1
{
    /// <summary>
    /// ユーティリティ
    /// </summary>
    public class Utility
    {
        /// <summary>
        /// Nullableかどうかを取得します。
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static bool IsNullable(Type type)
        {
            if (type.IsClass) return false;
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) return true;
            return false;
        }

        /// <summary>
        /// SqlTypeに対応するTypeを取得します。。(DBNull.ValueとNullable型の扱い)
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <returns></returns>
        public static T GetSqlTypeToType<T>(object value)
        {
            if (value == DBNull.Value) return default(T);
            if (Utility.IsNullable(typeof(T)))
            {
                var ga = typeof(T).GetGenericArguments();
                if (ga[0].IsEnum) return (T)Enum.ToObject(ga[0], value);
            }
            if (typeof(T).IsEnum) return (T)Enum.ToObject(typeof(T), value);
            return (T)value;
        }
    }
}
#define NullableProperties

using System;
using ClassLibrary1;

namespace ConsoleApplication1
{
    public class User
    {
        public enum EBloodType { UnKnown, A, B, AB, O }

#if NullableFields
        #region NullableFields
        [DataRowTypeMapping]
        public int? Id = default(int?);
        [DataRowTypeMapping]
        public string Name = default(string);
        [DataRowTypeMapping]
        public bool? Sex = default(bool?);
        [DataRowTypeMapping]
        public DateTime? BirthDay = default(DateTime?);
        [DataRowTypeMapping]
        public EBloodType? BloodType = default(EBloodType?);
        #endregion
#endif

#if NullableProperties
        #region NullableProperties
        [DataRowTypeMapping]
        public int? Id { get; set; }
        [DataRowTypeMapping]
        public string Name { get; set; }
        [DataRowTypeMapping]
        public bool? Sex { get; set; }
        [DataRowTypeMapping]
        public DateTime? BirthDay { get; set; }
        [DataRowTypeMapping]
        public EBloodType? BloodType { get; set; }
        #endregion
#endif

#if NotNullableFields
        #region NotNullableFields
        [DataRowTypeMapping]
        public int Id = default(int);
        [DataRowTypeMapping]
        public string Name = default(string);
        [DataRowTypeMapping]
        public bool Sex = default(bool);
        [DataRowTypeMapping]
        public DateTime BirthDay = default(DateTime);
        [DataRowTypeMapping]
        public EBloodType BloodType = default(EBloodType);
        #endregion
#endif

#if NotNullableProperties
        #region NotNullableProperties
        [DataRowTypeMapping]
        public int Id { get; set; }
        [DataRowTypeMapping]
        public string Name { get; set; }
        [DataRowTypeMapping]
        public bool Sex { get; set; }
        [DataRowTypeMapping]
        public DateTime BirthDay { get; set; }
        [DataRowTypeMapping]
        public EBloodType BloodType { get; set; }
        #endregion
#endif

    }
}


お試し用のプログラム

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using ClassLibrary1;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            var dt = CreateDataTable(10000);
            //var dt = CreateDataTable(10);

            var mdt = new DataRowTypeMapper<User>(dt) { IsVersionTolerant = true , IsDBNullTolerant = true };
            var sw = new Stopwatch();
            var list = new List<string>();

            for (var i = 1; i <= 6; i++)
            {
                sw.Reset();
                sw.Start();
                foreach (DataRow user in dt.Rows)
                {
                    Console.WriteLine("{0}:{1}-{2}({3}", Utility.GetSqlTypeToType<int?>(user["Id"])
                                                       , Utility.GetSqlTypeToType<string>(user["Name"])
                                                       , Utility.GetSqlTypeToType<DateTime?>(user["BirthDay"])
                                                       , Utility.GetSqlTypeToType<User.EBloodType?>(user["BloodType"]));
                }
                sw.Stop();
                list.Add(string.Format("通常のタイム         {0}回目: {1}", i, sw.Elapsed));

                Console.WriteLine();

                sw.Reset();
                sw.Start();
                foreach (var user in mdt.Rows)
                {
                    Console.WriteLine("{0}:{1}-{2}({3}", user.Id
                                                       , user.Name
                                                       , user.BirthDay
                                                       , user.BloodType);
                }
                sw.Stop();
                list.Add(string.Format("マッピング時のタイム {0}回目: {1}", i, sw.Elapsed));
            }

            Console.WriteLine();
            foreach (var result in list) Console.WriteLine(result);
            Console.WriteLine();

            //DBNull.Valueなデータ
            var u = mdt[5];
            Console.WriteLine("{0}:{1}-{2}({3}", u.Id, u.Name, u.BirthDay,u.BloodType);

            Console.ReadLine();
        }
        
        private static DataTable CreateDataTable(int record)
        {
            var dt = new DataTable();
            dt.Columns.AddRange(
                new DataColumn[]{
                new DataColumn("Id",typeof(int)),
                new DataColumn("Name",typeof(string)),
                new DataColumn("BirthDay",typeof(DateTime)),
                new DataColumn("BloodType",typeof(User.EBloodType)),
            });

            var r = new System.Random();
            for (var i = 0; i < record; i++)
            {
                var row = dt.NewRow();

                //DBNull.Valueなデータをこさえてみるよ
                if (i == 5)
                {
                    row["Id"] = DBNull.Value;
                    row["Name"] = DBNull.Value;
                    row["BirthDay"] = DBNull.Value;
                    row["BloodType"] = DBNull.Value;
                    dt.Rows.Add(row);
                    continue;                
                }

                row["Id"] = i;
                row["Name"] = "Test" + i.ToString();
                row["BirthDay"] = new DateTime(2009, 1, 17).AddDays(i);
                var blood = r.Next(0, 4);
                row["BloodType"] = (User.EBloodType)Enum.ToObject(typeof(User.EBloodType), blood); ;
                dt.Rows.Add(row);
            }
            return dt;
        }
    }
}


実行結果(前半省略)

通常のタイム         1回目: 00:00:06.4549531
マッピング時のタイム 1回目: 00:00:06.3403792
通常のタイム         2回目: 00:00:06.3198922
マッピング時のタイム 2回目: 00:00:06.3391677
通常のタイム         3回目: 00:00:06.3232300
マッピング時のタイム 3回目: 00:00:06.3234989
通常のタイム         4回目: 00:00:06.3259166
マッピング時のタイム 4回目: 00:00:06.3382356
通常のタイム         5回目: 00:00:06.3367330
マッピング時のタイム 5回目: 00:00:06.3249356
通常のタイム         6回目: 00:00:06.3248639
マッピング時のタイム 6回目: 00:00:06.3314302

:-(


うん。なかなかいい感じです。リフレクションでマッピングをすることでの速度的な問題も特にないので、
型なしDataSetを扱うような場合は、こういうのを一つ作っておくと幸せになれるかもしれません。


ちょっと余談

.NETでRDBを扱うような場合、今だとLINQ to SQLという選択肢もあったりすると思うのだけど、どうなのだろう。
LINQ to SQLについては触りを少しだけ勉強したけど、あまり使う気にはなれなかった(そもそも機会もないけど)。
LINQ to SQLを選択するという方針は、今のところまだマジョリティではないのではないだろうか(少なくとも日本では) 。
「LINQ to SQLが通常のSQLよりも優れた選択肢である」というような話は度々耳にするのだけど、それも状況によりけりなわけで。
政治的な面、技術的な面、さまざまな事情により現場ではなかなか素直に受け入れられない現状があるのも事実。
LINQ to SQLは、LINQ to ほにゃららシリーズの中でも最も浸透していないように思う。
マイノリティではなくなる日は来るのかな。なんにしろ、なるべく楽で且つ安全な方針を採用したいよなあ。

*1:あるいはORマッピング用フレームワーク利用の検討とかもあり