Wprowadzenie

W poprzednim wpisie skupiłem się na ogólnym omówienia scenariusza rozłączonego, tj. wykonywaniu operacji, które nie są śledzone przez DbContext. W tej części zagłębimy się nieco bardziej w szczegóły, przetestujemy różne dostępne metody oraz dodamy trochę danych do naszej bazy.

Na pierwszy ogień idzie metoda Add dostępna zarówno dla DbContext oraz DbSet. Pozwala ona na dodanie powiązanych danych do bazy danych. Samo użycie Add wiąże się z dodaniem encji do kontekstu oraz ustawieniem stanu na Added dla encji, której wartość klucza Id jest pusta lub została zdefiniowana jej domyślna wartość.

W naszym przykładzie będziemy bazować na relacji jeden do jednego omówionej w jednym z poprzednich wpisów:

public class Person
{
	public int Id { get; set; }

	public string FullName { get; set; }

	// referencyjna właściwość nawigacyjna
	public Passport Passport { get; set; }
}

public class Passport
{
	public int Id { get; set; }

	// Numer paszportu to zbiór liter i cyfr
	public string Number { get; set; }

	public string City { get; set; }

	public string Street { get; set; }

	// klucz obcy odpowiadający poniższej właściwości nawigacyjnej
	public int PassportNumberOfPersonId { get; set; }
		
	// referencyjna właściwość nawigacyjna
	public Person Person { get; set; }
}
Musicie pamiętać, że Entity Framework Core pozwala nam na dodawanie danych powiązanych w każdym innym typie relacji, nie jesteśmy tutaj oczywiście ograniczeni do relacji jeden do jednego – na takim typie relacji bazuje mój przykład:
// dodawanie paszportu odbywa się poza kontekstem
var passportDetails = new Passport()
{
	Number = "ABC12345678",
	City = "Gdynia",
	Street = "Nabrzeże Prezydenta"
};

// dodawanie konkretnej osoby odbywa się poza kontekstem
var person = new Person()
{
	FullName = "Paweł",
	// Zauważcie, że używam właściwości nawigacyjnej
	// w celu dodawania powiązanych danych
	Passport = passportDetails
};

using (var context = new ApplicationDbContext())
{
	// Dołączenie encji do kontekstu ze stanem 'Added'
	context.Attach<Person>(person);

	// Zapisanie zmian powoduje wykonanie polecania INSERT
	context.SaveChanges();
}

Wykonanie powyższego kodu powinno skutkować dodaniem dwóch połączonych w relacji jeden do jednego rekordów. Wykorzystajmy narzędzie SQL Server Profiler w celu sprawdzenia poleceń wykonanych przez EF Core:

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Person] ([FullName])
VALUES (@p0);
SELECT [Id]
FROM [Person]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
',N'@p0 nvarchar(4000)',@p0=N'Paweł'

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Passport] ([City], [Number], [PassportNumberOfPersonId], [Street])
VALUES (@p1, @p2, @p3, @p4);
SELECT [Id]
FROM [Passport]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p1 nvarchar(4000),@p2 nvarchar(4000),@p3 int,@p4 nvarchar(4000)',@p1=N'Gdynia',@p2=N'ABC12345678',@p3=1,@p4=N'Nabrzeże Prezydenta'

W ramach potwierdzenia poprawności działania powyższego kodu wykonajmy jeszcze instrukcję JOIN:

SELECT * FROM Passport as pp
INNER JOIN Person as p on pp.PassportNumberOfPersonId=p.Id
EFCore: JOIN

Dodawanie wielu rekordów

Tym razem wykorzystamy metodę AddRange w celu dodania wielu rekordów za jednym razem. Spójrzcie na poniższy przykład:

// lista nowych pracowników została utworzona poza kontekstem
var persons = new List<Person>()
{
	new Person()
	{
		FullName = "Rafał"
	},
	new Person()
	{
		FullName = "Krzysztof"
	}
};

using (var context = new ApplicationDbContext())
{
	// EntityState = Added
	context.AddRange(persons);
	context.SaveChanges();
}

Efektem wykonania powyższego kodu jest dodanie dwóch rekordów do tabeli Person.

Zanim przejdziemy dalej wykorzystamy listę obiektów. Dzięki takiemu podejściu będziemy w stanie dodać do bazy danych kilka rekordów do różnych tabel. Wszystkim zajmie się metoda AddRange, która w połączeniu z SaveChanges wykona całą pracę za nas:

// Definicja obiektów różnego typu
var p1 = new Person() { FullName = "Agnieszka" };
var p2 = new Person() { FullName = "Ola" };

var passport = new Passport()
{
	City = "Warszawa",
	Street = "Prosta",
	Number = "DEF654321",
	PassportNumberOfPersonId = 3
};

