Операции с множествами: объединение, пересечение, разность

Последнее обновление: 21.11.2021

Ряд методов Linq позволяют работать с результатами выборки как со множествами, производя операции на объединение, пересечение, разность двух выборок.

Но перед использованием данных методов надо учитывать, что они проводятся над однородными выборками с одинаковым определением строк, то есть которые совпадают по составу столбцов.

Для примеров возьмем модели из прошлых тем:

using Microsoft.EntityFrameworkCore;
public class Company
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public List<User> Users { get; set; } = new();
}

public class User
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public int Age { get; set; }
    public int CompanyId { get; set; }
    public Company? Company { get; set; }
}
public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!; 
    public DbSet<Company> Companies { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=helloapp.db");
    }
}

На примере следующих данных:

using (ApplicationContext db = new ApplicationContext())
{
	// пересоздаем базу данных
	db.Database.EnsureDeleted();
	db.Database.EnsureCreated();

	Company microsoft = new Company { Name = "Microsoft"};
	Company google = new Company { Name = "Google"};
	db.Companies.AddRange(microsoft, google);

	User tom = new User { Name = "Tom", Age = 36, Company = microsoft };
	User bob = new User { Name = "Bob", Age = 39, Company = google };
	User alice = new User { Name = "Alice", Age = 28, Company = microsoft };
	User kate = new User { Name = "Kate", Age = 25, Company = google };

	db.Users.AddRange(tom, bob, alice, kate);
	db.SaveChanges();
}

Объединение

Для объединения двух выборок используется метод Union():

using(ApplicationContext db = new ApplicationContext())
{
	var users = db.Users.Where(u => u.Age < 30)
        .Union(db.Users.Where(u=>u.Name!.Contains("Tom")));
    foreach (var user in users)
        Console.WriteLine(user.Name);
}

Консольный вывод:

Tom
Alice
Kate

Метод Union в качестве параметра принимает результаты второй выборки и объединяет ее с исходной. В результате мы получим два подзапроса SQL, результаты которых объединяются в общий набор:

SELECT "u"."Id", "u"."Age", "u"."CompanyId", "u"."Name"
FROM "Users" AS "u"
WHERE "u"."Age" < 30
UNION
SELECT "u0"."Id", "u0"."Age", "u0"."CompanyId", "u0"."Name"
FROM "Users" AS "u0"
WHERE ('Tom' = '') OR (instr("u0"."Name", 'Tom') > 0)

При этом мы не можем объединить две разнородные выборки, например, таблицу, пользователей и таблицу компаний. Однако уместна следующая запись:

var result = db.Users.Select(p => new { Name = p.Name })
    .Union(db.Companies.Select(c => new { Name = c.Name }));

Первая выборка после метода Select будет формировать набор элементов с одним столбцом Name. Вторая выборка из таблицы компаний после метода Select также будет формировать набор элементов с одним столбцом Name. Поэтому строки в обоих выборках будут однородны, и мы их сможем объединять.

Пересечение

Чтобы найти пересечение выборок, то есть те элементы, которые присутствуют сразу в двух выборках, используется метод Intersect(). Например, выберем все пользователей, у которых возраст больше 30 и в имени содержится подстрока "Tom":

using(ApplicationContext db = new ApplicationContext())
{
	var users = db.Users.Where(u => u.Age > 30)
        .Intersect(db.Users.Where(u=>u.Name!.Contains("Tom")));
    foreach (var user in users)
        Console.WriteLine(user.Name);
}

Консольный вывод:

Tom

Несмотря на то, что операция пересечения не равна операции объединения, рассмотренной выше, но при выполнении пересечение будет выполнять те же SQL-выражения:

SELECT "u"."Id", "u"."Age", "u"."CompanyId", "u"."Name"
FROM "Users" AS "u"
WHERE "u"."Age" > 30
INTERSECT
SELECT "u0"."Id", "u0"."Age", "u0"."CompanyId", "u0"."Name"
FROM "Users" AS "u0"
WHERE ('Tom' = '') OR (instr("u0"."Name", 'Tom') > 0)

Разность

Если нам надо найти элементы первой выборки, которые отсутствуют во второй выборке, то мы можем использовать метод Except:

using(ApplicationContext db = new ApplicationContext())
{
	var selector1 = db.Users.Where(u => u.Age > 30); // 
    var selector2 = db.Users.Where(u => u.Name!.Contains("Tom"));
    var users = selector1.Except(selector2);
	
    foreach (var user in users)
        Console.WriteLine(user.Name);
}
Bob

В результате будет формироваться следующий SQL-запрос:

SELECT "u"."Id", "u"."Age", "u"."CompanyId", "u"."Name"
FROM "Users" AS "u"
WHERE "u"."Age" > 30
EXCEPT
SELECT "u0"."Id", "u0"."Age", "u0"."CompanyId", "u0"."Name"
FROM "Users" AS "u0"
WHERE ('Tom' = '') OR (instr("u0"."Name", 'Tom') > 0)
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850