Dapper一对多关系

3

我有以下结构的3个表:

CREATE TABLE [User](
    Id int NOT NULL,
    Name varchar(50)
    PRIMARY KEY (Id)
)

CREATE TABLE [Role](
    Id int NOT NULL,
    UserId int NOT NULL,
    Name varchar(50),
    PRIMARY KEY (Id),
    FOREIGN KEY (UserId) REFERENCES [User](Id)
)


CREATE TABLE [Description](
    Id int NOT NULL,
    RoleId int NOT NULL,
    Name varchar(50)
    FOREIGN KEY (RoleId) REFERENCES [Role](Id)
)

正如您所看到的,它是一个嵌套两次的一对多关系。在代码中,我有以下类来表示它们:

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

    public IEnumerable<Role> Roles { get; set; }
}

public class Role
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public string Name { get; set; }

    public IEnumerable<Description> Descriptions { get; set; }
}

public class Description
{
    public int Id { get; set; }
    public int RoleId { get; set; }
    public string Name { get; set; }
}

现在我需要查询用户,并获取所有相关字段。我已经想出了一种使用QueryMultiple的方法,例如:
var queryOne = "SELECT Id, Name FROM [User] WHERE Id = 1";
var queryTwo = "SELECT r.Id, r.UserId, r.Name FROM  [User] u INNER JOIN [Role] r ON u.Id = r.UserId WHERE u.Id = 1";
var queryThree = "SELECT d.Id, d.RoleId, d.Name FROM  [User] u INNER JOIN [Role] r ON u.Id = r.UserId INNER JOIN [Description] d ON r.Id = d.RoleId WHERE u.Id = 1";

var conn = new SqlConnection();

using (var con = conn)
{
    var result = con.QueryMultiple(queryOne + " " + queryTwo + " " + queryThree);
    var users = result.Read<User>().FirstOrDefault();
    var roles = result.Read<Role>();
    var descriptions = result.Read<Description>();
    if (users != null && roles != null)
    {
        users.Roles = roles;
        Console.WriteLine("User: " + users.Name);
        foreach (var role in users.Roles)
        {
            Console.WriteLine("Role: " + role.Name);
            if (descriptions != null)
            {
                role.Descriptions = descriptions.Where(d => d.RoleId == role.Id);
                foreach (var roleDescription in role.Descriptions)
                {
                    Console.WriteLine("Description: " + roleDescription.Name);
                }
            }
        }
    }
}

结果是:

用户:Bob
角色:测试人员
描述:测试人员第一描述
描述:测试人员第二描述
描述:测试人员第三描述
角色:经理
描述:经理第一描述
描述:经理第二描述
描述:经理第三描述
角色:程序员
描述:程序员第一描述
描述:程序员第二描述
描述:程序员第三描述

主要问题: 虽然上面的代码可以工作,但感觉太凌乱了。我想知道是否有更好/更容易的方法来实现这个?
额外加分: 请随意提出比使用内部连接更好的查询方式。我的目标是提高性能。
编辑:
我也想到了选项2,但我认为这不是一个好的解决方案。使用选项2,我创建了第4个对象,其中包含3个对象组合的结果,如下所示:
public class Combination
{
    public int UserId { get; set; }
    public string UserName { get; set; }
    public int RoleId { get; set; }
    public string RoleName { get; set; }
    public int DescriptionId { get; set; }
    public string DescriptionName { get; set; }
}

然后我这样处理:
var queryFour = "SELECT u.Id as 'UserId', u.Name as 'UserName', r.Id as 'RoleId', r.Name as 'RoleName', d.Id as 'DescriptionId', d.Name as 'DescriptionName' FROM  [User] u INNER JOIN [Role] r ON u.Id = r.UserId INNER JOIN [Description] d ON r.Id = d.RoleId WHERE u.Id = 1";

var conn = new SqlConnection();
using (var con = conn)
{
    var myUser = new User();
    var result = con.Query<Combination>(queryFour);
    if (result != null)
    {
        var user = result.FirstOrDefault();
        myUser.Id = user.UserId;
        myUser.Name = user.UserName;
        var roles = result.GroupBy(x => x.RoleId).Select(x => x.FirstOrDefault());
        var myRoles = new List<Role>();
        if (roles != null)
        {
            foreach (var role in roles)
            {
                var myRole = new Role
                {
                    Id = role.RoleId,
                    Name = role.RoleName
                };

                var descriptions = result.Where(x => x.RoleId == myRole.Id);
                var descList = new List<Description>();
                foreach (var description in descriptions)
                {
                    var desc = new Description
                    {
                        Id = description.DescriptionId,
                        RoleId = description.RoleId,
                        Name = description.DescriptionName
                    };
                    descList.Add(desc);
                }
                myRole.Descriptions = descList;
                myRoles.Add(myRole);
            }
        }
        myUser.Roles = myRoles;
    }

    Console.WriteLine("User: " + myUser.Name);
    foreach (var myUserRole in myUser.Roles)
    {
        Console.WriteLine("Role: " + myUserRole.Name);
        foreach (var description in myUserRole.Descriptions)
        {
            Console.WriteLine("Description: " + description.Name);
        }
    }
}

