简体   繁体   中英

Dapper One To Many Relationship

I have 3 tables structured in the following way:

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

As you can see it is one to many relationship nested twice. In the code I have the following classes to represent them:

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

Now I need to query for the user and also get all fields that come with it. I have figured out a way to do that using QueryMultiple such as this:

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

The result is:

User: Bob
Role: Tester
Description: Tester First Description
Description: Tester Second Description
Description: Tester Third Description
Role: Manager
Description: Manager First Description
Description: Manager Second Description
Description: Manager Third Description
Role: Programmer
Description: Programmer First Description
Description: Programmer Second Description
Description: Programmer Third Description

Main Question: While the above code works it feels too messy. I was wondering if there is a better/easier way to achieve this?

Bonus Points: Please also feel free to suggest a better way to query this than using inner joins. My goal is to improve performance.

EDIT:

I came up with option two as well, but again I don't think its a good solution. With option 2 I create a 4th object that will contain results of the 3 objects combined such as this:

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

Then I process it like this:

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

The resulting output is the same in both methods and the second method uses 1 query as opposed to 3.

EDIT 2: Something to consider, my data for these 3 tables are updated often.

EDIT 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());
    }
}

An alternative approach (Working code snippet with .Net Core):

  1. Why not create a database View like this -

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 :

[ Note: This can be enhanced even further.]

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

From the performance standpoint, Inner Joins are better than other forms of query like subquery, Co-related subquery, etc. But ultimately everything boils down to the SQL execution plan.

Your first option can be simplified to the following. It removes the deep control structure nesting. You can generalize it to deeper nesting without adding more nesting/complexity.

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

Performance-wise it behaves the same as your first option.

I would't go with your second option (or the similar view based one): in case you have R roles, and on average D descriptions per roles, you would query 6*R*D cells instead of 2+3*R+3*D. If R and D are high, you'd be querying a lot more data, and the cost of deserialization will be to high compared to running 3 queries instead of 1.

The tl;dr of my answer to you:

Abstract. You should break-up things into the different jobs they do. C# gives you functions and classes, they can help clean up code a lot.

If you can look at a chunk of code and say "this chunk is basically taking something and doing blah with it to get some result..", then make a function called SomeResult DoBlah(SomeThing thing)

Don't let a little more code scare you, it may make the big picture seem more complex at first glance. But it will make small chunks of code way easier to understand, and that makes the big picture easier to understand.

My full answer:

There are a couple of things you could do that would make this cleaner. Probably the biggest thing you could do for yourself, is to separate out some of the responsibilities/concerns. You have a lot of different things going on, and all of it is in one place, relying on everything staying exactly like it is, forever. If you change something, you will have to change things all over the place to make sure everything works. This is called "coupling", and coupling is bad... um-kay.

At the broadest level, you have three different things going on: 1. You are trying to apply logic to users and roles (ie You want to get info on all of the users in a role.) 2. You are trying to pull info from a database, and fit it into User, Role, and Description objects. 3. You are trying to display information about Users, Roles, and Descriptions.

So if I were you, I would have at least 3 classes right there. \\

  • Program that is the driver of your application. It should have a main function and should use the other 3 classes.

  • DisplayManager that can be responsible for writing info out to a console. It should be constructed with an IEnumerable<User> and should have a public method public void DisplayInfo() that will write its list of users to a console.

  • UserDataAccess that can be used for fetching a list of users from a database. I would have it expose a public method for public IEnumerable<User> GetUsersByRoleID(int roleID)

This way, your main program would look something like this:

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

Honestly, this is a simplified version of what I would do if the problem had more functionality then just getting a single specific piece of info. You should look into SOLID principles. Specifically "Single Responsibility" and "Dependency Inversion", these principles can really clean up some "messy"/"smelly" code.

Next, as far as your actually data access goes, since you are using dapper, you should be able to map nested objects using build in dapper functionality. I believe this link should be of some help to you in this aspect.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM