Подтвердить что ты не робот

Как реализовать отношения "один-много"

У меня есть отношения от одного до многих, исходящие из хранимой процедуры. У меня есть несколько отношений от одного до многих в запросе, и я пытаюсь сопоставить эти поля с объектом С#. Проблема, с которой я сталкиваюсь, - это получение повторяющихся данных из-за отношений от одного до многих. Вот упрощенная версия моего кода:

Вот классы объектов:

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Color> FavoriteColors { get; set; }
    public List<Hobby> Hobbies { get; set; }

    public Person()
    {
        FavoriteColors = new List<Color>();
        Hobbies = new List<Hobby>();
    }
}

public class Color
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Hobby
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Вот как я извлекаю данные:

using (SqlConnection conn = new SqlConnection("connstring.."))
{
    string sql = @"
                    SELECT 
                        Person.Id AS PersonId, 
                        Person.Name AS PersonName, 
                        Hobby.Id AS HobbyId,
                        Hobby.Name AS HobbyName,
                        Color.Id AS ColorId,
                        Color.Name AS ColorName
                    FROM Person
                    INNER JOIN Color on Person.Id = Color.PersonId
                    INNER JOIN Hobby on Person.Id = Hobby.PersonId";
    using (SqlCommand comm = new SqlCommand(sql, conn))
    {
        using (SqlDataReader reader = comm.ExecuteReader(CommandBehavior.CloseConnection))
        {
            List<Person> persons = new List<Person>();
            while (reader.Read())
            {
                Person person = new Person();
                //What to do
            }
        }
    }
}

Как вы можете видеть, для данного Лица может быть несколько цветов и хобби. Обычно я использую Entity Framework для решения этого сопоставления, но нам не разрешено использовать любые ормы. Есть ли способ правильно отключить эти данные?

4b9b3361

Ответ 1

Идея заключается в том, что при повторении на читателе проверьте, существует ли существующий идентификатор лица строки в списке лиц. Если не создать объект нового человека и объявить два отдельных списка, чтобы сохранить информацию о хобби и цвета. Для последующих итераций продолжаются заполнение этих двух списков, поскольку они всегда будут одними и теми же данными. Вы попадаете в новую запись для нового человека, добавляете эти списки в объект человека и начинаете с нового объекта

Ниже приведен пример кода:

                string sql = @"
                SELECT 
                    Person.Id AS PersonId, 
                    Person.Name AS PersonName, 
                    Hobby.Id AS HobbyId,
                    Hobby.Name AS HobbyName,
                    Color.Id AS ColorId,
                    Color.Name AS ColorName
                FROM Person
                INNER JOIN Color on Person.Id = Color.PersonId
                INNER JOIN Hobby on Person.Id = Hobby.PersonId
                Order By PersonId"; // Order By is required to get the person data sorted as per the person id
            using (SqlCommand comm = new SqlCommand(sql, conn))
            {
                using (SqlDataReader reader = comm.ExecuteReader(CommandBehavior.CloseConnection))
                {
                    List<Person> persons = new List<Person>();
                    while (reader.Read())
                    {
                        var personId = reader.GetInt32(0);
                        var personName = reader.GetString(1);
                        var hobbyId = reader.GetInt32(3);
                        var hobbyName = reader.GetString(4);
                        var colorId = reader.GetInt32(5);
                        var colorName = reader.GetString(6);

                        var person = persons.Where(p => p.Id == personId).FirstOrDefault();
                        if (person == null)
                        {
                            person = new Person();
                            person.Id = personId;
                            person.Name = personName;

                            hobby = new Hobby() { Id = hobbyId, Name = hobbyName };
                            color = new Color() { Id = colorId, Name = colorName };

                            person.FavoriteColors = new List<Color>();
                            person.Hobbies = new List<Hobby>();

                            person.FavoriteColors.Add(color);
                            person.Hobbies.Add(hobby);

                            persons.Add(person);
                        }
                        else
                        {
                            hobby = new Hobby() { Id = hobbyId, Name = hobbyName };
                            color = new Color() { Id = colorId, Name = colorName };

                            //JT Edit: if the colour/hobby doesn't already exists then add it
                            if (!person.FavoriteColors.Contains(color))
                               person.FavoriteColors.Add(color);

                            if (!person.Hobbies.Contains(hobby))
                               person.Hobbies.Add(hobby);
                        }
                    }
                }
            }
        }

