アイデンティティ・マッピングと弱参照

データベースアプリケーションのテクニックの1つにアイデンティティ・マッピングがあります。これはデータベース上の行とメモリー上のオブジェクトを1対1に対応させようというものです。すべての行をメモリーに読み込むという意味ではなく、1つの行に対応するオブジェクトを複数作成しない点に主眼が置かれています。一意マッピング・恒等写像などと訳すことができますが、ここではアイデンティティ・マッピングと呼んでおきます。

1つの行に対応するオブジェクトがメモリー上に複数ある状況というのは、複数画面で同じ行を参照している場合に起こりやすいです。例えば一覧画面でロードしたいずれかの行が別画面で表示されていたりロジック実行で必要となったりといった状況です。

1つの行に対応するオブジェクトがメモリー上に複数あると、行を更新したときに同じ行に対応する別のオブジェクトも更新する必要があります。これを怠ると更新前のオブジェクトが表示されたり、更新前のオブジェクトを使ってロジックが実行されるといった不具合が発生します。アイデンティティ・マッピングではオブジェクト更新時に同じ行を表す別のオブジェクトがありませんのでこのような配慮が不要となります。

アイデンティティ・マッピングを実現するためにテーブルの主キーをキーに持ち、行に対応するオブジェクトを値に持つディクショナリーを用意することが考えられます。データベースからテーブルを読み取ったときは、このディクショナリと突き合わせ処理して、同一キーの要素があればオブジェクトのプロパティをデータベースから読み取った値に更新します。同一キーの要素がなければディクショナリーに追加します。リポジトリーにこのような仕組みを持たせておけば、すべての読み取り処理でアイデンティティ・マッピングを施すことができます。

この仕組みで問題となるのが行に対応するオブジェクトを保持するために設けたディクショナリーは要素を除外することができず、プログラム起動後に1度でも読み取った行が居座り続けるためメモリーを圧迫することです。NetFrameworkのガーベジコレクターはどこからも参照されなくなったオブジェクトを破棄対象とみなしますが、この場合ディクショナリーが参照を持っているので破棄対象になりません。

そこでこのディクショナリーの要素を弱参照するのです。 どこかの画面から参照されていれば要素は破棄されませんがどの画面からも参照されなくなりディクショナリーが弱参照するのみとなればガーベージコレクターによる破棄対象となります。

下記は要素を弱参照で持つディクショナリの例です(C# 6.0文法を使用しています)。インデクサーは読み取り専用にしています。新しい要素は内部で保持するItemCreatorによって作成します。RemoveDeadItems()によって破棄済みの要素を除外します。このようなディクショナリーを使って読み取ったデータを保持します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/// <summary>
/// 弱い参照によって、キーに対応する値を保持します。
/// </summary>
/// <typeparam name="TKey">キーの型</typeparam>
/// <typeparam name="TValue">値の型</typeparam>
/// <remarks>
/// このオブジェクト以外から参照されなくなった値はガーベジコレクションの対象となります。
/// ガーベジコレクションによって破棄された値を参照したときは、再作成します。
/// 全ての値を保持するとヒープメモリが不足する恐れがある場合に使用します。
/// </remarks>
public class WeakReferenceDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>
where TValue : class
{
IDictionary<TKey, WeakReference<TValue> UnderlyingDictionary = new Dictionary<TKey, WeakReference<TValue>();

/// <summary>
/// キーに対応する値を作成する関数を取得します。
/// </summary>
public Func<TKey, TValue> ItemCreator { get; private set; }

/// <summary>
/// 破棄されているキーのシーケンスを取得します。
/// </summary>
IEnumerable<TKey> GetDeadKeys()
{
foreach (var kv in UnderlyingDictionary) {
TValue target;
if (!kv.Value.TryGetTarget(out target)) yield return kv.Key;
}
}

/// <summary>
/// 破棄された値を持つ要素を除外します。
/// </summary>
public void RemoveDeadItems()
{
lock (this) {
foreach (var key in GetDeadKeys().ToArray()) {
UnderlyingDictionary.Remove(key);
}
}
}

/// <summary>
/// キーに対応する値を取得します。
/// </summary>
/// <param name="key">キー</param>
/// <returns>キーに対応する値</returns>
/// <remarks>
/// キーに対応する値がないかまたは既に破棄されている場合は <see cref="ItemCreator"/> によって作成します。
/// </remarks>
public TValue this[TKey key]
{
get
{
lock (this) {
WeakReference<TValue> value;
TValue target;
if (UnderlyingDictionary.TryGetValue(key, out value) && value.TryGetTarget(out target)) return target;
target = ItemCreator(key);
UnderlyingDictionary[key] = new WeakReference<TValue>(target);
return target;
}
}
}

/// <summary>
/// 格納されている要素の数を取得します。
/// </summary>
public int Count => UnderlyingDictionary.Count;

/// <summary>
/// 格納されているキーのシーケンスを取得します。
/// </summary>
/// <remarks>
/// キーに対応する値は破棄されている場合があります。
/// </remarks>
public IEnumerable<TKey> Keys => UnderlyingDictionary.Keys;

/// <summary>
/// 格納されている値のシーケンスを取得します。
/// </summary>
public IEnumerable<TValue> Values
{
get
{
foreach (var kv in UnderlyingDictionary) {
TValue target;
if (kv.Value.TryGetTarget(out target)) yield return target;
}
}
}

/// <summary>
/// コレクションを反復処理する列挙子を返します。
/// </summary>
/// <returns>コレクションを反復処理するために使用できる <see cref="IEnumerator{T}"/></returns>
public IEnumerator<KeyValuePair<TKey, TValue> GetEnumerator()
{
foreach (var kv in UnderlyingDictionary) {
TValue target;
if (kv.Value.TryGetTarget(out target)) yield return new KeyValuePair<TKey, TValue>(kv.Key, target);
}
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();

/// <summary>
/// <see cref="WeakReferenceDictionary{TKey, TValue}"/> 型のインスタンスを作成します。
/// </summary>
/// <param name="itemCreator"> キーに対応する値を作成する関数</param>
public WeakReferenceDictionary(Func<TKey, TValue> itemCreator)
{
ItemCreator = itemCreator;
}
}