执行内部联接

在关系数据库术语中,内部联接会生成一个结果集,在该结果集中,第一个集合的每个元素对于第二个集合中的每个匹配元素都会出现一次。 如果第一个集合中的元素没有匹配元素,则它不会出现在结果集中。 由 C# 中的 join 子句调用的 Join 方法可实现内部联接。

本文演示如何执行内联的四种变体:

  • 基于简单键使两个数据源中的元素相关联的简单内部联接。

  • 基于复合键使两个数据源中的元素相关联的内部联接。 复合键是由多个值组成的键,使你可以基于多个属性使元素相关联。

  • 在其中将连续联接操作相互追加的多联接

  • 使用分组联接实现的内部联接。

备注

本主题中的示例使用下面的数据类:

record Person(string FirstName, string LastName);
record Pet(string Name, Person Owner);
record Employee(string FirstName, string LastName, int EmployeeID);
record Cat(string Name, Person Owner) : Pet(Name, Owner);
record Dog(string Name, Person Owner) : Pet(Name, Owner);

以及查询对象集合中的 Student 类。

示例 - 简单键联接

下面的示例创建两个集合,其中包含两种用户定义类型 PersonPet 的对象。 查询使用 C# 中的 join 子句将 Person 对象与 Owner 是该 PersonPet 对象匹配。 C# 中的 select 子句定义结果对象的外观。 在此示例中,结果对象是由所有者名字和宠物姓名组成的匿名类型。

Person magnus = new(FirstName: "Magnus", LastName: "Hedlund");
Person terry = new("Terry", "Adams");
Person charlotte = new("Charlotte", "Weiss");
Person arlene = new("Arlene", "Huff");
Person rui = new("Rui", "Raposo");

List<Person> people = new() { magnus, terry, charlotte, arlene, rui };

List<Pet> pets = new()
{
    new(Name: "Barley", Owner: terry),
    new("Boots", terry),
    new("Whiskers", charlotte),
    new("Blue Moon", rui),
    new("Daisy", magnus),
};

// Create a collection of person-pet pairs. Each element in the collection
// is an anonymous type containing both the person's name and their pet's name.
var query =
    from person in people
    join pet in pets on person equals pet.Owner
    select new
    {
        OwnerName = person.FirstName,
        PetName = pet.Name
    };

foreach (var ownerAndPet in query)
{
    Console.WriteLine($"\"{ownerAndPet.PetName}\" is owned by {ownerAndPet.OwnerName}");
}

/* Output:
     "Daisy" is owned by Magnus
     "Barley" is owned by Terry
     "Boots" is owned by Terry
     "Whiskers" is owned by Charlotte
     "Blue Moon" is owned by Rui
*/

请注意,LastName 是“Huff”的 Person 对象未出现在结果集中,因为没有 Pet 对象的 Pet.Owner 等于该 Person

示例 - 组合键联接

可以使用复合键基于多个属性来比较元素,而不是只基于一个属性使元素相关联。 为此,请为每个集合指定键选择器函数,以返回由要比较的属性组成的匿名类型。 如果对属性进行标记,则它们必须在每个键的匿名类型中具有相同标签。 属性还必须按相同顺序出现。

下面的示例使用 Employee 对象的列表和 Student 对象的列表来确定哪些雇员同时还是学生。 这两种类型都具有 String 类型的 FirstNameLastName 属性。 通过每个列表的元素创建联接键的函数会返回由每个元素的 FirstNameLastName 属性组成的匿名类型。 联接运算会比较这些复合键是否相等,并从每个列表返回名字和姓氏都匹配的对象对。

List<Employee> employees = new()
{
    new(FirstName: "Terry", LastName: "Adams", EmployeeID: 522459),
    new("Charlotte", "Weiss", 204467),
    new("Magnus", "Hedland", 866200),
    new("Vernette", "Price", 437139)
};

List<Student> students = new()
{
    new(FirstName: "Vernette", LastName: "Price", StudentID: 9562),
    new("Terry", "Earls", 9870),
    new("Terry", "Adams", 9913)
};

// Join the two data sources based on a composite key consisting of first and last name,
// to determine which employees are also students.
var query =
    from employee in employees
    join student in students on new
    {
        employee.FirstName,
        employee.LastName
    } equals new
    {
        student.FirstName,
        student.LastName
    }
    select employee.FirstName + " " + employee.LastName;

Console.WriteLine("The following people are both employees and students:");
foreach (string name in query)
{
    Console.WriteLine(name);
}

/* Output:
    The following people are both employees and students:
    Terry Adams
    Vernette Price
 */

示例 - 多联接

可以将任意数量的联接操作相互追加,以执行多联接。 C# 中的每个 join 子句会将指定数据源与上一个联接的结果相关联。

下面的示例创建三个集合:Person 对象的列表、Cat 对象的列表和 Dog 对象的列表。