Ответ 2

Я думаю, что в конечном итоге все подходы, упомянутые здесь, работают. Мы могли бы решить эти проблемы, сосредоточившись на производительности.

@Mark Menchavez прокомментировал влияние производительности на повторное возвращение в базу данных, когда мы начинаем с простого списка лиц. Для огромного списка это влияние является значительным и его следует избегать как можно больше.

В конечном счете, лучше всего извлечь данные в виде нескольких кусков, насколько это возможно; в этом случае, поскольку один кусок будет идеальным (если соединения не слишком дороги). Базы данных оптимизированы для работы с наборами данных, и мы будем использовать это, чтобы избежать накладных расходов при настройке нескольких повторных подключений (особенно, если мы переходим через провод к экземпляру Sql, работающему на другой машине).

Я буду использовать подход @Luke101, но просто перейду в список в словарь значений. Хэш-поиск ключей будет быстрее, чем использование Where in @Koder response. Также обратите внимание, что я изменил SQL, чтобы читать как LEFT JOIN, чтобы разместить тех лиц, у которых нет записи Хобби или Цвет, и разрешить их возвращать как NULL (DBNull в .NET).

Также обратите внимание, что из-за формы таблиц и данных можно многократно повторять цвета и/или хобби, поэтому нам нужно также проверить их, а не просто предположить, что будет один цвет и один хобби.

Я не стал повторять здесь классы.

        public static IEnumerable<Person> DataFetcher(string connString)
    {
        Dictionary<int, Person> personDict = new Dictionary<int,Person>(1024);  //1024 was arbitrarily chosen to reduce the number of resizing operations on the underlying arrays; 
                                                                                //we can rather issue a count first to get the number of rows that will be returned (probably divided by 2).

        using (SqlConnection conn = new SqlConnection(connString))
        {
            string sql = @"
                SELECT 
                    Person.Id AS PersonId, 
                    Person.Name AS PersonName, 
                    Hobby.Id AS HobbyId,
                    Hobby.Name AS HobbyName,
                    Color.Id AS ColorId,
                    Color.Name AS ColorName
                FROM Person
                LEFT JOIN Color on Person.Id = Color.PersonId
                LEFT JOIN Hobby on Person.Id = Hobby.PersonId";

            using (SqlCommand comm = new SqlCommand(sql, conn))
            {
                using (SqlDataReader reader = comm.ExecuteReader(CommandBehavior.CloseConnection))
                {
                    while (reader.Read())
                    {
                        int personId = reader.GetInt32(0);
                        string personName = reader.GetString(1);

                        object hobbyIdObject = reader.GetValue(2);
                        object hobbyNameObject = reader.GetValue(3);
                        object colorIdObject = reader.GetValue(4);
                        object colorNameObject = reader.GetValue(5);

                        Person person;

                        personDict.TryGetValue(personId, out person);

                        if (person == null)
                        {
                            person = new Person
                            {
                                Id = personId,
                                Name = personName,

                                FavoriteColors = new List<Color>(),
                                Hobbies = new List<Hobby>()
                            };

                            personDict[personId] = person;
                        }

                        if (!Convert.IsDBNull(hobbyIdObject))
                        {
                            int hobbyId = Convert.ToInt32(hobbyIdObject);
                            Hobby hobby = person.Hobbies.FirstOrDefault(ent => ent.Id == hobbyId);

                            if (hobby == null)
                            {
                                hobby = new Hobby
                                {
                                    Id = hobbyId,
                                    Name = hobbyNameObject.ToString()
                                };

                                person.Hobbies.Add(hobby);
                            }
                        }

                        if (!Convert.IsDBNull(colorIdObject))
                        {
                            int colorId = Convert.ToInt32(colorIdObject);
                            Color color = person.FavoriteColors.FirstOrDefault(ent => ent.Id == colorId);

                            if (color == null)
                            {
                                color = new Color
                                {
                                    Id = colorId,
                                    Name = colorNameObject.ToString()
                                };

                                person.FavoriteColors.Add(color);
                            }
                        }
                    }
                }
            }
        }

        return personDict.Values;
    }

Ответ 3

SqlDataReader поддерживает набор результатов. Попробуйте это.

