【问题标题】:Emulating a join which uses a contains operator opposed to equals?模拟使用包含运算符而不是等于的连接?
【发布时间】:2018-07-06 18:11:36
【问题描述】:

我发现join 运算符does not allow the use of Containsonly performs equijoins。但是,我需要执行“not equijoin”。

我特别需要使用以下设置编写查询。给定两种类型的对象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 通过StudentId 引用其Student。我想写一个子句来查找所有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 的类型?

【问题讨论】:

  • 所以你只想要所有班级成绩都在 70 分以上的学生?
  • 我想要反过来,我想要所有学生平均成绩在 70 以上的班级。
  • 您标记了问题IQueryable,但您似乎正在使用LINQ to Objects;你能解释为什么标签?我通过查找提供的解决方案假定 LINQ to Objects。
  • IQueryable 来自于 MongoDB 中LinqExtension 的使用。这个问题代表了与我希望构建到我拥有的 MongoDB 的查询的更简单和类似的情况(因此您在回答中提到了奇怪的数据模型)。​​
  • 对不起,IMongoQueryable,我这里才意识到这个Mongo的具体用法。

标签: c# linq iqueryable


【解决方案1】:

首先,您的数据模型是错误的。学生没有一个年级。 他们有一个班级的成绩,而您的模型没有考虑到这一点。您需要第三张表,其中包含学生、班级和年级列。我强烈建议您解决这个问题。

解决上述问题很简单,但我不喜欢目前提出的任何解决方案。它们大多是合理的,但可能会更有效和更健壮。

您遇到的根本问题是:您没有快速简便的方法从学生 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# 不会生成空组,因此不再需要检查 Any。

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

【讨论】:

    【解决方案2】:

    您使用的是实体框架还是类似的?你有实体之间的导航属性吗?如果是这样,也许您可​​以在学生上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();
    

    【讨论】:

    • 我对实体框架不熟悉,之前在很多地方见过它,但我不确定它是什么或用于什么。问题是由于尝试以问题中指定的方式查询 MongoDB。
    • 啊,我明白了。实体框架是数据层的 ORM,并且(除其他外)为数据存储中的对象生成类。这很方便,因为它允许您以面向对象的方式处理数据层,而无需付出太多努力。
    • 你有什么方法可以在你的模型中从学生到班级?
    • 无法从Student 转到Class。只有Class 引用Student(通过Guid id),Student 即使在一个类中也“不知道”。
    【解决方案3】:

    我找到了一种天真的解决问题的方法。首先,我们将 student 中的 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】:

      这可能是一个解决方案(感谢@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,我都会创建一个新的子查询,在其中选择Class 中的所有Student,并且我只投影Grade,所以结果是IEnumerable&lt;string&gt; .下一部分很简单:Average() 在成绩上。

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

      【讨论】:

      • 这是一个合理的解决方案尝试,但存在一些问题。如果某个班级没有学生,则Average 崩溃;您可以添加where classGrades.Any。其次,在班级已经有成员学生列表的情况下,遍历所有学生并拉出班级中的学生有点奇怪!
      • @EricLippert 谢谢你提醒我Average 崩溃,我没想到。然而关于第二点,我知道这很奇怪,但鉴于他的模型和约束,我选择为他提供解决方案。我读了你的回答,你是绝对正确的,因为你强调了这背后的问题。
      猜你喜欢
      • 1970-01-01
      • 2011-11-04
      • 1970-01-01
      • 2017-09-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-02-06
      • 1970-01-01
      相关资源
      最近更新 更多