Użyj migracji. Mówili. Będzie fajnie. Mówili. To proste. Mówili.

Niedawno zabrałem się za coś co jest dla mnie zupełnie nowe. Migracje i podejście code-first w Entity Framework. Zawsze z jakiegoś dziwnego powodu wydawało mi się to skomplikowane, czasochłonne i niezbyt potrzebne. W jak wielkim byłem błędzie opiszę już dzisiaj.

 Ostatnio rozmawiałem z kolegą, któremu powiedziałem, że chciałbym spróbować podejścia code-first i odpalić migracje w swoim projekcie. Powiedział mi, że to jest banalnie proste. Wystarczy wpisać to i tamto i wszystko się samo spokojnie zrobi. W naszej rozmowie pojawiła się tylko jedna niewielka niewiadoma. Jak zachowają się migracje kiedy mam dwa konteksty w jednym projekcie? Stwierdziliśmy, że nie powinno być z tym żadnych problemów, ale mi coś jednak podpowiadało, że może nie pójść tak łatwo jak się wydaje.

Wróciłem do domu i przystąpiłem do lektury. Wg. internetów powinienem otworzyć managera pakietów i wpisać kolejno:

  • Enable-Migrations
  • Add-Migration NazwaMigracji
  • Update-Database

W najprostszej konfiguracji tak to powinno działać i już powinienem mieć zaktualizowaną bazę.

Oczywiście tak się nie wydarzyło. Już po pierwszej komendzie otrzymałem komunikat:

More than one context type was found in the assembly ‚SocialCooking.Domain’.
To enable migrations for ‚SocialCooking.Domain.Context.AuthContext’, use Enable-Migrations -ContextTypeName SocialCooking.Domain.Context.AuthContext.
To enable migrations for ‚SocialCooking.Domain.Context.SCContext’, use Enable-Migrations -ContextTypeName SocialCooking.Domain.Context.SCContext.

Ok to w sumie nie problem. Ponowiłem próbę i wpisałem tym razem:

Enable-Migrations -ContextTypeName SocialCooking.Domain.Context.SCContext

Poszło. Ufff. Pierwszy krok za mną. Dalej lecimy z Add-Migration Initial. O dziwo zadziałało. Na koniec zadowolony piszę Update-Database, skrypt się wykonał. ALE…

No właśnie patrzę na bazę, a tam żadnych zmian. Odświeżam wszystko i już wszystko jasne. Skrypt się wykonał, ale stworzył nową bazę o nazwie SocialCooking.Domain.Context

Wracam do przeszukiwania Internetu w poszukiwaniu natchnienia co zrobić. Jak to zwykle się u mnie dzieje próbuję kilku rzeczy nie zawsze wczytując się w to co dokładnie piszą. Próbowałem w związku z tym wyrzucić katalog Migrations, zrobić jeszcze raz Add tyle, że tym razem jako Add-Migration Initial -IgnoreChanges. Nie czytałem co zostało wygenerowane. Jeszcze w kontekście coś dopisałem i nic nie działało. Ehhhhh.

Powoli. Zacznijmy od początku.

Skoro moje szybkie „fixy” nie działają, trzeba będzie chyba jednak wczytać się dokładnie w instrukcje. Pierwsze co udało mi się znaleźć to to, że faktycznie trzeba wyrzucić wszystkie śmieci, które zostały wygenerowane. Katalog Migrations znowu ląduje w koszu. Teraz Kontekst, który początkowo wyglądał tak:

public class SCContext: DbContext
    {
        public DbSet<DishIngredient> Ingredients { get; set; }
        public DbSet<DishDifficultyLevel> DifficultyLevels { get; set; }
        public DbSet<DishCategory> Categories { get; set; }
        public DbSet<Dish> Dishes { get; set; }
        public DbSet<AspNetUsers> AspNetUsers { get; set; }
        public DbSet<UsersExtension> UsersExtension { get; set; }
    }

