Migracja schematu bazy danych z DbUp

Wprowadzenie

Dwa ostatnie wpisy na blogu dotyczyły sposobu migracji schematu bazy danych. Migracje w Entity Framework oraz Fluent Migrator charakteryzują się tym, że cały proces migracji jest zapisany w klasach za pomocą api, które udostępnia dana biblioteka. Ilość sqla, którą piszemy w tych migracjach, jest minimalizowana i sprowadza się głównie do wykonywania jakiś specyficznych rzeczy – np. skopiowania danych z jednego miejsca w drugie. Natomiast dzisiejsza biblioteka (DbUp – https://dbup.github.io/) działa zupełnie inaczej. Pomaga ona wykonywać migracje, które są zapisane w plikach sql. Dlatego właśnie DbUp jest moim pierwszym wyborem w sytuacji, gdy w projekcie migracje są zdefiniowane w skryptach.

DbUp

Podobnie jak w przypadku Entity Framework oraz Fluent Migrator, uruchamianie migracji organizuję w dedykowaną aplikację konsolową. Dlatego w przykładzie na githubie (https://github.com/danielplawgo/DbUpExample) znajduje się tylko aplikacja konsolowa. Aby skorzystać z DbUp, należy zainstalować pakiet https://www.nuget.org/packages/dbup z nugeta. W przykładzie dodałem również pakiety dla CommandLineParsera oraz NLoga, aby móc przekazać do aplikacji connection stringa oraz logować działanie migratora – podobnie jak to było w poprzednich przykładach.

Jak wspomniałem wyżej, wszystkie migracje będą zapisane w plikach sql. Tworzę dla nich w projekcie dedykowany katalog. Również tutaj nazwy plików migracji zaczynają się od daty dodania migracji – aby później można było łatwiej analizować to, co dzieje się z schematem bazy na przestrzeni czasu:

dbup solution explorer

Domyślnie DbUp wymaga, aby pliki sql z migracjami miały ustawiony Build Action na Embedded Resource w ustawieniach pliku:

dbup file properties

Jest to również jeden z częstych problemów z nieuruchamiającymi się migracjami w DbUp. Domyślnie nowy plik sql ma ustawiony Build Action na Content i trzeba pamiętać, aby zmienić to ręcznie.

Pierwsza migracja

W testowym projekcie posłużymy się tym samym przykładem, co w poprzednich dwóch wpisach. Dzięki temu będziesz mógł porównać sposoby działania wszystkich trzech bibliotek. Dlatego w przykładzie pierwsza migracja dodaje do bazy danych tabelę Products z trzema kolumnami oraz kilka rekordów:

CREATE TABLE [dbo].[Products] (
[Id] INT NOT NULL IDENTITY(1,1),
[Name] NVARCHAR(255) NOT NULL,
[Category] NVARCHAR(255) NOT NULL,
CONSTRAINT [PK_Products] PRIMARY KEY ([Id])
)
INSERT INTO [dbo].[Products] ([Name], [Category]) VALUES (N'Product 1.1', N'Category 1')
INSERT INTO [dbo].[Products] ([Name], [Category]) VALUES (N'Product 1.2', N'Category 1')
INSERT INTO [dbo].[Products] ([Name], [Category]) VALUES (N'Product 1.3', N'Category 1')
INSERT INTO [dbo].[Products] ([Name], [Category]) VALUES (N'Product 2.1', N'Category 2')
INSERT INTO [dbo].[Products] ([Name], [Category]) VALUES (N'Product 2.2', N'Category 2')
view raw AddProducts.sql hosted with ❤ by GitHub

Jak widać, w tym przypadku całą zmianę schematu bazy danych mamy zapisaną w czystym sqlu. W C# zapiszemy jedynie sposób uruchamiania aplikacji, natomiast wszystko inne będzie w sqlu.

Uruchomienie migracji

Samo uruchomienie migracji nie jest mocno skomplikowane, jest to kilkanaście linii kodu. Podobnie jak inne biblioteki tego typu, DbUp korzysta z specjalnej tabeli, w której zapisuje informacje o wykonanych skryptach. Podczas uruchamiania sprawdza, jakie migracje są dostępne w aplikacji, i uruchamia tylko te, które nie zostały wcześniej uruchomione.

Warto pamiętać o tym, że DbUp rozpoznaje skrypty po ścieżce w projekcie oraz nazwie pliku. Na przykład pierwsza migracja będzie rozpoznana jako „DbUpExample.Migrator.Migrations.201810100538_AddProducts.sql”. Przez to musimy uważać z przenoszeniem migracji do innego folderu, ponieważ DbUp może rozpoznać to jako zupełnie inną migrację.

Tak jak w poprzednich przykładach z migracjami, tak i w tym wykorzystałem klasę Options, która służy do parsowania parametrów za pomocą biblioteki CommandLineParser przekazywanych do aplikacji. Klasa Options zawiera tylko jeden parametr dla connection stringa:

public class Options
{
[Option('c', "connectionString", Required = true, HelpText = "The connection string to database that needs to be updated.")]
public string ConnectionString { get; set; }
}
view raw Options.cs hosted with ❤ by GitHub

Natomiast kod potrzebny do uruchomienia migracji wygląda tak:

class Program
{
static void Main(string[] args)
{
var result = Parser.Default.ParseArguments<Options>(args);
result
.WithParsed(r => Migrate(r));
}
private static void Migrate(Options options)
{
var migrator =
DeployChanges.To
.SqlDatabase(options.ConnectionString)
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), s => Filter(s))
.LogToAutodetectedLog()
.Build();
var result = migrator.PerformUpgrade();
if (result.Successful)
{
Console.WriteLine("Success!");
}
else
{
Console.WriteLine(result.Error);
}
}
private static bool Filter(string script)
{
return script.StartsWith("DbUpExample.Migrator.Migrations");
}
}
view raw Program.cs hosted with ❤ by GitHub