// Tworzymy listę obiektów
var entities = new List<object>()
{
	p1,
	p2,
	passport
};

using(var context = new ApplicationDbContext())
{
	// Metoda AddRange w połączeniu z SaveChanges zajmie się resztą
	context.AddRange(entities);
	context.SaveChanges();
}

Z naszego punktu widzenia najważniejszy jest fakt, że EF Core wykona instrukcje INSERT dla wszystkich powyższych rekordów. Jest jednak druga strona medalu. W naszym przykładzie nie wpływa ona na wydajność z uwagi na małą liczbę rekordów. W przypadku jednak 1000 lub większej liczby rekordów poniższe wykonanie będzie problematyczne z punktu widzenia wydajnościowego:

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Passport] ([City], [Number], [PassportNumberOfPersonId], [Street])
VALUES (@p0, @p1, @p2, @p3);
SELECT [Id]
FROM [Passport]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p0 nvarchar(4000),@p1 nvarchar(4000),@p2 int,@p3 nvarchar(4000)',@p0=N'Warszawa',@p1=N'DEF654321',@p2=3,@p3=N'Prosta'

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Person] ([FullName])
VALUES (@p0);
SELECT [Id]
FROM [Person]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p0 nvarchar(4000)',@p0=N'Ola'

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Person] ([FullName])
VALUES (@p0);
SELECT [Id]
FROM [Person]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p0 nvarchar(4000)',@p0=N'Agnieszka'

Wszystkiemu jest winna metoda SaveChanges - wymaga ona jednej rundy do bazy danych dla każdej dodawanej jednostki. W przypadku np. 100 000 wierszy będziemy musieli wykonać dokładnie tyle samo ‘podróży’ do bazy danych co będzie prowadziło do problemów wydajnościowych.

Rozwiązaniem jest przeprowadzenie operacji Bulk Insert, która ograniczna do minimum liczbę niezbędnych operacji wykonywanych na bazie danych. W naszym przypadku możemy wykorzystać paczkę Z.EntityFramework.Extensions.EFCore i nieznacznie zmodyfikować nasz kod. Wtyczka ta jest idealna do prostych zastosowań - w obrębie jednej tabeli:

// Definicja obiektów różnego typu
var p1 = new Person() { FullName = "Klaudia" };
var p2 = new Person() { FullName = "Jola" };

// Tworzymy listę obiektów
var entities = new List<object>()
{
	p1,
	p2
};

using(var context = new ApplicationDbContext())
{
	// Metoda AddRange w połączeniu z BulkInsert zajmie się resztą
	context.AddRange(entities);

	context.BulkInsert(entities);
	//context.SaveChanges();
}

Spójrzmy jeszcze na kod wygenerowany pod spodem:

exec sp_executesql N'MERGE INTO [Person]  AS DestinationTable
USING
(
SELECT TOP 100 PERCENT * FROM (SELECT @0_0 AS [Id], @0_1 AS [FullName], @0_2 AS ZZZ_Index
UNION ALL SELECT @1_0 AS [Id], @1_1 AS [FullName], @1_2 AS ZZZ_Index) AS StagingTable ORDER BY ZZZ_Index
) AS StagingTable
ON 1 = 2
WHEN NOT MATCHED THEN
    INSERT ( [FullName] )
    VALUES ( [FullName] )
OUTPUT
    $action,
    StagingTable.ZZZ_Index,
    INSERTED.[Id] AS [Id_zzzinserted]

;',N'@0_0 int,@0_1 nvarchar(max) ,@0_2 int,@1_0 int,@1_1 nvarchar(max) ,@1_2 int',@0_0=0,@0_1=N'Klaudia',@0_2=0,@1_0=0,@1_1=N'Jola',@1_2=1
Cała operacja została przeprowadzona w ramach jeden rundy dostępu do bazy danych.

Podsumowanie

W poprzednim wpisie powiedziałem, że operacje możemy wykonywać w obrębie kontekstu lub danej encji wykorzystując udostępione metody. Do tej pory stosowaliśmy zwykle zapis context.Add(...). Jeżeli chcemy być nieco bardziej precyzyjni i ułatwić sobie interpretację kodu możemy wykorzystać dostępne metody w ramach danej encji:

var p3 = new Person()
{
	FullName = "John"
};

using(var context = new ApplicationDbContext())
{
    // wskazujemy na konkretną encję w ramach kontekstu
	context.Person.Add(p3);

	context.SaveChanges();
}

W powyższym przykładzie operujemy dokładnie na encji Person więc jesteśmy również do niej ograniczeni – tylko taki typ będzie akceptowany. Samo działanie (pod spodem) jest identyczne: dołączamy encje Person do kontekstu ze stanem ustawionym na Added. Wywołanie metody SaveChanges skutkuje wywołaniem instrukcji INSERT.