3

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());
    }
}
Bagzli
  • 6,254
  • 17
  • 80
  • 163
  • 1
    You can solve it with one query by using a library called `Slapper.Automapper`: https://github.com/SlapperAutoMapper/Slapper.AutoMapper – silkfire Dec 01 '17 at 22:42
  • Slapper is great for simplicity, but it can be slow with large data sets, or deep nesting. – Tamas Dec 04 '17 at 19:55
  • Have you measured the performance of both of your solutions? What have you seen? Are they acceptable in speed? If so, why do you want to optimize any of them? – Tamas Dec 04 '17 at 19:58
  • We have used both variants, and in the end chose basically the first option. You can do the in memory filtering with `.ToLookup()` calls. That's very simple and performant. In this simple case, your second option works too, but if you have many columns and many joins, then the exponential growth of the selected data will result in poor (query) serialization performance. – Tamas Dec 04 '17 at 20:05
  • @Tamas The third query of the first solution is exactly the same as the query in the second solution. The only exception is I'm getting an extra column worth of data. For this reason, I know that second approach is faster on the database end. I came up with these solutions but I do not know if they are good practices or not. I am trying to understand what is the correct approach to this. Maybe it's not to query children objects until they are called? But I'm not sure how to achieve that. In any case, I'm hoping to understand the best approach for doing this. – Bagzli Dec 04 '17 at 21:35
  • @Bojan Option #2 is used by EF6 while option #1 - by EF Core. It can't be said in general which one is better. – Ivan Stoev Dec 05 '17 at 11:07
  • @Bojan, that extra column which you read might make a difference if it's deserialized. – Tamas Dec 05 '17 at 20:05
  • Why do you think that inner joins are slow? Some time ago for example a inner join was the fastest join on an mssql server. – user743414 Dec 11 '17 at 11:27

3 Answers3

2

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.

Tamas
  • 6,260
  • 19
  • 30
  • I tried writing a demo with your code, however, the `roles.ForEach(r => r.Descriptions = descriptions[r.Id]);` never fills up how its meant to. When I do a count on the final result, it is always 0. I was doing a performance test and I can't seem to figure out whats wrong. Not familiar with ToLookup method. I will post sample code on my question. – Bagzli Dec 11 '17 at 01:06
  • What do you get if you change tolookup to tolist? Do you get the expected data back? If not, then something is wrong with your mapping. – Tamas Dec 11 '17 at 15:59
1

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.

Koder101
  • 844
  • 15
  • 28
  • @Bojan: If you like it, please hit the Geen Nike button or donate some bounty... ;) – Koder101 Dec 04 '17 at 21:41
  • From my understanding Views are bad for data that is constantly updated. I should have mentioned this before, but the 3 objects I have mentioned above are updated quite often. According to: https://learn.microsoft.com/en-us/sql/relational-databases/views/views "They are not well-suited for underlying data sets that are frequently updated." Having said that, would you still suggest to go about and use Views? – Bagzli Dec 04 '17 at 21:47
  • 2
    @Bojan: In the event that data is constantly updated, I would also not suggest the Views approach. However, instead of view, you can go with Stored procs. – Koder101 Dec 05 '17 at 06:31
0

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 (i.e. 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.