C # Изящные отношения «один ко многим» с подходом дженериков

#c# #dapper

#c# #dapper

Вопрос:

У меня есть две такие модели

 public class Teacher
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public List<Class> Classes { get; set; } = new();
}   

public class Class
{
    public int Id { get; set;}
    public int NumOfStudents { get; set;}
}
 

У меня также есть хранимая процедура для выбора всех записей.
Моя проблема в том, как можно использовать dapper с дженериками. В общем случае я использую этот общий метод

 public List<T> LoadData<T, U>(string storedProcedure, U parameters, string connectionStringName)
{
     var connectionString = GetConnectionString(connectionStringName);

     using (IDbConnection connection = new SqlConnection(connectionString))
     {
         var list = connection.Query<T>(storedProcedure, parameters,
             commandType: CommandType.StoredProcedure).ToList();

        return list;
     }
 }
 

Теперь из-за отношения «один ко многим» я пытаюсь использовать этот общий метод, но безуспешно

 public List<T> LoadMultipleData<T, L, U>(string storedProcedure, T parentModel, L childModel, U parameters, string connectionStringName)
{
     var connectionString = GetConnectionString(connectionStringName);

     using (IDbConnection connection = new SqlConnection(connectionString))
     {
         var list= connection.Query<T, U, T>(storedProcedure, 
                                         (parent, child) =>
                                          {
                                             parent.parentModel = child;
                                             return parent;
                                          },
                                          parameters,
                commandType: CommandType.StoredProcedure).ToList();

         return list;
     }
 }
 

Как правильно это сделать?

Комментарии:

1. Посмотрите на это: dapper-tutorial.net/result-multi-mapping

2. Обратите внимание, что Dapper использует ‘splitOn’ в запросах с несколькими отображениями, поэтому он может определить, где заканчивается объект и начинается другой, не уверен, правильно ли вы делаете это в своем SP, но явно не делаете этого на стороне Dapper

Ответ №1:

После долгого времени я придумал решение для этого:

 public async Task<List<TEntity>> LoadMultipleDataAsync<TEntity, TManyEntity, TParameters>(string query, TParameters parameters, string connectionStringName)
{
    var connectionString = GetConnectionString(connectionStringName);

    using IDbConnection connection = new SqlConnection(connectionString);
    var dictionary = new Dictionary<int, TEntity>();
    var result = await connection.QueryAsync<TEntity, TManyEntity, TEntity>(query,
        (one, many) =>
        {
            var onePropertyInfo = typeof(TEntity).GetProperty("Id");
            var oneId = (int)onePropertyInfo?.GetValue(one);
            if (!dictionary.TryGetValue(oneId, out var currentOne))
            {
                currentOne = one;
                dictionary.Add(oneId, currentOne);
            }

            var manyPropertyType = typeof(TManyEntity).Name;
            var manyPropertyName = typeof(TEntity)?.GetProperty($"{manyPropertyType}es")?.Name;
            var list = (IList<TManyEntity>)currentOne?.GetType()?.GetProperty(manyPropertyName)?.GetValue(currentOne);                
            list.GetType().GetMethod("Add").Invoke(list, new object[] { many });

            return currentOne;
        }, parameters);

    return result.Distinct().ToList();
}
 

Это решение предполагает, что Id всегда int .

В этой строке кода:

 var manyPropertyName = typeof(TEntity)?.GetProperty($"{manyPropertyType}es")?.Name;
 

Мы ссылаемся на множественное число Class (Классы) через отражение, но это всего лишь пример, и его можно улучшить.

Ответ №2:

Как насчет этого:

 public List<T> LoadMultipleData<T, L, U>(string storedProcedure, T parentModel, L childModel, U parameters, string connectionStringName)
{
    var connectionString = GetConnectionString(connectionStringName);

    using (IDbConnection connection = new SqlConnection(connectionString))
    {
        var parents = new Dictionary<int, T>();
        var list= connection.Query<T, U, T>(storedProcedure, 
                                         (parent, child) =>
                                          {
                                             T foundParent;
                                             if (!parents.TryGetValue(parent.id, out foundParent))
                                             {
                                                 foundParent = parent;
                                                 parents.Add(foundParent.id, foundParent);
                                             } 
                                             foundParent.parentModel = child;
                                             return foundParent;
                                          },
                                          parameters,
                commandType: CommandType.StoredProcedure).ToList();

         return list.Distinct(); // or return parents if you like.
    }
}
 

splitOn Параметр не нужен, если ваши поля имеют имя «Id». Вы должны помнить, что любое соединение возвращает 2D «таблицу», а родительские элементы (левая часть соединения) будут умножены столько раз, сколько у них есть дочерних элементов. Вот почему вам нужен родительский словарь, чтобы отслеживать, является ли он «новым» родительским или существующим.

Комментарии:

1. Это только я или этот код даже не компилируется? T doesn't contain a definition for id .