Bell’s diary

WPFやC#の小ネタ、その他いろいろ気まぐれで投稿してみる予定。

LINQtoSQLで、主キーの変更をする

LINQtoSQLでは、主キーのフィールドを変更してSubmitChanges()しようとすると例外が発生してしまい、変更することができません。

代わりに、元々のレコードは削除して、代わりに、主キー以外の全てのフィールドをコピーして(DBの値でなく、画面で主キー以外のいろいろなフィールドも編集した結果からコピー)主キーだけを新しいものにしたレコードを追加する、という組み合わせで、一応、見かけ上は主キーを変更したのと同等の変更をすることができます。(厳密にはすでに書いている通り、旧レコードの削除+新レコードの追加ですが)。

これを行うコード例です。Dbは、LINQtoSQLで作成されるDataContextのインスタンスが入っており、同一アセンブリ内のBellという名前空間に、作りたいエンティティのクラスが定義されていて、その主キーは「Id」という名前の列であり、画面に呼び出して編集対象にしてるテーブル名がTargetTableNameに入ってる、という前提のコードです(前提多くてすみません)。

もともと、複数のコード系マスタテーブルを編集するために作ったもので、Idという名前のフィールドが存在し、それが主キーになっているテーブルならどれでも対応できるよう、途中でdynamicで受けています。また、複数ある対象の中からどれを編集対象とするか、そのテーブル名をTargetTableNameに入れて呼び出すものとなっています。

なお、Id列は、「Id」という名称ですが、SQL server側の列のプロパティとしては「IDである」は「いいえ」になってる必要があります^^ 「はい」で自動で番号振られる状態だと、手動で値を設定することはできませんので。

        public void Save()
        {
            Action doAfterDelete = () => { };

            var changeSet = Db.GetChangeSet();

            for (var i = changeSet.Updates.Count-1; i >=0; i--)
            {
                var oldEntity = changeSet.Updates[i];

                var table = Db.GetTable(oldEntity.GetType());

                dynamic beforeEdit = table.GetOriginalEntityState(oldEntity);
                dynamic afterEdit = oldEntity;

                //主キーが「Id」という名前の列の場合。
                int beforeId = beforeEdit.Id;
                int afterId = afterEdit.Id;

                if (beforeId == afterId) continue;

                //ID変更の処理。LINQtoSQLでは変更はできないので、旧レコードの削除と新レコードの追加に組み替える。

                //元のレコードの削除
                table.DeleteOnSubmit(oldEntity);

                //新しいレコードの作製
                var assembly = Assembly.GetExecutingAssembly();
                dynamic newEntity = assembly.CreateInstance(
                    "Bell." + TargetTableName, false, BindingFlags.CreateInstance,
                    null, null, null, null
                    );

                //作ったレコードの全フィールドに画面で編集した値を設定
                //(IDも画面で新しいものに編集されている前提)
                foreach (var info in  oldEntity.GetType().GetProperties())
                {
                    info.SetValue(newEntity, info.GetValue(oldEntity, null));
                }

                //SubmitChangeで旧レコードを削除した後でないと新レコード追加できないのでデリゲートに追加するだけ
                doAfterDelete+=()=> { table.InsertOnSubmit(newEntity); };
            }
            //まずここで主キー変更以外の変更の更新(主キー変更する旧レコードの削除含む)
            Db.SubmitChanges();

            //主キー変更したレコードのコピーはここで追加される。
            doAfterDelete();
            Db.SubmitChanges();
        }

forループを逆順で回してるのは特に意味はないです^^

元々、更新処理を、元レコードの削除+コピーの追加に置き換えるわけだから、ChangesetのUpdatesからは該当レコードを除去した方がいいんじゃないかと思い、そのために逆順にしてコード書き始めたと思うのですが、そもそもそんなことする必要もなく無事動作したというオチ。