C# 中的第一个 join 子句基于与 Cat.Owner 匹配的 Person 对象来匹配人和猫。 它返回包含 Person 对象和 Cat.Name 的匿名类型的序列。

C# 中的第二个 join 子句基于由 Owner 类型的 Person 属性和动物姓名的第一个字母组成的复合键,将第一个联接返回的匿名类型与提供的狗列表中的 Dog 对象相关联。 它返回包含来自每个匹配对的 Cat.NameDog.Name 属性的匿名类型的序列。 由于这是内部联接,因此只返回第一个数据源中在第二个数据源中具有匹配项的对象。

Person magnus = new(FirstName: "Magnus", LastName: "Hedlund");
Person terry = new("Terry", "Adams");
Person charlotte = new("Charlotte", "Weiss");
Person arlene = new("Arlene", "Huff");
Person rui = new("Rui", "Raposo");
Person phyllis = new("Phyllis", "Harris");

List<Person> people = new() { magnus, terry, charlotte, arlene, rui, phyllis };

List<Cat> cats = new()
{
    new(Name: "Barley", Owner: terry),
    new("Boots", terry),
    new("Whiskers", charlotte),
    new("Blue Moon", rui),
    new("Daisy", magnus),
};

List<Dog> dogs = new()
{
    new(Name: "Four Wheel Drive", Owner: phyllis),
    new("Duke", magnus),
    new("Denim", terry),
    new("Wiley", charlotte),
    new("Snoopy", rui),
    new("Snickers", arlene),
};

// The first join matches Person and Cat.Owner from the list of people and
// cats, based on a common Person. The second join matches dogs whose names start
// with the same letter as the cats that have the same owner.
var query =
    from person in people
    join cat in cats on person equals cat.Owner
    join dog in dogs on new
    {
        Owner = person,
        Letter = cat.Name.Substring(0, 1)
    } equals new
    {
        dog.Owner,
        Letter = dog.Name.Substring(0, 1)
    }
    select new
    {
        CatName = cat.Name,
        DogName = dog.Name
    };

foreach (var obj in query)
{
    Console.WriteLine(
        $"The cat \"{obj.CatName}\" shares a house, and the first letter of their name, with \"{obj.DogName}\"."
    );
}

/* Output:
     The cat "Daisy" shares a house, and the first letter of their name, with "Duke".
     The cat "Whiskers" shares a house, and the first letter of their name, with "Wiley".
 */

示例 - 使用分组联接的内联

下面的示例演示如何使用分组联接实现内部联接。

query1 中,Person 对象的列表会基于与 Pet.Owner 属性匹配的 Person,分组联接到 Pet 对象队列中。 分组联接会创建中间组的集合,其中每个组都包含 Person 对象和匹配 Pet 对象的序列。

通过向查询添加另一个 from 子句,此序列的序列会合并(或平展)为一个较长的序列。 最后一个序列的元素的类型由 select 子句指定。 在此示例中,该类型是由每个匹配对的 Person.FirstNamePet.Name 属性组成的匿名类型。

query1 的结果等效于通过使用 join 子句(不使用 into 子句)执行内部联接来获取的结果集。 query2 变量演示了此等效查询。

Person magnus = new(FirstName: "Magnus", LastName: "Hedlund");
Person terry = new("Terry", "Adams");
Person charlotte = new("Charlotte", "Weiss");
Person arlene = new("Arlene", "Huff");

List<Person> people = new() { magnus, terry, charlotte, arlene };

List<Pet> pets = new()
{
    new(Name: "Barley", Owner: terry),
    new("Boots", terry),
    new("Whiskers", charlotte),
    new("Blue Moon", terry),
    new("Daisy", magnus),
};

var query1 =
    from person in people
    join pet in pets on person equals pet.Owner into gj
    from subpet in gj
    select new
    {
        OwnerName = person.FirstName,
        PetName = subpet.Name
    };

Console.WriteLine("Inner join using GroupJoin():");
foreach (var v in query1)
{
    Console.WriteLine($"{v.OwnerName} - {v.PetName}");
}

var query2 =
    from person in people
    join pet in pets on person equals pet.Owner
    select new
    {
        OwnerName = person.FirstName,
        PetName = pet.Name
    };

Console.WriteLine();
Console.WriteLine("The equivalent operation using Join():");
foreach (var v in query2)
{
    Console.WriteLine($"{v.OwnerName} - {v.PetName}");
}

/* Output:
    Inner join using GroupJoin():
    Magnus - Daisy
    Terry - Barley
    Terry - Boots
    Terry - Blue Moon
    Charlotte - Whiskers

    The equivalent operation using Join():
    Magnus - Daisy
    Terry - Barley
    Terry - Boots
    Terry - Blue Moon
    Charlotte - Whiskers
*/

另请参阅