模拟使用包含等于等于的contains运算符的联接?

时间:2018-07-06 18:11:37

标签: c# linq iqueryable

我发现join运算符does not allow the use of Contains以及only performs equijoins。但是,我需要执行“不等分”。

我特别需要使用以下设置编写查询。给定两种类型的对象ClassStudent

public class Class
{
    public string Name { get; set; } = "";
    public List<Guid> Students { get; set; } = new List<Guid>();
}

public class Student
{
    public Guid StudentId { get; set; } = Guid.NewGuid();
    public int Grade { get; set; } = 0;
}

Class通过其Student引用其StudentId的地方。我想写一个子句来查找所有Class的平均成绩都高于某个值的所有Student


class Program
{
    static void Main(string[] args)
    {
        // Create all of the students
        var class1Students = new List<Student>()
        {
            new Student() {Grade = 70 },
            new Student() {Grade = 70 }
        };
        var class2Students = new List<Student>()
        {
            new Student() {Grade = 80 },
            new Student() {Grade = 80 }
        };
        var class3Students = new List<Student>()
        {
            new Student() {Grade = 90 },
            new Student() {Grade = 90 }
        };
        var allStudents = new List<Student>();
        allStudents.AddRange(class1Students);
        allStudents.AddRange(class2Students);
        allStudents.AddRange(class3Students);

        // Create all of the classes
        var class1 = new Class()
        {
            Name = "Class1",
            Students = class1Students.Select(s => s.StudentId).ToList()
        };
        var class2 = new Class()
        {
            Name = "Class2",
            Students = class2Students.Select(s => s.StudentId).ToList()
        };
        var class3 = new Class()
        {
            Name = "Class3",
            Students = class3Students.Select(s => s.StudentId).ToList()
        };
        var allClasses = new List<Class>() { class1, class2, class3  };

        // Get all classes where the average grade is above 70
        var query = from cls in allClasses
                    join std in allStudents on 


    }
}

我想这样写查询

var query = from cls in allClasses
    join std in allStudents on cls.Students.Contains(std.StudentId) into clsStds
    where clsStds.Select(aStd => aStd.Grade).Average() > 70
    select cls;

尽管这显然是无效的语法。 page linked above提供了一个非等值连接的示例,尽管我试图在此处应用它,但似乎无法正确地复制它(和/或我已经严重混淆了自己)。

如何模拟上面描述的join的类型?

4 个答案:

答案 0 :(得分:6)

首先,您的数据模型错误。学生没有一个年级。 他们在一个班级中有一个成绩,而您的模型不占该成绩。您需要第三个表,其中包含学生,班级和年级列。我强烈建议您解决此问题。

解决上述问题很简单,但是我不喜欢到目前为止提出的任何解决方案。它们基本上是合理的,但可能会更加高效和强大。

您遇到的基本问题是:您没有从学生ID到学生对象的快捷简便的方法。 首先解决该问题

var idToStudent = allStudents.ToLookup(s => s.id);

太好了。现在解决方案很简单:

var query = 
  from cls in allClasses
  let grades = from id in cls.Students select idToStudent(id).Grade
  where grades.Any()
  where grades.Average() > 70
  select cls;

请注意,我们正在测试是否有Any年级,因为可能有一个班级没有学生。如果Average被要求平均取零物品,则会崩溃,因此进行检查是明智的。

修复数据模型以正确关联学生,班级和成绩后,它将是:

var query = 
  from cls in allClasses
  let grades = 
    from grade in allGrades 
    where grade.Class == cls 
    select grade.grade
  where grades.Any()
  where grades.Average() > 70
  select cls;

在一组经过适当设计的表格中,计算平均值时不会将学生ID纳入其中;您可以直接关联成绩和班级。

现在,通过指出您需要一种C#不支持的联接来开始这个问题。不,您需要修复数据关系,然后C#支持您需要的那种连接!上面的内容可以更有效地写为

var query = 
  from cls in allClasses
  join g in allGrades on cls equals g.Class into grades
  where grades.Average() > 70
  select cls;

由于C#不会产生空组,因此检查“任何”的需求消失了。

这就是您需要的联接;正确设计表格,然后使用它!

答案 1 :(得分:1)

您使用的是Entity Framework还是类似的?实体之间是否具有导航属性?如果是这样,也许您可​​以GroupBy在学生身上,然后导航到父对象。像这样:

    var query = allStudents
        .GroupBy(i=>i.Class)
        .Select(i=>new{
            Class = i,
            Average = i.Average(j=>j.Grade)
        })
        .Where(i=>i.Average > 70)
        .ToList();

答案 2 :(得分:0)

我找到了一种天真的解决方案。首先,我们将学生中的Guid引用“解析”为实际的Student对象

var clsStd = 
    from cls in allClasses
    select new
    {
        cls,
        stdObjs = allStudents.Where(aStd => cls.Students.Contains(aStd.StudentId))
    };

然后我们可以查询该匿名对象集合

var classes = 
   from clsStdObj in clsStd
   where clsStdObj.stdObjs.Select(stdObj => stdObj.Grade).Average() > 70
   select clsStdObj.cls;

然后解析为“平均学生成绩高于70的所有班级的集合”。

但是我仍然对不太天真的解决方案持开放态度。

答案 3 :(得分:0)

这可能是一个解决方案(感谢@EricLippert提供有关使用Any()的建议,以避免在classGrades为空时可能发生的崩溃):

var query = from _class in allClasses
            let classGrades = from std in allStudents
                           where _class.Students.Contains(std.StudentId)
                           select std.Grade
            where classGrades.Any()
            where classGrades.Average() > 70
            select _class;

基本上,对于每个Class,我都会创建一个新的子查询,在其中选择该Student中的所有Class,并且仅投影Grade,因此结果是IEnumerable<string>。下一部分很容易:在年级上Average()

出于好奇,我已经使用Stopwatch来比较您的解决方案与我的解决方案,即使它只是指示性的,延迟时间之间的比率(我/您的)约为0.02。