Migracje Code First

Migracje Code First to zalecany sposób rozwoju schematu bazy danych aplikacji, jeśli używasz przepływu pracy Code First. Migracje zapewniają zestaw narzędzi, które umożliwiają wykonywanie następujących czynności:

  1. Utworzenie początkowej bazy danych, która współdziała z modelem EF
  2. Generowanie migracji w celu śledzenia zmian w modelu EF
  3. Aktualizowanie bazy danych na bieżąco z tymi zmianami

Poniższy przewodnik zawiera omówienie migracji Code First na platformie Entity Framework. Możesz ukończyć cały przewodnik lub przejść do interesującego Cię tematu. Omówione są następujące tematy:

Tworzenie początkowego modelu i bazy danych

Przed rozpoczęciem korzystania z migracji potrzebujemy projektu i modelu Code First. W tym przewodniku użyjemy kanonicznego modelu Blog i Post.

  • Tworzenie nowej aplikacji konsolowej MigrationsDemo
  • Dodawanie najnowszej wersji pakietu NuGet EntityFramework do projektu
    • Narzędzia —> Menedżer pakietów biblioteki —> konsola menedżera pakietów
    • Uruchom polecenie Install-Package EntityFramework
  • Dodaj plik Model.cs z poniższym kodem. Ten kod definiuje pojedynczą klasę Blog, która jest naszym modelem domeny, i klasę BlogContext, która jest naszym kontekstem EF Code First
    using System.Data.Entity;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Data.Entity.Infrastructure;

    namespace MigrationsDemo
    {
        public class BlogContext : DbContext
        {
            public DbSet<Blog> Blogs { get; set; }
        }

        public class Blog
        {
            public int BlogId { get; set; }
            public string Name { get; set; }
        }
    }
  • Teraz, gdy mamy model, nadszedł czas, aby użyć go do uzyskania dostępu do danych. Zaktualizuj plik Program.cs przy użyciu poniższego kodu.
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;

    namespace MigrationsDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                using (var db = new BlogContext())
                {
                    db.Blogs.Add(new Blog { Name = "Another Blog " });
                    db.SaveChanges();

                    foreach (var blog in db.Blogs)
                    {
                        Console.WriteLine(blog.Name);
                    }
                }

                Console.WriteLine("Press any key to exit...");
                Console.ReadKey();
            }
        }
    }
  • Uruchom aplikację, a zobaczysz, że utworzono bazę danych MigrationsCodeDemo.BlogContext.

    Database LocalDB

Włączanie migracji

Nadszedł czas, aby wprowadzić więcej zmian w naszym modelu.

  • Wprowadźmy właściwość Url do klasy Blog.
    public string Url { get; set; }

