Entity Framework с MySql и Migrations терпит неудачу, потому что "максимальная длина ключа составляет 767 байтов"

[Править] Эта проблема была решена! Смотрите инструкцию в конце поста.

[Правка 2] Хорошо, этот поток старый, и более новые версии MySQL Connector уже справляются с этим с помощью распознавателей MySQL EF. Ищите ответ @KingPong в этой теме. Я не проверял это, все же.

Я пытаюсь использовать MySql и EntityFramework с миграциями, но что-то не так.

Когда я вхожу Update-Database -Verbose в консоли диспетчера пакетов EF выполняет некоторые запросы, которые будут "отражать" классы моей модели, и все идет отлично, НО затем EF пытается выполнить этот запрос:

create table `__MigrationHistory` 
(
  `MigrationId` varchar(150)  not null 
  ,`ContextKey` varchar(300)  not null 
  ,`Model` longblob not null 
  ,`ProductVersion` varchar(32)  not null
  ,primary key ( `MigrationId`,`ContextKey`) 
 ) engine=InnoDb auto_increment=0

И результат: Specified key was too long; max key length is 767 bytes

Я попытался изменить сортировку базы данных на utf-8, но все так же. Возможно, длина ключа составляет 450 символов, в математике UTF-8 (что я могу ошибаться), я думаю, что она пытается создать ключ длиной около 1800 байт.

Поскольку я новичок в EF, я следовал некоторым учебникам, и они сказали мне сделать это:

    public Configuration()
    {
        AutomaticMigrationsEnabled = false;

        SetSqlGenerator("MySql.Data.MySqlClient", new MySql.Data.Entity.MySqlMigrationSqlGenerator());
    }

Возможно, этот генератор SQL делает что-то не так, или сам EF просит генератор сделать ключ длиной до 767 байт.

Как я могу это исправить, избежать этой проблемы и заставить ее работать с MySql?

[Править] Хорошо, эта проблема была решена. Вы должны сказать EF, что он должен изменить способ генерации таблицы __MigrationHistory.

Что я сделал: во-первых, создайте файл с именем MySqlHistoryContext.cs (или что вы хотите) с этим содержанием:

...
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Migrations.History;


namespace [YOUR NAMESPACE].Migrations //you can put any namespace here, but be sure you will put the corret using statement in the next file. Just create a new class :D
{
    public class MySqlHistoryContext : HistoryContext
    {

        public MySqlHistoryContext(DbConnection connection, string defaultSchema):base(connection,defaultSchema)
        {

        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<HistoryRow>().Property(h => h.MigrationId).HasMaxLength(100).IsRequired();
            modelBuilder.Entity<HistoryRow>().Property(h => h.ContextKey).HasMaxLength(200).IsRequired(); 
        }
    }
}

У вас может быть файл с именем Configuration.cs внутри вашего Migrations папка. Если да, внесите необходимые изменения, в противном случае создайте новый файл. На самом деле вы не сможете получить эту ошибку, если у вас не было этого файла, так как EF создает его автоматически, когда вы Add-Migration [name],

namespace [YOUR NAMESPACE].Migrations
{
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;

    internal sealed class Configuration : DbMigrationsConfiguration<CodeFirstMySql.Models.Context>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;

            SetSqlGenerator("MySql.Data.MySqlClient", new MySql.Data.Entity.MySqlMigrationSqlGenerator()); //it will generate MySql commands instead of SqlServer commands.

            SetHistoryContextFactory("MySql.Data.MySqlClient", (conn, schema) => new MySqlHistoryContext(conn, schema)); //here s the thing.



        }

        protected override void Seed(CodeFirstMySql.Models.Context context){}//ommited
    }
}

затем Update-Database -Verbose и получайте удовольствие!

8 ответов

Решение

Ответ перефразирован из Добавление пользовательского контекста MigrationHistory...

