如何实现一对多关系

Posted

技术标签:

【中文标题】如何实现一对多关系【英文标题】:How to implement one to many relationship 【发布时间】:2015-02-06 21:20:20 【问题描述】:

我有一个来自存储过程的一对多关系。我在查询中有几个一对多的关系,我正在尝试将这些字段映射到 C# 对象。我遇到的问题是由于一对多的关系,我得到了重复的数据。这是我的代码的简化版本:

这是对象类:

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
            
        
    

如您所见,给定的 Person 可以有多种颜色和爱好。通常,我会使用实体框架来解决这个映射,但我们不允许使用任何形式。有没有一种技术可以正确地展开这些数据?

【问题讨论】:

或许你可以使用reflect构建sql并得到结果 @JeffreyZhang 你好,我不知道你的意思。什么是反射?以及如何在映射中构建 sql me?对不起,只是想理解的问题。 您将需要 3 个“选择”语句。 在对阅读器进行迭代时,检查人员列表中是否存在现有行人员 ID。如果不创建一个新的人对象并声明两个单独的列表来保存爱好和颜色信息。对于后续的迭代,继续填充这两个列表,因为它们将始终是相同的人员数据。一个你得到一个新人的新记录,将这些列表添加到人员对象并从一个新的人员对象开始 【参考方案1】:

这个想法是在对阅读器进行迭代时检查人员列表中是否存在现有行人员 ID。如果不创建一个新的人对象并声明两个单独的列表来保存爱好和颜色信息。对于后续的迭代,继续填充这两个列表,因为这些将始终是相同的人员数据。您可以为新人员创建新记录,将这些列表添加到人员对象并从新人员对象重新开始

下面是示例代码:

                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);
                        
                    
                
            
        

【讨论】:

@JeremyThompson 这个答案很棒。谢谢。【参考方案2】:

我认为最终,这里提到的所有方法都有效。我们可以通过关注性能来整合解决方案。

@Mark Menchavez 评论了当我们从一个简单的人员列表开始时反复返回数据库对性能的影响。对于一个庞大的列表,这种影响是显着的,应尽可能避免。

归根结底,最好的办法是获取尽可能少的数据块;在这种情况下,一个块将是理想的(如果连接不太昂贵)。数据库针对处理数据集进行了优化,我们将使用它来避免设置多个重复连接的开销(尤其是当我们通过线路连接到另一台机器上运行的 Sql 实例时)。

我将使用@Luke101 的方法,但只是将列表更改为值字典。键的哈希查找将比在 @Koder 的响应中使用 Where 更快。另请注意,我已将 SQL 更改为读取为 LEFT JOIN 以适应那些没有记录中的爱好或颜色的人员,并允许他们返回为 NULL(.NET 中的 DBNull)。

另请注意,由于表格和数据的形状,颜色和/或爱好可能会重复多次,因此我们也需要检查这些,而不是假设只有一种颜色和一种爱好。

我没有费心重复这里的课程。

        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;
    

【讨论】:

“我已将 SQL 更改为读取为 LEFT JOIN 以适应那些没有记录爱好或颜色的人,并允许他们返回为 NULL(.NET 中的 DBNull )." - 这可能很好,我还没有尝试过查询,但作者不会说FROM Person 表中是否有 NULL 人/颜色/爱好或加入吗?如果未指定,我认为它们是默认值。如果是这样,您提出的所有优化都会被这个错误的LEFT JOIN 优化所抵消? ps 不要误会,欢迎您和您的回答Stack Overflow :) @JeremyThompson,你试过查询吗?我的工作可能是没有任何关于 Color 或 Hobby 的记录,我想那是在冒犯他人。他的应用程序的指令可能是如果没有这些记录类型中的至少一种,则无法保存个人记录。但如果它们不是强制性的,那么 LEFT JOIN 将适合包括那些没有爱好或颜色的人。除非此查询的目的是仅检索具有其中一个或另一个的那些。 Luke101,你愿意称重吗?也许更多细节可以更好地调整脚本和流程? @Eniola 嘿,好问题。你是对的。我应该改用左连接。这样所有的人都将被加载。如果一个人没有颜色或爱好,那么他们将是空的。现在,我的真实查询确实使用了一些内部联接,因此很高兴看到考虑内部联接的示例代码。【参考方案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);
                        
                    
                
            
        

【讨论】:

我喜欢每个查询检索多个结果集的想法。如果您需要检索多个人,如何扩展此解决方案? 删除 where 条件或更改 where 条件以检索更多人。您可能需要在检索后进行一些数据操作来隔离数据。 如果我只想要前 10 个人,我将如何使用此方法? 您可以使用嵌套/子查询。最初的问题是处理多个查询并填充数据模型。所以我提出了一个具有多个结果集的解决方案。现在我看到问题发生了一些变化。【参考方案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】:

我认为问题不在于如何将检索到的数据映射到对象(为此我建议使用 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,它将它们分配给一个唯一的人。 (因此内连接返回即 personId, blue, Fishing, personId, red, Fishing, personId, blue, Swing, personId, red, Swing

而不是想要的 personId,红色,钓鱼,personId,蓝色,游泳

如果我没有读错,我建议改为在表 Person 中添加一列 ColorIdHobbyId。如果您这样做了,您可以使用

检索没有冗余的数据
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";

koders 方法将结果绑定到您的 Person 类将为您提供所需的结果。

编辑:实际上,由于

,koders 代码以任何一种方式返回正确的结果
if (!person.FavoriteColors.Contains(color))

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

【讨论】:

将 ColorId 和 HobbyId 添加到 Person 表会更改关系。在问题中,每个人都可以有多种喜欢的颜色和爱好。在提议的表格结构中,每个人现在可以拥有一种颜色和爱好。 你是绝对正确的 - 他可能不想创建具有相同 personId 的多个数据集。不能将相同的颜色分配给多个人,这感觉很不自然。【参考方案6】:

您可以使用下面的查询,它为每个人返回一行。 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();

【讨论】:

以上是关于如何实现一对多关系的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Hibernate 和 postgresql 为一对多关系实现每个表的自动递增 id 字段

mybatis如何实现一对多关联关系

如何在 node.js 中使用 sequelize-npm 实现与连接表的一对多关系

使用一对一关系实现一对多关系,没有循环依赖

java中如何在map中实现一对多的关系?

Swift如何实现通用类型的弱引用数组(下)