Jeśli ponownie uruchomisz aplikację, zostanie wyświetlony wyjątek InvalidOperationException z informacją, że Model tworzący kontekst „BlogContext” zmienił się od czasu utworzenia bazy danych. Rozważ użycie migracji Code First do zaktualizowania bazy danych (http://go.microsoft.com/fwlink/?LinkId=238269).

Jak sugeruje wyjątek, nadszedł czas, aby rozpocząć używanie migracji Code First. Pierwszym krokiem jest włączenie migracji dla naszego kontekstu.

  • Uruchom polecenie Enable-Migrations w konsoli menedżera pakietów

    To polecenie spowodowało dodanie folderu Migrations do naszego projektu. Ten nowy folder zawiera dwa pliki:

  • Klasa Configuration. Ta klasa umożliwia skonfigurowanie sposobu działania migracji dla kontekstu. W tym przewodniku po prostu użyjemy konfiguracji domyślnej. Ponieważ w projekcie istnieje tylko jeden kontekst Code First, polecenie Enable-Migrations automatycznie wypełniło typ kontekstu, do którego ta konfiguracja ma zastosowanie.

  • Migracja InitialCreate. Ta migracja została wygenerowana, ponieważ funkcja Code First już utworzyła dla nas bazę danych, zanim włączyliśmy migracje. Kod w tej migracji szkieletowej reprezentuje obiekty, które zostały już utworzone w bazie danych. W naszym przypadku jest to tabela Blog z kolumnami BlogId i Name. Nazwa pliku zawiera znacznik czasu, który ułatwia porządkowanie. Gdyby baza danych nie została jeszcze utworzona, ta migracja InitialCreate nie zostałaby dodana do projektu. Zamiast tego przy pierwszym wywołaniu metody Add-Migration kod umożliwiający utworzenie tych tabel zostałby dodany do nowej migracji.

Wiele modeli przeznaczonych dla tej samej bazy danych

W przypadku korzystania z wersji wcześniejszych niż EF6 tylko jeden model Code First mógł posłużyć do wygenerowania schematu bazy danych i zarządzania nim. Jest to wynik pojedynczej tabeli __MigrationsHistory na bazę danych bez możliwości określenia, które wpisy należą do którego modelu.

Począwszy od wersji EF6, klasa Configuration zawiera właściwość ContextKey. Działa to jako unikatowy identyfikator dla każdego modelu Code First. Odpowiadająca kolumna w tabeli __MigrationsHistory pozwala na współdzielenie tabeli przez wpisy z wielu modeli. Domyślnie ta właściwość jest ustawiana na w pełni kwalifikowaną nazwę kontekstu.

Generowanie i uruchamianie migracji

Migracje Code First mają dwa podstawowe polecenia, z którymi się zapoznasz.

  • Add-Migration utworzy szkielet następnej migracji na podstawie zmian wprowadzonych w modelu od czasu utworzenia ostatniej migracji
  • Update-Database zastosuje wszelkie oczekujące migracje do bazy danych

Musimy utworzyć szkielet migracji, aby zająć się nową właściwością Url, którą dodaliśmy. Polecenie Add-Migration umożliwia nadanie tej migracji nazwy. Nadajmy jej nazwę AddBlogUrl.

  • Uruchom polecenie Add-Migration AddBlogUrl w konsoli menedżera pakietów
  • W folderze Migrations mamy teraz nową migrację AddBlogUrl. Aby ułatwić porządkowanie, nazwa pliku migracji ma prefiks w postaci znacznika czasu
    namespace MigrationsDemo.Migrations
    {
        using System;
        using System.Data.Entity.Migrations;

        public partial class AddBlogUrl : DbMigration
        {
            public override void Up()
            {
                AddColumn("dbo.Blogs", "Url", c => c.String());
            }

            public override void Down()
            {
                DropColumn("dbo.Blogs", "Url");
            }
        }
    }

Możemy teraz edytować tę migrację, ale wszystko wygląda całkiem dobrze. Użyjmy polecenia Update-Database, aby zastosować tę migrację do bazy danych.

  • Uruchom polecenie Update-Database w konsoli menedżera pakietów
  • Narzędzie Migracje Code First porówna migracje w folderze Migrations z tymi, które zostały zastosowane do bazy danych. Zobaczy, że należy zastosować migrację AddBlogUrl, i uruchomi ją.

Baza danych MigrationsDemo.BlogContext została zaktualizowana w celu uwzględnienia kolumny Url w tabeli Blogs.

Dostosowywanie migracji

Do tej pory wygenerowaliśmy i uruchomiliśmy migrację bez wprowadzania żadnych zmian. Teraz przyjrzyjmy się edytowaniu kodu, który jest domyślnie generowany.

  • Nadszedł czas, aby wprowadzić więcej zmian w naszym modelu. Dodajmy nową właściwość Rating do klasy Blog
    public int Rating { get; set; }
  • Dodajmy również nową klasę Post
    public class Post
    {
        public int PostId { get; set; }
        [MaxLength(200)]
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
  • Dodamy również kolekcję Posts do klasy Blog, aby utworzyć drugi koniec relacji między klasami Blog a Post
    public virtual List<Post> Posts { get; set; }

Użyjemy polecenia Add-Migration, aby umożliwić narzędziu Migracje Code First określenie najlepszej migracji. Nadamy tej migracji nazwę AddPostClass.

  • Uruchom polecenie Add-Migration AddPostClass w konsoli menedżera pakietów.

Narzędzie Migracje Code First całkiem dobrze poradziło sobie z utworzeniem szkieletu tych zmian, ale warto jeszcze zmienić kilka rzeczy:

  1. Najpierw dodajmy unikatowy indeks do kolumny Posts.Title (Dodanie w wierszu 22 i 29 w poniższym kodzie).
  2. Dodajmy również niedopuszczającą wartości null kolumnę Blogs.Rating. Jeśli w tabeli znajdują się jakiekolwiek dane, zostanie przypisana domyślna wartość CLR typu danych dla nowej kolumny (ocena to liczba całkowita, więc będzie to 0). Chcemy jednak określić wartość domyślną 3, aby istniejące wiersze w tabeli Blogs zaczynały się od przyzwoitej oceny. (Wartość domyślna jest określona w wierszu 24 poniższego kodu)
    namespace MigrationsDemo.Migrations
    {
        using System;
        using System.Data.Entity.Migrations;

        public partial class AddPostClass : DbMigration
        {
            public override void Up()
            {
                CreateTable(
                    "dbo.Posts",
                    c => new
                        {
                            PostId = c.Int(nullable: false, identity: true),
                            Title = c.String(maxLength: 200),
                            Content = c.String(),
                            BlogId = c.Int(nullable: false),
                        })
                    .PrimaryKey(t => t.PostId)
                    .ForeignKey("dbo.Blogs", t => t.BlogId, cascadeDelete: true)
                    .Index(t => t.BlogId)
                    .Index(p => p.Title, unique: true);

                AddColumn("dbo.Blogs", "Rating", c => c.Int(nullable: false, defaultValue: 3));
            }

            public override void Down()
            {
                DropIndex("dbo.Posts", new[] { "Title" });
                DropIndex("dbo.Posts", new[] { "BlogId" });
                DropForeignKey("dbo.Posts", "BlogId", "dbo.Blogs");
                DropColumn("dbo.Blogs", "Rating");
                DropTable("dbo.Posts");
            }
        }
    }

Nasza edytowana migracja jest gotowa do użycia, więc użyjemy polecenia Update-Database, aby zaktualizować bazę danych. Tym razem określimy flagę –Verbose, aby zobaczyć, jak działa kod SQL uruchamiany przez narzędzie Migracje Code First.

  • Uruchom polecenie Update-Database –Verbose w konsoli menedżera pakietów.

Ruch danych / niestandardowy kod SQL

Do tej pory przyjrzeliśmy się operacjom migracji, które nie zmieniają ani nie przenoszą żadnych danych. Teraz przyjrzyjmy się czemuś, co wymaga przeniesienia danych. Nie ma jeszcze natywnej obsługi ruchu danych, ale możemy uruchomić kilka różnych poleceń SQL w dowolnym punkcie naszego skryptu.

  • Dodajmy do modelu właściwość Post.Abstract. Później wstępnie wypełnimy kolumnę Abstract dla istniejących wpisów przy użyciu tekstu z początku kolumny Content.
    public string Abstract { get; set; }

Użyjemy polecenia Add-Migration, aby umożliwić narzędziu Migracje Code First określenie najlepszej migracji.

  • Uruchom polecenie Add-Migration AddPostAbstract w konsoli menedżera pakietów.
  • Wygenerowana migracja zajmuje się zmianami schematu, ale chcemy również wstępnie wypełnić kolumnę Abstract przy użyciu pierwszych 100 znaków zawartości dla każdego wpisu. Możemy to zrobić, schodząc do języka SQL i uruchamiając instrukcję UPDATE po dodaniu kolumny. (Dodanie w wierszu 12 w poniższym kodzie)
    namespace MigrationsDemo.Migrations
    {
        using System;
        using System.Data.Entity.Migrations;

        public partial class AddPostAbstract : DbMigration
        {
            public override void Up()
            {
                AddColumn("dbo.Posts", "Abstract", c => c.String());

                Sql("UPDATE dbo.Posts SET Abstract = LEFT(Content, 100) WHERE Abstract IS NULL");
            }

            public override void Down()
            {
                DropColumn("dbo.Posts", "Abstract");
            }
        }
    }

Nasza edytowana migracja wygląda dobrze, więc użyjemy polecenia Update-Database, aby zaktualizować bazę danych. Określimy flagę –Verbose, aby zobaczyć kod SQL uruchamiany względem bazy danych.

  • Uruchom polecenie Update-Database –Verbose w konsoli menedżera pakietów.

Migrowanie do określonej wersji (w tym do starszej)

Do tej pory zawsze przeprowadzaliśmy uaktualnienie do najnowszej migracji, ale mogą wystąpić sytuacje, w których trzeba uaktualnić migrację lub zmienić ją na starszą do określonej wersji.

Załóżmy, że chcemy przeprowadzić migrację bazy danych do stanu, w którym znajdowała się po uruchomieniu migracji addBlogUrl. Możemy użyć przełącznika –TargetMigration, aby obniżyć poziom do tej migracji.

  • Uruchom polecenie Update-Database –TargetMigration: AddBlogUrl w konsoli menedżera pakietów.

To polecenie spowoduje uruchomienie skryptu Down dla migracji AddBlogAbstract i AddPostClass.

Jeśli chcesz wrócić do pustej bazy danych, możesz użyć polecenia Update-Database –TargetMigration: $InitialDatabase.

Pobieranie skryptu SQL

Jeśli inny deweloper chce tych zmian na swojej maszynie, może po prostu przeprowadzić synchronizację po sprawdzeniu zmian w kontroli źródła. Po utworzeniu nowych migracji można po prostu uruchomić polecenie Update-Database, aby zmiany zostały zastosowane lokalnie. Jeśli jednak chcemy wypchnąć te zmiany na serwer testowy i ostatecznie produkcyjny, prawdopodobnie będziemy potrzebować skryptu, który można przekazać administratorowi bazy danych.

  • Uruchom polecenie Update-Database, ale tym razem określ flagę —Script, aby zmiany zostały zapisane w skrypcie, a nie zastosowane. Określimy również migrację źródłową i docelową w celu wygenerowania skryptu. Chcemy, aby skrypt przechodził z pustej bazy danych ($InitialDatabase) do najnowszej wersji (migracja addPostAbstract). Jeśli nie określisz migracji docelowej, narzędzie Migracje będzie używać najnowszej migracji jako docelowej. Jeśli nie określisz migracji źródłowej, narzędzie Migracje będzie używać bieżącego stanu bazy danych.
  • Uruchom polecenie Update-Database -Script -SourceMigration: $InitialDatabase -TargetMigration: AddPostAbstract w konsoli menedżera pakietów

Narzędzie Migracje Code First uruchomi potok migracji, ale zamiast stosować zmiany, zostaną one zapisane w pliku sql. Po wygenerowaniu skryptu zostanie on otwarty w programie Visual Studio i będzie gotowy do wyświetlenia lub zapisania.

Generowanie skryptów idempotentnych

Począwszy od platformy EF6, jeśli określisz wartość –SourceMigration $InitialDatabase, wygenerowany skrypt będzie „idempotentny”. Skrypty idempotentne mogą uaktualnić bazę danych będącą obecnie w dowolnej wersji do najnowszej wersji (lub określonej wersji, jeśli użyjesz parametru –TargetMigration). Wygenerowany skrypt zawiera logikę sprawdzania tabeli __MigrationsHistory i stosuje tylko te zmiany, które nie zostały wcześniej zastosowane.

Automatyczne uaktualnianie podczas uruchamiania aplikacji (inicjator MigrateDatabaseToLatestVersion)

Jeśli wdrażasz aplikację, możesz chcieć, aby po uruchomieniu automatycznie uaktualniała bazę danych (stosując wszelkie oczekujące migracje). Można to zrobić, rejestrując inicjator bazy danych MigrateDatabaseToLatestVersion. Inicjator bazy danych zawiera po prostu pewną logikę, która służy do upewnienia się, że baza danych jest poprawnie skonfigurowana. Ta logika jest uruchamiana podczas pierwszego użycia kontekstu w procesie aplikacji (AppDomain).

Możemy zaktualizować plik Program.cs, jak pokazano poniżej, aby ustawić inicjator MigrateDatabaseToLatestVersion dla elementu BlogContext przed użyciem kontekstu (wiersz 14). Należy również dodać instrukcję using dla przestrzeni nazw System.Data.Entity (wiersz 5).

Podczas tworzenia wystąpienia tego inicjatora musimy określić typ kontekstu (BlogContext) i konfigurację migracji (Configuration) — konfiguracja migracji jest klasą, która została dodana do folderu Migrations po włączeniu narzędzia Migracje.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Data.Entity;
    using MigrationsDemo.Migrations;

    namespace MigrationsDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                Database.SetInitializer(new MigrateDatabaseToLatestVersion<BlogContext, Configuration>());

                using (var db = new BlogContext())
                {
                    db.Blogs.Add(new Blog { Name = "Another Blog " });
                    db.SaveChanges();

                    foreach (var blog in db.Blogs)
                    {
                        Console.WriteLine(blog.Name);
                    }
                }

                Console.WriteLine("Press any key to exit...");
                Console.ReadKey();
            }
        }
    }

Teraz za każdym razem, gdy nasza aplikacja zostanie uruchomiona, najpierw sprawdzi, czy jej docelowa baza danych jest aktualna, a jeśli nie jest, zastosuje wszelkie oczekujące migracje.