EF6 использует таблицу MigrationHistory для отслеживания изменений модели и обеспечения согласованности между схемой базы данных и концептуальной схемой. Эта таблица не работает для MySQL по умолчанию, потому что первичный ключ слишком велик. Чтобы исправить эту ситуацию, вам нужно уменьшить размер ключа для этой таблицы.

По сути, EF6 позволяет вам изменять размер ключа для столбцов индекса MigrationId/ContextKey, используя Fluent API, например:

modelBuilder.Entity<HistoryRow>().Property(h => h.MigrationId).HasMaxLength(100).IsRequired();
modelBuilder.Entity<HistoryRow>().Property(h => h.ContextKey).HasMaxLength(200).IsRequired();

Полная инструкция здесь...

С MySQL Connector/Net 6.8 и Entity Framework версии 6 вы можете решить эту проблему, используя встроенную поддержку MySQL для EF. Хитрость заключается в том, чтобы сказать Entity Framework, что нужно использовать преобразователи MySQL. Из Руководства разработчика по Connector / Net:

Это можно сделать тремя способами:

  • Добавление атрибута DbConfigurationTypeAttribute в класс контекста:

    [DbConfigurationType (TypeOf(MySqlEFConfiguration))]
  • Вызов DbConfiguration.SetConfiguration(new MySqlEFConfiguration()) при запуске приложения

  • Установите тип DbConfiguration в файле конфигурации:

Также возможно создать пользовательский класс DbConfiguration и добавить необходимые средства разрешения зависимостей.

Когда я следовал этим инструкциям (я использовал подход файла конфигурации), таблица была успешно создана. Он использовал следующий DDL:

create table `__MigrationHistory` (
  `MigrationId` nvarchar(150) not null,
  `ContextKey` nvarchar(300)  not null,
  `Model` longblob not null,
  `ProductVersion` nvarchar(32) not null,
  primary key ( `MigrationId`)
) engine=InnoDb auto_increment=0

Хотя принятый ответ ChAmp33n решил проблему, поднятую спрашивающим, я думаю, что после этого ответа произошли некоторые "критические" изменения. Я применил принятый ответ, но исключения остались.

Проверяя запросы SQL, сгенерированные EF, я обнаружил, что проблема связана с UserName (varchar utf8 256) в таблице AspNetUsers и Name (varchar utf8 256) в таблице AspNetRoles, в то время как таблица для HistoryRow была в порядке.

Таким образом, следующие коды решили проблему.

    public class WebPortalDbContext : IdentityDbContext<ApplicationUser>
{
    public WebPortalDbContext()
        : base("IdentityConnection")
    {

    }

    public static WebPortalDbContext Create()
    {
        return new WebPortalDbContext();
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Microsoft.AspNet.Identity.EntityFramework.IdentityRole>()
            .Property(c => c.Name).HasMaxLength(128).IsRequired();

        modelBuilder.Entity<Microsoft.AspNet.Identity.EntityFramework.IdentityUser>().ToTable("AspNetUsers")//I have to declare the table name, otherwise IdentityUser will be created
            .Property(c => c.UserName).HasMaxLength(128).IsRequired();
    }


}

Чтобы прояснить решение, вот библиотеки, которые я использую:

  1. EF 6.1.0
  2. Microsoft ASP.NET Identity EntityFramework 2.0.0
  3. ASP.NET Identity Owin 2.0.0
  4. MySql.NET библиотеки 6.8.3

И это решение также работает для

  1. EF 6.1.1
  2. Microsoft ASP.NET Identity EntityFramework 2.0.1
  3. ASP.NET Identity Owin 2.0.1

Для тех, кто до сих пор страдает от такой головной боли, как я, я сам нашел решение. Перейдите к файлу миграции (в моем случае 201603160820243_Initial.cs), отредактируйте все maxLength: от 256 до 128. Запустите Update-database снова, и это работает для меня.

До сих пор не знаю, почему nvarchar(256) (512 байт) не удовлетворяет 767 байтам и обновления не для столбцов ключей.