using (SqlConnection connection = new SqlConnection("connection string here"))
        {
            using (SqlCommand command = new SqlCommand
                   ("SELECT Id, Name FROM Person WHERE Id=1; SELECT Id, Name FROM FavoriteColors WHERE PersonId=1;SELECT Id, Name FROM Hobbies WHERE PersonId=1", connection))
            {
                connection.Open();
                using (SqlDataReader reader = command.ExecuteReader())
                {
                    Person p = new Person();
                    while (reader.Read())
                    {
                        p.Id = reader.GetInteger(0);
                        p.Name = reader.GetString(1);
                    }

                    if (reader.NextResult())
                    {
                        while (reader.Read())
                        {
                            var clr = new Color();
                            clr.Id = reader.GetInteger(0);
                            clr.Name = reader.GetString(1);
                            p.FavoriteColors.Add(clr);
                        }
                    }
                    if (reader.NextResult())
                    {
                        while (reader.Read())
                        {
                            var hby = new Hobby();
                            hby.Id = reader.GetInteger(0);
                            hby.Name = reader.GetString(1);
                            p.Hobbies.Add(clr);
                        }
                    }
                }
            }
        }

Ответ 4

Для этого, вероятно, проще использовать 3 отдельных запроса.

Запрос пользователя

SELECT * FROM Person

Затем сделайте свой цикл while по результатам этого запроса.

...
var persons = new List<Person>();
while (reader.Read())
{
    var person = new Person();
    Person.Id = reader.GetInt32(0);
    ... // populate the other Person properties as required

    // Get list of hobbies for this person
    // Use a query to get hobbies for this person id
    // e.g. "SELECT * FROM Hobby WHERE Hobby.PersonId = " + Person.Id

    // Get a list of colours
    // Use a query to get colours for this person id

}

Ответ 5

Я думаю, что проблема заключается не в том, как сопоставить полученные данные с объектом (для этого я бы предложил использовать подход кодеров), а скорее, что оператор select возвращает слишком много результатов.

SELECT 
    Person.Id AS PersonId, 
    Person.Name AS PersonName, 
    Hobby.Id AS HobbyId,
    Hobby.Name AS HobbyName,
    Color.Id AS ColorId,
    Color.Name AS ColorName
FROM Person
INNER JOIN Color on Person.Id = Color.PersonId
INNER JOIN Hobby on Person.Id = Hobby.PersonId";

мне кажется, что таблицы Color и Hobby содержат в себе PersonId, который присваивает их одному уникальному человеку. (Таким образом, Внутреннее соединение возвращается, т.е. {PersonId, blue, fishing}, {personId, red, fishing}, {personId, blue, swimming}, {personId, red, swimming}

вместо желаемого {personId, красный, рыбалка}, {personId, синий, плавание}

В случае, если я не пропустил это, я бы предложил вместо столбца ColorId и HobbyId добавить столбец Person. Если вы это сделаете, вы можете получить свои данные без избыточности, используя

SELECT 
    Person.Id AS PersonId, 
    Person.Name AS PersonName, 
    Hobby.Id AS HobbyId,
    Hobby.Name AS HobbyName,
    Color.Id AS ColorId,
    Color.Name AS ColorName
FROM Person
INNER JOIN Color on Person.ColorId = Color.Id
INNER JOIN Hobby on Person.HobbyId = Hobby.Id";

и подход кодеров для привязки результата к вашему классу Person даст желаемый результат.

edit: фактически код кодеров возвращает правильный результат в любом случае из-за

if (!person.FavoriteColors.Contains(color))

и

if (!person.Hobbies.Contains(hobby))

Ответ 6

Вы можете использовать запрос ниже, который возвращает одну строку для каждого человека. Цвета и хобби возвращаются в виде строки xml, вы можете анализировать его в своем коде.

select p.personId, p.personName
,cast((select colorId,colorName from Color as c where c.personId = p.personId for xml raw) as nvarchar(max)) as Colors
,cast((select hobbyId,hobbyName from Hobby as h where h.personId = p.personId for xml raw) as nvarchar(max)) as Hobbies
from Person as p

вы можете использовать этот код для анализа цветов

var root = XElement.Parse("<root>" + colorXml + "</root>");
var colors = root.Nodes()
    .Where(n => n.NodeType == XmlNodeType.Element)
    .Select(node =>
    {
        var element = (XElement)node;
        return new Color()
        {
            Id = Convert.ToInt32(element.Attribute("colorId").Value),
            Name = element.Attribute("colorName").Value
        };
    }).ToList();