使用这两种方法得到的输出结果相同,而第二种方法只使用了1个查询,而不是3个。
编辑2:需要考虑的一点是,我这三个表格的数据经常更新。
编辑3:
private static void SqlTest()
{
    using (IDbConnection connection = new SqlConnection())
    {
        var queryOne = "SELECT Id FROM [TestTable] With(nolock) WHERE Id = 1";
        var queryTwo = "SELECT B.Id, B.TestTableId FROM [TestTable] A With(nolock) INNER JOIN [TestTable2] B With(nolock) ON A.Id = B.TestTableId WHERE A.Id = 1";
        var queryThree = "SELECT C.Id, C.TestTable2Id FROM [TestTable3] C With(nolock) INNER JOIN [TestTable2] B With(nolock) ON B.Id = C.TestTable2Id INNER JOIN [TestTable] A With(nolock) ON A.Id = B.TestTableId WHERE A.Id = 1";

            var gridReader = connection.QueryMultiple(queryOne + " " + queryTwo + " " + queryThree);
            var user = gridReader.Read<Class1>().FirstOrDefault();

            var roles = gridReader.Read<Class2>().ToList();

            var descriptions = gridReader.Read<Class3>().ToLookup(d => d.Id);

            user.Roles= roles;

            user.Roles.ForEach(r => r.Properties = descriptions[r.Id].ToList());
    }
}

1
你可以使用一个叫做 Slapper.Automapper 的库来通过一个查询解决它:https://github.com/SlapperAutoMapper/Slapper.AutoMapper - silkfire
Slapper非常适合简单的操作,但是在处理大数据集或深度嵌套时可能会变慢。 - Tamas
我们使用了两种变体,最终基本上选择了第一种选项。您可以通过.ToLookup()调用来进行内存过滤。这非常简单且高效。在这个简单的情况下,您的第二个选项也可以工作,但如果您有许多列和许多连接,那么所选数据的指数增长会导致查询序列化性能差。 - Tamas
@Tamas 第一种解决方案的第三个查询与第二种解决方案中的查询完全相同。唯一的例外是我得到了额外的一列数据。因此,我知道第二种方法在数据库端更快。我想出了这些解决方案,但我不知道它们是否是好的实践。我试图理解正确的方法是什么。也许是直到调用子对象之前不查询它们?但我不确定如何实现。无论如何,我希望了解最佳处理方法。 - Bagzli
你为什么认为内连接很慢?比如说,一段时间以前,在 MSSQL 服务器上,内连接是最快的连接方式。 - user743414
显示剩余3条评论
3个回答

2
你的第一个选项可以简化为以下内容。这将消除深层次的控制结构嵌套。你可以将其推广到更深层次的嵌套,而不会增加更多的嵌套/复杂性。
var queryOne = "SELECT Id, Name FROM [User] WHERE Id = 1";
var queryTwo = "SELECT r.Id, r.UserId, r.Name FROM  [User] u INNER JOIN [Role] r ON u.Id = r.UserId WHERE u.Id = 1";
var queryThree = "SELECT d.Id, d.RoleId, d.Name FROM  [User] u INNER JOIN [Role] r ON u.Id = r.UserId INNER JOIN [Description] d ON r.Id = d.RoleId WHERE u.Id = 1";

var conn = new SqlConnection();

using (var con = conn)
{
    var gridReader = con.QueryMultiple(queryOne + " " + queryTwo + " " + queryThree);
    var user = gridReader.Read<User>().FirstOrDefault();
    if (user == null)
    {
        return;
    }

    var roles = gridReader.Read<Role>().ToList();
    var descriptions = gridReader.Read<Description>().ToLookup(d => d.RoleId);

    user.Roles = roles;
    roles.ForEach(r => r.Descriptions = descriptions[r.Id]);
}

就性能而言,它与您的第一选项表现相同。

我不会选择您的第二个选项(或类似的基于视图的选项):如果您有R个角色,并且每个角色平均有D个描述,则您将查询6 * R * D个单元格,而不是2 + 3 * R + 3 * D。如果R和D很高,您将查询更多数据,反序列化的成本将比运行3个查询而不是1个要高得多。


我尝试使用你的代码编写演示,但是 roles.ForEach(r => r.Descriptions = descriptions[r.Id]); 从未按照预期填充。当我对最终结果进行计数时,它总是为0。我正在进行性能测试,但似乎无法找出问题所在。不熟悉 ToLookup 方法。我将在我的问题中发布示例代码。 - Bagzli
如果你将 tolookup 改为 tolist,你会得到什么?你能得到预期的数据吗?如果不能,那么你的映射可能出了问题。 - Tamas

1

一种可选的方法(使用 .Net Core 的工作代码片段):

  1. 为什么不创建这样一个数据库视图 -