Ознакомьтесь с этим рабочим пунктом на сайте EF CodePlex. Если вы используете EF6, вы можете настроить таблицу истории миграций и сделать столбцы короче - вот статья, которая показывает, как это сделать.

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

Это проще с более прямым доступом к вашей базе данных, таким как MySQL Workbench или phpMyAdmin, для запуска команд MySQL. Вы также можете закодировать некоторые команды SQL в C #, но я этого не сделал.

  1. Я не уверен, что этот первый шаг актуален для всех, но я хотел бы сделать это первым вместо того, чтобы запускать Add-Migration/Update-Database несколько раз позже. Чтобы добавить начальную учетную запись администратора в базу данных, мне пришлось

    1. изменить мой ApplicationDbContextкласс в IdentityModels.csк:

                 public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
       {
           public static ApplicationDbInitializer adi = new ApplicationDbInitializer();
      
           public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false)
           {
               adi.InitializeDatabase(this); //Delete after first run:
           }
      
           static ApplicationDbContext()
           {
               Database.SetInitializer<ApplicationDbContext>(adi); //Delete after first run
           }
           public static ApplicationDbContext Create()
           {
               return new ApplicationDbContext();
           }
      
      
           /* Optional: If you'd like to rename your tables, 
           *  you can do it by overriding this "OnModelCreating()" method.
           *  Be sure to change the table names in your 
           *  "Migrations\#######EmptyMigrations.cs" file to match the new table names
           *  before running "Update-Database" in the PM Console. */
           protected override void OnModelCreating(DbModelBuilder modelBuilder)
           {
               base.OnModelCreating(modelBuilder);
      
               /* Note that for the AspNetUsers table
               *  —because the "IdentityUser" class was extended 
               *  into "ApplicationUser" earlier in IdentityModels.cs—
               *  You must refer to the new class rather than the base class */
      
               //modelBuilder.Entity<ApplicationUser>().ToTable("tblIdentityUsers");
               //modelBuilder.Entity<IdentityUserClaim>().ToTable("tblIdentityUserClaims");
               //modelBuilder.Entity<IdentityUserLogin>().ToTable("tblIdentityUserLogins");
               //modelBuilder.Entity<IdentityUserRole>().ToTable("tblIdentityUserRoles");
               //modelBuilder.Entity<IdentityRole>().ToTable("tblIdentityRoles");
           }
       }
      
    2. Я мало знаю о том, как работает OWIN, но стандартный код давал мне нули для userManagerа также roleManagerв InitializeIdentityForEFметод IdentityConfig.csпоэтому вместо этого я использовал эти конструкторы. Очевидно, измените учетную запись администратора на любую, которую вы хотите использовать.

                 public static void InitializeIdentityForEF(ApplicationDbContext db) {
           //var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
           //var roleManager = HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>();
           var roleStore = new RoleStore<IdentityRole>(db);
           var roleManager = new RoleManager<IdentityRole>(roleStore);
           var userStore = new UserStore<ApplicationUser>(db);
           var userManager = new UserManager<ApplicationUser>(userStore);
           //In normal usage I would probably wrap these in a series of using() statements as well.
      
           const string name = "Administrator";
           const string email = "foo@bar.com";
           const string password = "Password123";
           const string roleName = "Admin";
      
           //Create Role Admin if it does not exist
           var role = roleManager.FindByName(roleName);
           if (role == null) {
               role = new IdentityRole(roleName);
               var roleresult = roleManager.Create(role);
           }
      
           var user = userManager.FindByName(name);
           if (user == null) {
               user = new ApplicationUser { UserName = name, Email = email };
               var result = userManager.Create(user, password);
               result = userManager.SetLockoutEnabled(user.Id, false);
           }
      
           // Add user admin to Role Admin if not already added
           var rolesForUser = userManager.GetRoles(user.Id);
           if (!rolesForUser.Contains(role.Name)) {
               var result = userManager.AddToRole(user.Id, role.Name);
           }
       }
      
  2. Откройте консоль диспетчера пакетов

    1. Бежать PM> Enable-Migrations
    2. Бежать PM> Add-Migration EmptyMigration(это можно назвать как угодно)
  3. Откройте только что созданный Project\Migrations\###########EmptyMigration.cs

    1. в Up()метод, найти CreateTable()вызывает «dbo.AspNetRoles» и «dbo.AspNetUsers» (или как вы могли их переименовать).

    2. Закомментируйте .Index()метод на них и добавьте точку с запятой, чтобы закончить эти две цепочки методов на .PrimaryKey()

      Это должно выглядеть примерно так:

                 CreateTable(
           "dbo.AspNetRoles",
           c => new
           {
               Id = c.String(nullable: false, maxLength: 128, storeType: "nvarchar"),
               Name = c.String(nullable: false, maxLength: 256, storeType: "nvarchar"),
           })
           .PrimaryKey(t => t.Id);
       //.Index(t => t.Name, unique: true, name: "RoleNameIndex");
      

      а также

                CreateTable(
           "dbo.AspNetUsers",
           c => new
               {
                   Id = c.String(nullable: false, maxLength: 128, storeType: "nvarchar"),
                   Email = c.String(maxLength: 256, storeType: "nvarchar"),
                   EmailConfirmed = c.Boolean(nullable: false),
                   PasswordHash = c.String(unicode: false),
                   SecurityStamp = c.String(unicode: false),
                   PhoneNumber = c.String(unicode: false),
                   PhoneNumberConfirmed = c.Boolean(nullable: false),
                   TwoFactorEnabled = c.Boolean(nullable: false),
                   LockoutEndDateUtc = c.DateTime(precision: 0),
                   LockoutEnabled = c.Boolean(nullable: false),
                   AccessFailedCount = c.Int(nullable: false),
                   UserName = c.String(nullable: false, maxLength: 256, storeType: "nvarchar"),
               })
           .PrimaryKey(t => t.Id);
       // .Index(t => t.UserName, unique: true, name: "UserNameIndex");
      
  4. Вернитесь в консоль диспетчера пакетов и запустите PM> Update-Database -Verbose

  5. Перейдите в свой MySQL Workbench или phpMyAdmin или что-то еще, что вы используете для прямого запуска команд SQL.

    1. Выполните следующие команды, чтобы изменить набор символов ключевого столбца на что-то меньшее:

                ALTER TABLE `DatabaseName`.`AspNetUsers` 
          MODIFY `UserName` VARCHAR(256) 
          CHARACTER SET latin1;
      ALTER TABLE `DatabaseName`.`AspNetRoles` 
          MODIFY `Name` VARCHAR(256) 
          CHARACTER SET latin1;
      
    2. Создайте индексы, которые он не смог сделать в первый раз:

                 CREATE UNIQUE INDEX `UserNameIndex` 
           ON `DatabaseName`.`AspNetUsers` (`UserName` DESC) USING HASH;
       CREATE UNIQUE INDEX `RoleNameIndex` 
           ON `DatabaseName`.`AspNetRoles` (`Name` DESC) USING HASH;
      

PS Я заметил, что в Down()метод при попытке отменить миграцию, он сработал только после того, как я взял «dbo». вне имен таблиц. Ваш пробег может отличаться.

Это связано с тем, что имя внешнего ключа, рассчитанное структурой сущности, может быть слишком длинным. Мы можем указать имя внешнего ключа в файле migartion.cs. Четвертый параметр ForeignKey() метод ForeignKeyName,

По умолчанию это выглядит примерно так:

.ForeignKey("xxx", t => t.yyy) <br/>
Modify it as: <br/>
.ForeignKey("xxx", t => t.yyy,false, "YourForeignKeyName")

Вы указали первичный ключ длиной 450 символов Юникод. MySQL предполагает наихудший случай для размера символов при проверке ограничений размера столбца - возможно, 2 или 4 байта, в зависимости от вашего набора символов и параметров сортировки - так что это либо 900, либо 1800 байтов, оба слишком длинные.

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

Другие вопросы по тегам