views:

53

answers:

1

I have the following tables:

Person(Id, FirstName, LastName)
{
    (1, "John", "Doe"),
    (2, "Peter", "Svendson")
    (3, "Ola", "Hansen")
    (4, "Mary", "Pettersen")
}

Sports(Id, Name)
{
    (1, "Tennis")
    (2, "Soccer")
    (3, "Hockey")
}

SportsPerPerson(Id, PersonId, SportsId)
{
    (1, 1, 1)
    (2, 1, 3)
    (3, 2, 2)
    (4, 2, 3)
    (5, 3, 2)
    (6, 4, 1)
    (7, 4, 2)
    (8, 4, 3)
}

Looking at the tables, we can conclude the following facts:
John plays Tennis
John plays Hockey
Peter plays Soccer
Peter plays Hockey
Ola plays Soccer
Mary plays Tennis
Mary plays Soccer
Mary plays Hockey

Now I would like to create a Linq2Sql query which retrieves the following:
Get all Persons who play Hockey and Soccer

Executing the query should return: Peter and Mary
Anyone has any idea's on how to approach this in Linq2Sql?

+1  A: 

One of the great things about Linq is that you don't HAVE to write this all as one monolithic query because it won't actually execute until you enumerate the results anyway. You could write a single query, but you don't have to. Instead, you can write this as multiple, separate queries, increasing the readability, and clarifying your intent.

var sportIds = Sports
    .Where(s => s.Name == "Hockey" || s.Name == "Soccer")
    .Select(s => s.Id);

var people = Person.Where(p => SportsPerPerson
    .Count(spp => (spp.PersonId == p.Id) 
    && sportIds.Contains(spp.SportId)) == 2);

First, we get the collection of sport Ids we're interested in. Then, we find all the people with two sports in the first list. Although it's expressed as multiple queries, Linq will compress it all into one operation for us when we finally enumerate the results.

EDIT: Here is a complete test class illustrating the query:

using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace L2STest
{
    public class Sport
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Person
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    public class SportPerPerson
    {
        public int Id { get; set; }
        public int PersonId { get; set; }
        public int SportId { get; set; }
    }

    [TestClass]
    public class SportsTest
    {
        private List<Person> persons;
        private List<Sport> sports;
        private List<SportPerPerson> sportsPerPerson;

         [TestInitialize]
         public void MyTestInitialize()
         {
             persons = new List<Person>
             {
                 new Person {Id = 1, FirstName = "John", LastName = "Doe"},
                 new Person {Id = 2, FirstName = "Peter", LastName = "Svendson"},
                 new Person {Id = 3, FirstName = "Ola", LastName = "Hansen"},
                 new Person {Id = 4, FirstName = "Marv", LastName = "Petterson"},
             };

             sports = new List<Sport>
             {
                 new Sport {Id = 1, Name = "Tennis"},
                 new Sport {Id = 2, Name = "Soccer"},
                 new Sport {Id = 3, Name = "Hockey"},
             };

             sportsPerPerson = new List<SportPerPerson>
             {
                 new SportPerPerson {Id = 1, PersonId = 1, SportId = 1}, 
                 new SportPerPerson {Id = 2, PersonId = 1, SportId = 3}, 
                 new SportPerPerson {Id = 3, PersonId = 2, SportId = 2}, 
                 new SportPerPerson {Id = 4, PersonId = 2, SportId = 3}, 
                 new SportPerPerson {Id = 5, PersonId = 3, SportId = 2}, 
                 new SportPerPerson {Id = 6, PersonId = 3, SportId = 1}, 
                 new SportPerPerson {Id = 7, PersonId = 4, SportId = 2}, 
                 new SportPerPerson {Id = 8, PersonId = 4, SportId = 3}, 
             };
         }

        [TestMethod]
        public void QueryTest()
        {
            var sportIds = sports
                .Where(s => s.Name == "Hockey" || s.Name == "Soccer")
                .Select(s => s.Id);

            var people = persons.Where(p => sportsPerPerson
                .Count(spp => (spp.PersonId == p.Id)
                && sportIds.Contains(spp.SportId)) == 2); 

            Assert.AreEqual(2, people.Count());
            Assert.AreEqual("Peter", people.First().FirstName);
            Assert.AreEqual("Marv", people.Last().FirstName);
        }
    }
}
Mel
using `let` and the query comprehension syntax is an option too.
Johannes Rudolph
I could be wrong... but executing this query returns me 0 records ;o sportIds does have the correct ids though
Bas
I have added a complete test class to the answer. This should illustrate the answer, and returns Peter and Marv as expected.
Mel
Great job on explaining! Only problem is performance, this doesn't work on really large collections >.< I'm using a db with alot of records and the code can't handle it
Bas
I suppose that depends on how you're running the query. If you're using a linq-aware data source such as Linq to SQL, or the Entity Framework, the query will be translated into SQL, and executed on the database server, so your performance should be great. If your data source is not linq aware, then I'm not sure there's a lot you can do. You'll always be retrieving and traversing entire tables to get your answer. That's not a limitation of Linq, so much as a limitation of the data layer you are operating over.
Mel
StackOverflow runs on L2S, so big tables are not the cause, but rather how they are being queried. Make sure you're not trying to retrieve the tables into a list or collection in memory first. Make sure you're not calling ToList() anywhere. Let L2S get what it needs from the database, and you should be just fine.
Mel
Hmm ok :] I'll take a look into that! Really thanks for explaining it so clearly +1
Bas