web-dev-qa-db-ja.com

1対多の関係を実装する方法

ストアドプロシージャからの1対多の関係があります。クエリに1対多の関係がいくつかあり、これらのフィールドをC#オブジェクトにマップしようとしています。私が抱えている問題は、1対多の関係のために重複データを取得することです。これが私のコードの簡単なバージョンです:

オブジェクトクラスは次のとおりです。

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を使用しますが、オームを使用することはできません。このデータを適切に平坦化する手法はありますか?

25
Luke101

アイデアは、既存の行の個人IDが個人リストに存在するかどうかをリーダーで繰り返し確認することです。そうでない場合は、新しい人物オブジェクトを作成し、趣味と色の情報を保持する2つの個別のリストを宣言します。これらは常に同じ人物のデータになるため、以降の反復では、これら2つのリストの入力を続けます。新しい人物の新しいレコードを取得し、これらのリストを人物オブジェクトに追加して、新しい人物オブジェクトからやり直す

以下はサンプルコードです:

                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);
                        }
                    }
                }
            }
        }
15
koder

最終的には、ここで説明したすべてのアプローチが機能すると思います。パフォーマンスに焦点を当てることで、ソリューションを1つ上げることができます。

@Mark Menchavezは、単純な人のリストから始めたときに、データベースに繰り返し戻ることによるパフォーマンスへの影響についてコメントしました。膨大な数のリストの場合、この影響は重大であり、できるだけ回避する必要があります。

結局のところ、最善の方法は、データをできるだけ少ないチャンクでフェッチすることです。この場合、1つのチャンクが理想的です(結合が高すぎない場合)。データベースはデータのセットを操作するために最適化されており、これを使用して、複数の繰り返し接続を設定するオーバーヘッドを回避します(特に、別のマシンで実行されているSQLのインスタンスにネットワーク経由で接続している場合)。

@ Luke101のアプローチを使用しますが、リストから値の辞書に変更します。キーのハッシュルックアップは、@ Koderの応答でWhereを使用するよりも高速になります。また、SQLを変更してLEFT JOINとして読み取るように変更しました。これは、レコードに趣味または色がなく、それらをNULL(.NETではDBNull)として返すことができる人物に対応するためです。

また、テーブルとデータの形状により、色や趣味が複数回繰り返される可能性があるため、色と趣味が1つだけであると想定するのではなく、それらもチェックする必要があることに注意してください。

ここでクラスを繰り返す気になりませんでした。

        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;
    }
12
Eniola

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);
                        }
                    }
                }
            }
        }
5
Ahuman

これを実現するには、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

}
3

問題は、取得したデータをオブジェクトにマップする方法ではなく(kodersアプローチを使用することをお勧めします)、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";

テーブルColorHobbyにはそれぞれPersonIdが含まれていて、それらを1人の一意の人物に割り当てるように思えます。 (したがって、内部結合は、{personId、青、釣り}、{personId、赤、釣り}、{personId、青、水泳}、{personId、赤、水泳}を返します

目的の{personId、red、Fishing}、{personId、blue、Swimming}の代わりに

これを読み忘れなかった場合は、代わりに列ColorIdHobbyIdをテーブル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クラスに結果をバインドするためのkodersアプローチはあなたに望ましい結果を与えます。

編集:実際にはkodersコードはいずれかの方法で正しい結果を返します

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

そして

if (!person.Hobbies.Contains(hobby))
3
H W

以下のクエリを使用できます。これは、各人について1行を返します。 Colors and Hobbiesは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();
2
Tien Dinh