ストアドプロシージャからの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を使用しますが、オームを使用することはできません。このデータを適切に平坦化する手法はありますか?
アイデアは、既存の行の個人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);
}
}
}
}
}
最終的には、ここで説明したすべてのアプローチが機能すると思います。パフォーマンスに焦点を当てることで、ソリューションを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;
}
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);
}
}
}
}
}
これを実現するには、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
}
問題は、取得したデータをオブジェクトにマップする方法ではなく(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";
テーブルColor
とHobby
にはそれぞれPersonId
が含まれていて、それらを1人の一意の人物に割り当てるように思えます。 (したがって、内部結合は、{personId、青、釣り}、{personId、赤、釣り}、{personId、青、水泳}、{personId、赤、水泳}を返します
目的の{personId、red、Fishing}、{personId、blue、Swimming}の代わりに
これを読み忘れなかった場合は、代わりに列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
クラスに結果をバインドするためのkodersアプローチはあなたに望ましい結果を与えます。
編集:実際にはkodersコードはいずれかの方法で正しい結果を返します
if (!person.FavoriteColors.Contains(color))
そして
if (!person.Hobbies.Contains(hobby))
以下のクエリを使用できます。これは、各人について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();