Разберем чтение записей из таблиц, связанных отношением Parent-Child, с помощью Dapper.
Допустим, есть две таблицы:
tst_parent с колонками id, name
tst_child с колонками id, parentID, name, где parentID – внешний ключ для таблицы Parent
Создадим хранимую процедуру для чтения записей из этих таблиц. Запрос будет выглядеть так:
SELECT * from tst_parent p
inner join tst_child c on c.parendID = p.id
Очевидно, что в этом запросе будут повторяться записи из таблицы tst_parent для каждой связанной с ней записью из таблицы tst_child. Мы распределим эти записи в специально методе Dapper’a.
Вызовем хранимую процедуру с помощью Dapper:
using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings[“RudenTestEntitiesSimple”].ConnectionString))
{
conn.Open();
var parentLookup = new Dictionary<int, tst_parent>();
conn.Query<tst_parent, tst_child, tst_parent>(“GetObjects”, (p, c) =>
{
tst_parent parent;
if (!parentLookup.TryGetValue(p.id, out parent))
{
parentLookup.Add(p.id, parent = p);
}
if (parent.tst_child == null) parent.tst_child = new List();
parent.tst_child.Add(c);
return parent;
});
return parentLookup.Values.ToArray();
}
Разберем приведенный выше код построчно.
conn.Query<tst_parent, tst_child, tst_parent>: указываем в шаблоне запроса, что мы хотим получить записи из таблиц tst_parent и tst_child, и в результате будет запись из tst_parent, хранящая ссылки на свои дочерние записи.
“GetObjects”: указываем имя хранимой процедуры.
Далее создаем функцию преобразования. В качестве параметров она принимает значения из таблиц tst_parent и tst_child, при этом, как уже упоминалось, значения таблицы tst_parent будут повторяться для каждой записи из tst_child. В этой фукции мы должны исключить эти повторения.
До вызова функции мы создали словарь
var parentLookup = new Dictionary<int, tst_parent>();
В нем мы будем хранить найденные значения tst_Parent, и проверять, не был ли уже добавлен tst_Parent.
tst_parent parent;
if (!parentLookup.TryGetValue(p.id, out parent))
{
parentLookup.Add(p.id, parent = p);
}
Проверяем, не был ли уже добавлен parent, если нет, то добавляем.
if (parent.tst_child == null) parent.tst_child = new List();
parent.tst_child.Add(c);
Записи из child у нас не повторяются, поэтому просто добавим запись.
Теперь представим, что у нас добавилась еще одна таблица tst_subChild c колонками id, childID, name, где childID ссылается на таблицы tst_child. Изменим запрос хранимой процедуры, чтобы включить новую таблицу в результат.
SELECT * from tst_parent p
inner join tst_child c on c.parendID = p.id
inner join tst_subChild s on s.childID = c.id;
В данном случае значения из tst_child также будут повторяться для каждой дочерней записи из tst_subChild. В данном случае однозначно для того, чтобы однозначно идентифицировать, в какую таблицу добавлять запись, нам необходимо два ключа из таблиц tst_Parent и tst_Child. Поэтому создадим вспомогательный класс:
public class ChildKey
{
public int parentId { get; set; }
public int childId { get; set; }
public override bool Equals(object obj)
{
var a = obj as ChildKey;
return parentId == a.parentId && childId == a.childId;
}
public override int GetHashCode()
{
return this.childId + this.parentId;
}
}
Данный класс будет использоваться в качестве ключа в Dictionary, поэтому необходимо переопределить в нем методы Equals и GetHashCode.
Перепишем функцию запроса:
var childLookup = new Dictionary<ChildKey, tst_child>();
conn.Query<tst_parent, tst_child, tst_subChild, tst_parent>(“GetObjects”, (p, c, s) =>
{
tst_parent parent;
if (!parentLookup.TryGetValue(p.id, out parent))
{
parentLookup.Add(p.id, p);
}
tst_child child;
var childKey = new ChildKey { parentId = c.parendID, childId = c.id };
if (!childLookup.TryGetValue(childKey, out child))
{
childLookup.Add(childKey, child = c);
parent = parentLookup[c.parendID];
if (parent.tst_child == null) parent.tst_child = new List();
parent.tst_child.Add(c);
}
if (child.tst_subChild == null) child.tst_subChild = new List();
child.tst_subChild.Add(s);
return parent;
});
Разберем код построчно:
conn.Query<tst_parent, tst_child, tst_subChild, tst_parent> теперь в результате запросы мы получаем записи из трех таблиц.
var childLookup = new Dictionary<ChildKey, tst_child>(); создадим дополнительный словарь для таблицы tst_Child.
Функция отображения теперь в качестве параметров получает три значения.
По прежнему исключаем повторы из таблицы parent.
if (!parentLookup.TryGetValue(p.id, out parent))
{
parentLookup.Add(p.id, p);
}
Для каждой записи из таблицы tst_Child создаем ключ.
var childKey = new ChildKey { parentId = c.parendID, childId = c.id };
Eсли такая запись еще не была обработана, добавим ее в словарь и найдем для нее parent.
if (!childLookup.TryGetValue(childKey, out child))
childLookup.Add(childKey, child = c);
parent = parentLookup[c.parendID];
Добавим запись к соответствующему parent.
if (parent.tst_child == null) parent.tst_child = new List();
parent.tst_child.Add(c);
Записи из tst_subChild не повторяются, поэтому просто добавим ее к соответствующему child.
if (child.tst_subChild == null) child.tst_subChild = new List();
child.tst_subChild.Add(s);
На самом деле в алгоритме можно обойтись и без дополнительных словарей, но в этом случае придется проверять существование записи поиском в массиве, а это сильно замедлит алгоритм при большом количестве записей.
При более глубокой вложенности таблиц необходимо будет создавать более сложные ключи, содержащие ключи по количеству вложенных таблиц, и аналогичным способом менять функцию отображения.