CREATE VIEW myview AS SELECT u.Id AS 'UserId', u.Name AS 'UserName', r.Id AS 'RoleId', r.Name AS 'RoleName', d.Id AS 'DescriptionId', d.Name AS 'DescriptionName' FROM User u INNER JOIN Role r ON u.Id = r.UserId INNER JOIN Description d ON r.Id = d.RoleId;

  1. Then you can use your cleaner query as below :

        public List<Combination> GetData(int userId)
        {
            String query = "select * from myview" + " where userId = " + userId + ";";
    
            using (System.Data.Common.DbConnection _Connection = database.Connection)
            {
                _Connection.Open();
                return _Connection.Query<Combination>(query).ToList();
            }
        }
    
  2. And your processing code will look like this :

注意:这可以进一步增强。
        public static void process (List<Combination> list)
        {
            User myUser = new User(); myUser.Id = list[0].UserId; myUser.Name = list[0].UserName;
            var myroles = new List<Role>(); var r = new Role(); string currentRole = list[0].RoleName;
            var descList = new List<Description>(); var d = new Description();

            // All stuff done in a single loop.
            foreach (var v in list)
            {
                d = new Description() { Id = v.DescriptionId, RoleId = v.RoleId, Name = v.DescriptionName };
                if (currentRole == v.RoleName)
                {
                    r = new Role() { Id = v.RoleId, Name = v.RoleName, UserId = v.UserId, Descriptions = descList };                    
                    descList.Add(d);
                }
                else
                {
                    myroles.Add(r);
                    descList = new List<Description>(); descList.Add(d);
                    currentRole = v.RoleName;
                }
            }
            myroles.Add(r);
            myUser.Roles = myroles;

            Console.WriteLine("User: " + myUser.Name);
            foreach (var myUserRole in myUser.Roles)
            {
                Console.WriteLine("Role: " + myUserRole.Name);
                foreach (var description in myUserRole.Descriptions)
                {
                    Console.WriteLine("Description: " + description.Name);
                }
            }
        }

从性能角度来看,内连接比其他查询形式(如子查询、相关子查询等)更好。但最终一切都归结于SQL执行计划。


@Bojan:如果你喜欢它,请点击绿色的Nike按钮或捐赠一些赏金... ;) - Koder101
据我理解,视图对于经常更新的数据并不好。我应该在之前提到过,但是我上面提到的三个对象经常被更新。根据 https://learn.microsoft.com/en-us/sql/relational-databases/views/views 的说法,“它们不适用于底层数据集经常更新的情况。”话虽如此,您仍然建议使用视图吗? - Bagzli
2
@Bojan:如果数据不断更新,我也不建议使用Views方法。然而,你可以选择存储过程来代替视图。 - Koder101

0

我的回答简而言之:

摘要。你应该将事物分解为它们所做的不同工作。C#提供了函数和类,它们可以帮助清理代码。如果你能看到一段代码并说“这段代码基本上是拿某些东西并用它来做某些事情以获得一些结果..”,那么就创建一个名为SomeResult DoBlah(SomeThing thing)的函数。

不要让更多的代码吓到你,它可能会使整体看起来更加复杂。但它将使小块的代码更容易理解,这使得整体更容易理解。

我的完整回答:

有几件事情你可以做,可以使代码更清晰。也许对自己最大的帮助是分离一些职责/关注点。你有很多不同的事情在进行,而且所有的事情都在一个地方,依赖于一切都保持不变,永远不变。如果你改变了某些东西,你将不得不在各个地方改变东西,以确保一切正常运行。这被称为“耦合”,而耦合是不好的...嗯-好的。

在最广泛的层面上,您有三件不同的事情要处理:1. 您正在尝试将逻辑应用于用户和角色(即,您想获取某个角色中所有用户的信息)。2. 您正在尝试从数据库中提取信息,并将其放入用户、角色和描述对象中。3. 您正在尝试显示有关用户、角色和描述的信息。

因此,如果我是您,我会至少有3个类。

  • Program 是应用程序的驱动程序。它应该有一个主函数,并应使用其他3个类。

  • DisplayManager 可以负责将信息写入控制台。它应该使用 IEnumerable<User> 构造,并应具有公共方法 public void DisplayInfo(),该方法将其用户列表写入控制台。

  • UserDataAccess 可用于从数据库中获取用户列表。我会让它公开一个方法 public IEnumerable<User> GetUsersByRoleID(int roleID)

这样,你的主程序看起来会像这样:

public static void Main(String[] args)
{
    int roleID = 6;

    UserDataAccess dataAccess = new UserDataAccess();
    IEnumerable<User> usersInRole = dataAccess.GetUsersByRoleID(roleID);

    DisplayManager displayManager = new DisplayManager(usersInRole);
    displayManager.DisplayInfo();
}

说实话,这只是我在问题具有更多功能(而不仅仅是获取一个特定信息)时所做的简化版。你应该研究一下SOLID原则,特别是“单一职责”和“依赖反转”。这些原则可以真正清理一些“混乱” /“臭味相投”的代码。
其次,关于你实际的数据访问,由于你正在使用dapper,你应该能够使用内置的dapper功能来映射嵌套对象。 我认为这个链接对你在这方面会有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接