Do użycia Entity Framework i połączenia z bazą danych to wystarcza. Jednak jeżeli chcę użyć migracji i zapisywać zmiany we wskazanej przeze mnie bazie danych muszę dodać konstruktor:

public SCContext() 
            : base("SCContext")
        {
        }

Konstruktor ten jawnie definiuje z jakiego ConnectionStringa EF ma korzystać.

Właściwie to powinno wystarczyć. Jeszcze raz wracamy do początku migracji:

  • Enable-Migrations -ContextTypeName SocialCooking.Domain.Context.SCContext -Force

Nadpisuję wszystko co zrobiłem do tej pory dzięki parametrowi -Force oczywiście.

  • Add-Migration Initial

Tym razem już się przyjrzałem temu co zostało wygenerowane i niestety nadal czekała mnie niemiła niespodzianka:

public partial class Initial : DbMigration
    {
        public override void Up()
        {
        }
        
        public override void Down()
        {
        }
    }

Migracje nie wykryły żadnych zmian… Powoli ręce zaczynały mi opadać. „Użyj migracji. Mówili. Będzie fajnie. Mówili. To proste. Mówili.” Już miałem odpuścić, aż tu nagle gdzieś StackOverflow, ktoś podpowiedział, żeby spojrzeć w tabelkę _MigrationHistory. I co? BINGO… W moich poprzednich próbach chaotycznego naprawiania kodu powstało tutaj kilka śmieci, które blokowały moje zmiany. Wyczyściłem całą tabelkę i jeszcze raz wywołałem Add-Migration SCContext. W końcu moim oczom ukazał się piękny kod:

public override void Up()
        {
            
            CreateTable(
                "dbo.UsersExtensions",
                c => new
                    {
                        Id = c.Guid(nullable: false),
                        UsersId = c.String(maxLength: 128),
                        ImagePath = c.String(),
                        Description = c.String(),
                        BirthDate = c.DateTime(nullable: false),
                        JoinDate = c.DateTime(nullable: false),
                        LastLogin = c.DateTime(nullable: false),
                        City = c.String(),
                        Country = c.String(),
                        PhoneNumber = c.String(),
                    })
                .PrimaryKey(t => t.Id)
                .ForeignKey("dbo.AspNetUsers", t => t.UsersId)
                .Index(t => t.UsersId);
            
            CreateTable(
                "dbo.DishCategories",
                c => new
                    {
                        Id = c.Guid(nullable: false),
                        Name = c.String(),
                        Active = c.Boolean(nullable: false),
                    })
                .PrimaryKey(t => t.Id);
            
            CreateTable(
                "dbo.Dishes",
                c => new
                    {
                        Id = c.Guid(nullable: false),
                        Name = c.String(),
                        Recipe = c.String(),
                        PhotoPath = c.String(),
                        AddDate = c.DateTime(nullable: false),
                        ModifyDate = c.DateTime(nullable: false),
                        PublishDate = c.DateTime(nullable: false),
                        Archived = c.Boolean(nullable: false),
                        Published = c.Boolean(nullable: false),
                        DishDifficultyLevelId = c.Guid(nullable: false),
                        DishCategoryId = c.Guid(nullable: false),
                        AuthorId = c.String(),
                    })
                .PrimaryKey(t => t.Id)
                .ForeignKey("dbo.DishDifficultyLevels", t => t.DishDifficultyLevelId, cascadeDelete: true)
                .ForeignKey("dbo.DishCategories", t => t.DishCategoryId, cascadeDelete: true)
                .Index(t => t.DishDifficultyLevelId)
                .Index(t => t.DishCategoryId);
            
            CreateTable(
                "dbo.DishDifficultyLevels",
                c => new
                    {
                        Id = c.Guid(nullable: false),
                        Name = c.String(),
                        DifficultyLevel = c.Int(nullable: false),
                    })
                .PrimaryKey(t => t.Id);
            
            CreateTable(
                "dbo.DishIngredients",
                c => new
                    {
                        Id = c.Guid(nullable: false),
                        Name = c.String(),
                    })
                .PrimaryKey(t => t.Id);
            
        }
        
        public override void Down()
        {
            DropForeignKey("dbo.Dishes", "DishCategoryId", "dbo.DishCategories");
            DropForeignKey("dbo.Dishes", "DishDifficultyLevelId", "dbo.DishDifficultyLevels");
            DropForeignKey("dbo.UsersExtensions", "UsersId", "dbo.AspNetUsers");
            DropIndex("dbo.Dishes", new[] { "DishCategoryId" });
            DropIndex("dbo.Dishes", new[] { "DishDifficultyLevelId" });
            DropIndex("dbo.UsersExtensions", new[] { "UsersId" });
            DropTable("dbo.DishIngredients");
            DropTable("dbo.DishDifficultyLevels");
            DropTable("dbo.Dishes");
            DropTable("dbo.DishCategories");
            DropTable("dbo.UsersExtensions");
        }

