代码之家  ›  专栏  ›  技术社区  ›  KDecker

模拟使用与等于相反的contains运算符的联接?

  •  3
  • KDecker  · 技术社区  · 6 年前

    我发现 join 操作人员 does not allow the use of Contains 因此 only performs equijoins 是的。不过,我需要表演一个“不平等连接”。

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

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

    其中 等级 引用ITS 学生 他们的 StudentId 是的。我想写一个条款 等级 其中 学生 高于某个值。


    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 提供了一个非equijoin的例子,尽管我试图在这里应用它,但似乎无法正确地复制它(和/或我已经严重地混淆了自己)。

    我怎样才能模仿 参加 我在上面描述过?

    4 回复  |  直到 6 年前
        1
  •  6
  •   Eric Lippert    6 年前

    首先,你的数据模型是错误的。学生没有一个年级。 他们在一个班级里有一个年级,而你的模型并不能说明这一点 是的。您需要第三个表,其中有student、class和grade列。我强烈建议你把它修好。

    如前所述解决问题很简单,但我不喜欢目前为止提出的任何解决方案。它们基本上是合理的,但可能会更有效率和活力。

    你所面临的根本问题是:你没有一个从学生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不会产生空组,因此检查是否有任何需要消失。

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

        2
  •  1
  •   Jonathan    6 年前

    您使用的是实体框架还是类似的?实体之间是否有导航属性?如果是的话,也许你可以 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();
    
        3
  •  0
  •   KDecker    6 年前

    我找到了一种解决问题的天真办法。首先我们“解决”了 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分以上的所有班级的集合”。

    但我仍然愿意接受不那么幼稚的解决方案。

        4
  •  0
  •   Marco Luzzara    6 年前

    这可能是一个解决方案(感谢@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 在那里面的 等级 ,我只投射 Grade ,因此结果是 IEnumerable<string> 是的。下一部分很简单: Average() 在成绩上。

    出于好奇,我用了 Stopwatch 要将您的解决方案与我的解决方案进行比较,即使它纯粹是指示性的,延迟时间(我的/您的)之间的比率约为0.02。