Jak widać, po sparsowaniu parametrów z wiersza poleceń w metodzie Migrate konfigurowany jest migrator z DbUp. Określamy w nim przede wszystkich connection stringa do bazy, którą chcemy zaktualizować. Do tego korzystamy z dostępnych providerów (wywołanie metody WithScriptsEmbeddedInAssembly), którzy określają, skąd mają zostać załadowane pliki sql. W przykładzie są to dołączone pliki sql do projektu. DbUp udostępnia również inne sposoby ładowania plików – https://dbup.readthedocs.io/en/latest/more-info/script-providers/

DbUp wpiera też różne biblioteki do logowania działania aplikacji, takie jak nlog, log4net. Konfiguruje się je za pomocą metody LogToAutodetectedLog, która pod spodem korzysta z LibLog – https://github.com/damianh/LibLog

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 30 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Druga migracja

Druga migracja, którą dodałem do projektu, jest odpowiedzialna za utworzenie tabeli Categories, skopiowanie do niej nazwy kategorii oraz ustawienie CategoryId w tabeli Products. Sam sql migracji wygląda tak:

CREATE TABLE [dbo].[Categories] (
[Id] INT NOT NULL IDENTITY(1,1),
[Name] NVARCHAR(255) NOT NULL,
CONSTRAINT [PK_Categories] PRIMARY KEY ([Id])
)
INSERT INTO dbo.Categories SELECT DISTINCT Category FROM dbo.Products;
ALTER TABLE [dbo].[Products] ADD [CategoryId] INT
GO
UPDATE p SET p.CategoryId = (SELECT c.Id FROM dbo.Categories c WHERE c.Name = p.Category) FROM dbo.Products p;
ALTER TABLE [dbo].[Products] ALTER COLUMN [CategoryId] INT NOT NULL;
ALTER TABLE [dbo].[Products] ADD CONSTRAINT [FK_Products_CategoryId_Categories_Id] FOREIGN KEY ([CategoryId]) REFERENCES [dbo].[Categories] ([Id]);
CREATE INDEX [IX_Products_CategoryId] ON [dbo].[Products] ([CategoryId] ASC);
DECLARE @default sysname, @sql nvarchar(max);
SELECT @default = name
FROM sys.default_constraints
WHERE parent_object_id = object_id('[dbo].[Products]')
AND type = 'D'
AND parent_column_id = (
SELECT column_id
FROM sys.columns
WHERE object_id = object_id('[dbo].[Products]')
AND name = 'Category'
);
SET @sql = N'ALTER TABLE [dbo].[Products] DROP CONSTRAINT ' + QUOTENAME(@default);
EXEC sp_executesql @sql;
ALTER TABLE [dbo].[Products] DROP COLUMN [Category];
view raw AddCategories.sql hosted with ❤ by GitHub