W końcu po odpaleniu Update-Database wszystko poszło tak jak chciałem 🙂

A teraz wnioski i nauka na przyszłość

Po pierwsze: Migracje są fajne! Skoro już od jakiegoś czasu i tak sam tworzę wszystkie obiekty dla EFa to czemu sam tworzę tabelki w SQLu skoro coś może to zrobić za mnie 🙂

Po drugie: Najpierw muszę się lepiej przygotować jak czegoś nowego próbuję, bo moja szybko łapiąca frustracja nie pomaga przy bardziej oryginalnych problemach.

Po trzecie: Jak z kolegą w sklepie rozmawiasz i wszystko wydaje się banalnie proste to pamiętaj, że na pewno takie nie będzie 😀

Po czwarte: Cieszę się, że udało mi się doprowadzić tę sprawę do końca. Mam nadzieję, że komuś przyda się rozwiązanie mojego problemu 🙂

Related posts

  • Pingback: dotnetomaniak.pl()

  • AP

    Mi się przyda, bo wkrótce zanosi się na konieczność pracy na 2 contextach w aplikacji. Swoją drogą przypadki tu opisane są nawet logicznymi zachowaniami. Prawdziwe cuda to widziałem w chwili gdy uruchamiałem migracje, ale z driverem do MySQL! Tam to są jazdy, na przykład z ustawianiem domyślnego generatora SQL za pomocą wywołania: SetSqlGenerator(„MySql.Data.MySqlClient”, new MySqlMigrationSqlGenerator()); co zajęło sporo czasu zanim udało mi się doczytać czemu migracje z MySQL nie działają. Kolejny cud techniki to obowiązkowe wywołanie DbConfiguration.SetConfiguration(new MySqlEFConfiguration()); w metodzie Application_Start z Global.asax.cs. O enigmatycznych wpisach w Web.config nawet już nie wspomnę 🙂 W końcu udało się to skonfigurować i działa nawet poprawnie. Bywają za to problemy z cofaniem niektórych migracji, bo często baza wywala błędy typu ‚Table your_database.dbo.User does not exist’. Problemem jest tu wlepione na siłę ‚.dbo.’ jako jakaś schema. Wyraźny błąd autorów drivera do MySQL. Generalnie nie wszystkie schematy zapytań SQL rzutują się 1:1 z MSSQL na MySQL. Czasem pisze się trochę ręcznego kodu sql w migracjach, żeby coś tam osiągnąć. Tak czy inaczej zawracanie gitary takimi problemami wciąż rekompensuje koszt licencji Windows Servera i MSSQL 🙂

    • Kajetan Duszyński

      Przypadki opisane są zapewne logiczne, ale przy pierwszym podejściu do migracji wcale nie takie oczywiste 😉 Mam nadzieję, że faktycznie przyda się na później. Z MySQLem już bardzo dawno nie pracowałem, ale jak kiedyś mi się zdarzy to będę się odzywał 🙂

  • Zaba
    • Kajetan Duszyński

      Och. Gdybym to znalazł wcześniej 🙂