Dapper неправильно отображает объекты «один ко многим»C#

Место общения программистов C#
Anonymous
Dapper неправильно отображает объекты «один ко многим»

Сообщение Anonymous »

Я динамически извлекаю вложенные объекты типа «один ко многим». Dapper неправильно отображает вложенные объекты «один ко многим». Внутри функции карты objs имеет 2 элемента. objs[0] является родительским. Родительская сущность хороша с действительным значением идентификатора, но дочерняя коллекция содержит 0 элементов. objs[1] — это тип дочернего элемента, но он имеет нулевые значения и Id = 0.
Сгенерированный запрос из приведенного ниже кода в порядке, я могу успешно запустить его в pgadmin, он возвращает 2 строки, но Dapper не заполняет objs[0] дочерними элементами в родительском объекте. А objs[1] — это объект дочернего типа, который имеет все значения NULL и идентификатор 0.

Код: Выделить всё

 public async Task GetByIdAsync(DynamicGetByIdRequest getByIdReq, CancellationToken cancellationToken = default)
{
Type rootType = ResolveEntityType(getByIdReq.DynamicGetParameter!.EntityName);
var sqlMeta = _metaExtractor.Extract(rootType);

var queryParts = BuildQuery(getByIdReq.DynamicGetParameter, sqlMeta, rootType, "T0");

string sql = $@"SELECT {queryParts.SelectClause}
FROM {sqlMeta.Table.Quote()} AS {"T0".Quote()}
{queryParts.JoinClause}
WHERE {"T0".Quote()}.{"Id".Quote()} = @id";

var lookup = new Dictionary();

await _db.QueryAsync(
sql,
types: queryParts.Types.ToArray(),
map: (object[] objs) =>
{
var root = objs[0];
var rootId = (int)root.GetType().GetProperty("Id").GetValue(root);

if (!lookup.TryGetValue(rootId, out object existing))
{
lookup.Add(rootId, root);
existing = root;

// Initialize collection properties
foreach (var prop in root.GetType().GetProperties())
{
if (prop.PropertyType.IsGenericType &&
prop.PropertyType.GetGenericTypeDefinition() == typeof(List))
{
prop.SetValue(existing, Activator.CreateInstance(prop.PropertyType));
}
}
}

// Handle child objects
for (int i = 1; i < objs.Length; i++)
{
var child = objs[i];

if (child == null)
continue;

var childType = queryParts.Types[i];
var navProp = rootType.GetProperties()
.FirstOrDefault(p => p.PropertyType.IsGenericType &&
p.PropertyType.GetGenericTypeDefinition() == typeof(List) &&
p.PropertyType.GetGenericArguments()[0] == childType);

if (navProp != null)
{
var collection = navProp.GetValue(existing) as IList;
var childId = (int)child.GetType().GetProperty("Id").GetValue(child);

if (!collection.Cast().Any(x =>
(int)x.GetType().GetProperty("Id").GetValue(x) == childId))
{
collection.Add(child);
}
}
}

return existing;
},

param: new { id = getByIdReq.Id },
splitOn: string.Join(",", queryParts.SplitOnColumns)
);

return lookup.Values.FirstOrDefault();
}

private (List Types, string SelectClause, string JoinClause, List SplitOnColumns)
BuildQuery(DynamicGetParameter param, SqlMeta meta, Type entityType, string alias, string? parentAlias = null)
{
var types = new List { entityType };
var selectParts = new List();
var joinParts = new List();
var splitOnColumns = new List();

// 1.  Select all scalar properties for root entity
foreach (var prop in meta.ScalarProps)
{
if (param.Properties == null || param.Properties.Contains(prop.Name, StringComparer.OrdinalIgnoreCase))
{
selectParts.Add($"{alias.Quote()}.{prop.Name.Quote()} AS \"{prop.Name}\"");
}
}

// Include root ID (only once)
if (!selectParts.Any(sp => sp.Contains($"\"Id\"")))
{
selectParts.Add($"{alias.Quote()}.{"Id".Quote()} AS \"Id\"");
}

// 2. Handle nested properties
if (param.Navigation != null)
{
foreach (var nav in param.Navigation)
{
Type navType = ResolveEntityType(nav.EntityName);
var navMeta = _metaExtractor.Extract(navType);
string navAlias = $"{alias}_{nav.EntityName}";
types.Add(navType);

// Join clause
joinParts.Add($"LEFT JOIN {navMeta.Table.Quote()} AS {navAlias.Quote()} " +
$"ON {alias.Quote()}.{"Id".Quote()} = {navAlias.Quote()}.{"ReferralId".Quote()}");

// Select child properties
foreach (var prop in navMeta.ScalarProps)
{
if (nav.Properties == null || nav.Properties.Contains(prop.Name, StringComparer.OrdinalIgnoreCase))
{
selectParts.Add($"{navAlias.Quote()}.{prop.Name.Quote()} AS \"{prop.Name}\"");
}
}

// Select child ID with correct alias for mapping (only once)
//selectParts.Add($"{navAlias.Quote()}.{"Id".Quote()} AS \"Id\"");

// Select child ID again with unique alias for splitOn
selectParts.Add($"{navAlias.Quote()}.{"Id".Quote()} AS \"{navAlias}_Id\"");
splitOnColumns.Add($"{navAlias}_Id");
}
}

return (
Types: types,
SelectClause: string.Join(", ", selectParts),
JoinClause: string.Join(" ", joinParts),
SplitOnColumns: splitOnColumns
);
}
Сгенерированный SQL-запрос:

Код: Выделить всё

SELECT
"r"."Id",
"r"."ReservationStatus",
"r"."Language",
"r"."ReferralSource",
"ra"."Id",
"ra"."AppointmentDate",
"ra"."AppointmentTime",
"ra"."LegSequence",
"ra"."LegStatus",
"ra"."ReferralId"
FROM
"Referral" AS "r"
LEFT JOIN "ReferralAppointment" AS "ra" ON "r"."Id" = "ra"."ReferralId"
WHERE
"r"."Id" = 1
Разделение столбца — это идентификатор.
Тот же запрос работал со статическими типами и жестко запрограммированным запросом. Я использовал тот же жестко закодированный запрос с динамической функцией и с тем же SplitOn. Но динамическая функция не заполняет дочернюю коллекцию. Это пусто, а не ноль. ИИ говорит, что в случае статических данных ( _db.QueryAsync) типы статически. вот в чем проблема.
Я спросил, могу ли я использовать свои динамические типы в общей функции (_db.QueryAsync), мне предложили вызвать QueryAsync через отражение. Но следующий возврат равен нулю:

Код: Выделить всё

var method = typeof(IDbConnection).GetMethods()
.FirstOrDefault(m => m.Name == "QueryAsync" && m.IsGenericMethod);
Запрос возвращает правильные строки в pgAdmin.

Подробнее здесь: https://stackoverflow.com/questions/796 ... -correctly

Вернуться в «C#»