@chichi1091 の 雑記

技術系のネタメインで。

DBFlute.NET+ODP.NETでbatchInsertモドキ(#dbflute)

久しぶりの投稿です・・・

今回は大好きなDBFluteのネタです
やっぱり.NETでもbatchInsertがほしい!ということで昔作ったものを紹介します
MBPに変えてしまって動かす環境がないためコンパイルエラーが出ちゃうかもしれませんが勘弁してください^^;



残念ながら制約があってOracleのODP.NETを使っている場合のみという限定です
ODP.NETの配列バインドという機能で配列をバインドして一括実行を行います
InsertのデータはDBFluteのEntityから取得しています
あとはQuillからConnectionを取得しているところがポイントでしょうか

他のDBはできるのかちょっとわからないです・・・
すみません<(_ _)>

    /// <summary>
    /// ODP.NETのArrayBindでINSERTするクラス
    /// </summary>
    public class ArrayBindInsert : IDisposable {
        private OracleConnection conn = null;

        /// <summary>
        /// コンストラクタ
        /// トランザクション制御を呼び出し元で行わない場合
        /// </summary>
        public ArrayBindInsert() {
            QuillInjector injector = QuillInjector.GetInstance();
            ITransactionSetting s =
                (ITransactionSetting)ComponentUtil.GetComponent(injector.Container, typeof(TypicalTransactionSetting));
            ITransactionContext txc = s.TransactionContext;
            ITransactionContext current = null;
            if (txc.Current == null) current = txc.Create();
            else current = txc.Current;
            conn = (OracleConnection)current.Connection;
        }
        /// <summary>
        /// コンストラクタ
        /// トランザクション制御を呼び出し元で行う場合
        /// </summary>
        /// <param name="current">ITransactionContext</param>
        public ArrayBindInsert(ITransactionContext current) {
            conn = (OracleConnection)current.Connection;
        }

        /// <summary>
        /// ArrayBindによるINSERT実行
        /// valuesに設定されたkeyでパラメータを設定し、SQL文を実行する
        /// </summary>
        /// <param name="sql">SQL文</param>
        /// <param name="types">カラムの型のDictionaryコレクション</param>
        /// <param name="values">カラムの値のDictionaryコレクション</param>
        /// <param name="recordCount">実行するレコード数</param>
        /// <returns>処理された件数</returns>
        private int ExecuteQuery(String sql, IDictionary<string, OracleDbType> types, IDictionary<string, object> values, int recordCount) {
            OracleCommand cmd = new OracleCommand(sql, conn);
            cmd.CommandText = sql;
            cmd.ArrayBindCount = recordCount;
            cmd.BindByName = true;

            foreach(string key in values.Keys) {
                OracleParameter prm = new OracleParameter();
                prm.ParameterName = key;
                prm.OracleDbType = types[key];
                prm.Value = values[key];
                prm.Direction = System.Data.ParameterDirection.Input;
                cmd.Parameters.Add(prm);
            }
            return cmd.ExecuteNonQuery();
        }

        /// <summary>
        /// ArrayBindによるINSERT
        /// </summary>
        /// <param name="insertList">登録対象データリスト</param>
        /// <param name="autoSeqFlag">サロゲートキーの採番を自動で行う(シーケンスを利用する)場合true、自動で行わない場合false (省略時、false)</param>
        /// <returns>Insert件数</returns>
        public int ExecuteInsert<ENTITY>(IList<ENTITY> insertList, bool autoSeqFlag = false) where ENTITY : Entity {
            if (insertList != null && insertList.Count <= 0) return 0;

            string tableName = insertList[0].TableDbName;
            DBMeta metaData = insertList[0].DBMeta;

            // SQL文の生成、バインドするカラム名と型を設定する
            StringBuilder sql = new StringBuilder();
            StringBuilder sqlValue = new StringBuilder();
            Dictionary<string, OracleDbType> types = new Dictionary<string, OracleDbType>();
            List<ColumnInfo> columnInfoList = (List<ColumnInfo>)metaData.ColumnInfoList.getList();

            sql.Append(string.Format("INSERT INTO {0}", tableName));
            sql.Append("(");
            for (int i = 0; i < columnInfoList.Count; ++i) {
                var columnInfo = columnInfoList[i];
                types.Add(columnInfo.ColumnDbName, GetOracleDbType(columnInfo));

                if (i != 0) {
                    sql.Append(",");
                    sqlValue.Append(",");
                }

                sql.Append(string.Format(" {0}", columnInfo.ColumnDbName));

                if (autoSeqFlag && columnInfo.IsPrimary) {
                    sqlValue.Append(string.Format("{0}.NEXTVAL", columnInfo.DBMeta.SequenceName));
                } else {
                    sqlValue.Append(string.Format(" :{0}", columnInfo.ColumnDbName));
                }
            }
            sql.Append(") VALUES (");
            sql.Append(sqlValue.ToString());
            sql.Append(")");

            Dictionary<string, object> values = new Dictionary<string, object>();
            Type t = insertList[0].GetType();
            for (int i = 0; i < columnInfoList.Count; ++i) {
                var columnInfo = columnInfoList[i];
                //シーケンスを利用する場合はバインド変数を利用しないので、valuesからも省く
                if (autoSeqFlag && columnInfo.IsPrimary) continue;

                List<object> columnDatas = new List<object>();
                for (int j = 0; j < insertList.Count; ++j) {
                    object colunData = t.InvokeMember(columnInfo.PropertyName, BindingFlags.GetProperty, null, insertList[j], null);
                    columnDatas.Add(colunData);
                }
                values.Add(columnInfo.ColumnDbName, columnDatas.ToArray());
            }
            return ExecuteQuery(sql.ToString(), types, values, insertList.Count);
        }

        /// <summary>
        /// ColumnInfoのColumnDbTypeからOracleDbTypeに変換する
        /// </summary>
        /// <param name="columnInfo"></param>
        /// <returns></returns>
        private OracleDbType GetOracleDbType(ColumnInfo columnInfo) {
            OracleDbType oracleDbType = (OracleDbType)0;
            switch (columnInfo.ColumnDbType) {
                case "NUMBER":
                    int? size = columnInfo.ColumnSize;
                    int? decimalSize = columnInfo.ColumnDecimalDigits;
                    if (decimalSize != null && decimalSize > 0) {
                        oracleDbType = OracleDbType.BinaryDouble;
                    } else {
                        if (size != null) {
                            size = size - decimalSize;
                            if (size > 20) {
                                oracleDbType = OracleDbType.Decimal;
                            } else if (size > 9) {
                                oracleDbType = OracleDbType.Int64;
                            } else {
                                oracleDbType = OracleDbType.Int32;
                            }
                        } else {
                            oracleDbType = OracleDbType.Decimal;
                        }
                    }
                    break;
                case "VARCHAR2":
                    oracleDbType = OracleDbType.Varchar2;
                    break;
                case "NVARCHAR2":
                    oracleDbType = OracleDbType.NVarchar2;
                    break;
                case "CHAR":
                    oracleDbType = OracleDbType.Char;
                    break;
                case "DATE":
                    oracleDbType = OracleDbType.Date;
                    break;
                case "TIMESTAMP":
                case "TIMESTAMP(6)":
                    oracleDbType = OracleDbType.TimeStamp;
                    break;
                case "NCLOB":
                    oracleDbType = OracleDbType.NClob;
                    break;
                case "BINARY_DOUBLE":
                    oracleDbType = OracleDbType.BinaryDouble;
                    break;
                default:
                    break;
            }
            return oracleDbType;
        }

        /// <summary>
        /// Dispose
        /// </summary>
        public void Dispose() {
            if (conn != null) {
                conn.Close();
                conn = null;
            }
        }        
    }
}

発行されるSQLは一回なので大量Insertの場合、早いと思われます
DBFluteみたいにシーケンスで発行したIDをEntityに戻せればいいんですが、そのためにはselectが必要になってくるので速度とのトレードオフって感じで難しいですね