Wynik działania migratora

Poniżej znajduje się wynik działania migratora, który został uruchomiony dla pustej, dopiero co utworzonej bazy danych. Widać, że migrator wykrył, iż żadna migracja nie została wykonana, i wykonał obie migracje, które są dodane do aplikacji.

2018-10-12 05:37:59.1027 INFO Beginning database upgrade
2018-10-12 05:37:59.2127 INFO Checking whether journal table exists..
2018-10-12 05:37:59.2618 INFO Journal table does not exist
2018-10-12 05:37:59.2888 INFO Executing Database Server script 'DbUpExample.Migrator.Migrations.201810100538_AddProducts.sql'
2018-10-12 05:37:59.3031 INFO Checking whether journal table exists..
2018-10-12 05:37:59.3140 INFO Creating the [SchemaVersions] table
2018-10-12 05:37:59.3327 INFO The [SchemaVersions] table has been created
2018-10-12 05:37:59.4007 INFO Executing Database Server script 'DbUpExample.Migrator.Migrations.201810100553_AddCategories.sql'
2018-10-12 05:37:59.5687 INFO Upgrade successful
view raw log.txt hosted with ❤ by GitHub

Filtrowanie migracji

Na koniec warto jeszcze wspomnieć o ciekawej funkcjonalności, którą daje DbUp i której używałem w przykładzie. DbUp, szukając migracji do wykonania, umożliwia również filtrowanie znalezionych migracji. Robi się to poprzez przekazanie metody filtrującej (przyjmującej stringa z pełną nazwą migracji i zwracającą true lub false) do metody WithScriptsEmbeddedInAssembly, jak to zrobiłem w powyższym przykładzie.

Dzięki temu możemy używać jednej aplikacji do aktualizowania schematu różnych baz danych. Wystarczy na przykład dla każdego typu bazy danych utworzyć dedykowany katalog i do niego wrzucać skrypty. Później w metodzie filtrującej możemy na podstawie jakiegoś parametru przekazanego do aplikacji decydować, z którego katalogu będą się ładować skrypty. Tak jak w przykładzie – korzystając z metody StartWith klasy string.

Innym przykładem użycia takiego podejścia może być dodanie folderów z różnymi danymi testowymi, które również będzie filtrować metoda na podstawie parametrów przekazanych do aplikacji. Filtrowanie działa również w przypadku innych providerów.

Przykład

Tradycyjnie na githubie (https://github.com/danielplawgo/DbUpExample) znajduje się przykład, którego użyłem podczas przygotowywania tego wpisu. Podobnie jak w przypadku pozostałych przykładów z migracjami schematu bazy danych, tak i w tym, aby uruchomić przykład, musisz w ustawieniach projektu ustawić poprawny connection string do bazy, na której mają wykonać się migracje.

Zachęcam do pobawienia się przykładowym projektem.

Podsumowanie

DbUp jest kolejną biblioteką, której możesz użyć do zarządzania schematem baz danych. Jak widać na przykładzie, biblioteka przydaje się głównie wtedy, gdy planujemy w projekcie używać migracji zapisanych w czystym sqlu. Dlatego w pierwszej kolejności to ją proponuję zespołom, które używają plików sql do zmiany schematu bazy danych, a nie korzystają jeszcze z żadnego narzędzia do automatyzowania uruchamia migracji i wszystko robią ręcznie.

Migracje z Entity Framework lub Fluent Migratora używam w sytuacji, gdy decydujemy się na tworzenie migracji w kodzie za pomocą dostępnego api, a nie w czystym sqlu.

Używałeś DbUp? Jakie są Twoje doświadczenia z tą biblioteką? A może używasz innego narzędzia, które ma podobne funkcje jak DbUp? Proszę, podziel się tym w komentarzu. 🙂

1 thought on “Migracja schematu bazy danych z DbUp

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *