Подводные камни при миграции с MSSQL на MySQL и BLToolkit

    Имеем MSSQL 2008
    Хотим MySQL версии 5.х

    Зачем это может быть нужно?

    Для разработчиков на .NET променять MSSQL на MySQL это наверное все равно, что пересесть с мерседеса на что-то по-проще. Как говорится, к хорошему быстро привыкаешь.
    Но есть как минимум две причины сделать это
    • Сэкономить на лицензиях
    • Получить простую master-slave репликацию

    Работа с базой MSSQL в нашем случае осуществляется через LINQ провайдер.
    При переходе, не хотелось бы терять эту возможность, поэтому для работы с MySQL выбор пал на BLToolkit.

    Мигрируем

    Самое простое — это переписать код. BLToolkit в отличие от MS-провайдера относится к классу легких ORM, поэтому там немного другие конструкции подключения к базе, но LINQ-выражения останутся теми же.

    Думаете осталось перенести данные и все заработает?
    Как бы не так.


    Переносим данные

    Для миграции мы использовали MySQL Migration Toolkit.
    Большинство типов полей MSSQL экспортировались без проблем, но два поля на отрез отказались:
    • varchar
    • datetime2

    В нашем случае мы в исходной базе изменили varchar на nvarchar, а datetime2 на datetime. С первым все ясно — поле просто стало хранить символы в unicode, а во втором у нас не было причин по которым разработчикам понадобилось хранить datetime с точностью до 100 наносекунд, кроме той, что если положить DateTime.Now в базу, а потом извлечь, то значения могут быть не равны из-за округления (какой-то функциональный тест написанный таким образом падал и разработчик решил проблему использованием datetime2).

    LINQ mapping

    Поле типа timestamp экспортируется в поле с аналогичным типом, однако его поведение немного отличается.
    В классе .NET для MSSQL оно представлено каким-то недоделанным типом Binary, в результате чего приходится делать хаки для LINQ вроде этого
    В MySQL оно транслируется в обычный DateTime, но есть два подводных камня
    1. После экспорта нужно инициализировать все значения timestamp (например SET NULL для timestamp поля), т.к. после экспорта там будут сплошные нули, которые BLToolkit не понимает
    2. Задать DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, иначе поле не будет автоматически обновляться, как мы привыкли в MSSQL. (Прочитать про особенности обновления timestamp в MySQL)

    На этом этапе у вас скорее всего уже собирается проект и возможно что-то даже работает.

    Если mapping не работает по причине того, что для каких-то полей класса не находятся соответствующие поля в таблице, то это возможно потому, что для MS LINQ нужные поля классов нужно помечать атрибутом [Column], а для BLToolkit наоборот лишние поля нужно помечать атрибутом [MapIgnore]

    Следующим неприятным сюрпризом может стать тот факт, что BLToolkit не экранирует названия полей, поэтому не стоит использовать в ваших таблицах для имени поля всякие ключевые слова типа Key. При трансляции LINQ-выражения в SQL-запрос он упадет с синтаксической ошибкой.

    О пользе функционального тестирования

    Если ваша программ не покрыта тестами, то это очень плохо.
    Только благодаря простому функциональному тесту, который проверял, что возвращаемое NULL поле в базе равно null-строке в .NET выяснилось, что BLToolkit по-умолчанию достает string.Empty.
    Вот был бы неприятный сюрприз, если где-то стоит if(value == null).

    Чтобы поменять это поведение нужно прописать аттрибут mapping-класса
    [NullValue(typeof(string), null)]

    Последний подводный камень о котором я могу рассказать, это отображение поля типа Guid.
    Как вы знаете в MySQL нет специального типа для хранения Guid.
    Для этого там используется char(36).
    BLToolkit при отображении использует функцию Guid.Parse.
    Например, такой код следует использовать для отображения char(36) полей в Guid
    [TableName("Boxes")]
    public class Box:
    	{
    		[PrimaryKey]
    		public Guid BoxId { get; set; }
    	}
    

    Все выглядит прекрасно, пока кто-нибудь не попытается вставить в таблицу что-нибудь отлично от Guid.
    И даже несмотря на то, что в классе mapping-а вы определите это поле как string, BLToolkit все равно попытается сделать Guid.Parse
    Например, такой код свалится, при попытке прочитать из char(36) поля StringMayContainsGuid что-то не являющееся guid-ом.
    	
    [TableName("Boxes")]
    public class Box:
    	{
    		public string StringMayContainsGuid { get; set; }
    	}
    

    Простое решение в данном случае — поменять тип поля на varchar.

    В качестве инструмента для поиска подобных проблем я написал простой функциональный тест, который перечитывает все данные из таблиц:
    [TestFixture, Category("Functional")]
    	public class DbFunctionalTest
    	{
    		private readonly IMysqlClient mysqlClient = new MysqlClient(); // тут ваш клиент для подключения к базе
    
    		[Test]
    		public void ReadAllTables()
    		{
    			var dbMappingClasses = from classType in Assembly.GetExecutingAssembly().GetTypes()
    					where classType.IsClass && classType.GetCustomAttributes(typeof(TableNameAttribute), true).Length > 0
    					&& classType.Namespace == typeof(DbFunctionalTest).Namespace
    					select classType;
    
    			mysqlClient.PerformRequest(db =>
    			{
    				foreach (Type dbMappingClass in dbMappingClasses)
    				{
    					var tableName = ((TableNameAttribute) dbMappingClass.GetCustomAttributes(typeof (TableNameAttribute), true).First()).Name;
    					try
    					{
    						db.SetCommand(CommandType.Text, string.Format("SELECT * FROM {0}", tableName)).ExecuteList(dbMappingClass);
    					}catch(Exception e)
    					{
    						throw new Exception(string.Format("Can not read all records from table {0}", tableName), e);
    					}
    				}
    			});
    		}
    	}


    В заключение

    Надеюсь описанный опыт будет кому-то полезен.
    Но на самом-то деле мой коварный план состоял в том, чтобы получить еще больше пользы от ваших комментариев, так что welcome!
    Поделиться публикацией
    Комментарии 13
      –3
      Есть мнение, что можно было пересесть с мерседеса не на жигули(Mysql), а хотя бы на Man(postgresql) :))
        0
        Несмотря на то, что postgresql — действительно лучше mysql как сервер, иногда нужно тупо хранить какие-то данные просто в таблице без замашек на всякие крутые штуки, типа хранимых процедур, хитрых индексов, типов данных и т.п.
        В нашем случае это как раз так — мы очень редко пишем в базу, чуть чаще читаем, а для всего остального используем масштабируемые key-value хранилища.
          +2
          Банально в плане лицензий postgresql более лоялен, нежели mysql.
          А «хитрыми» процедурами и индексами, вас никто и не заставляет пользоваться.

          Если же необходимо key-value хранилище, к тому же не распределенное, то sqlite вам ещё больше подходит.

          А если необходимо распределенное, да вы еще и на C# пишете, то добро пожаловать в мир RavenDB
          0
          Подскажите MS Sql > PostgreSql migration tool, please
        0
        Спасибо, очень познавательно.
          +1
          Прекрасно!!! Спасибо.
          С гуидами печаль, конечно. Все сконвертить в строку — оно конечно можно, но если данных гигабайы? Ссыкотно.

          Кстати, еще одной практикой хранения гуидов в мускуле является binary[16]
            0
            Как-то непонятно

            >>Работа с базой MSSQL в нашем случае осуществляется через LINQ провайдер.

            Через какой LINQ провайдер? «Linq provider» уж слишком обощенный термин. Провайдер есть в Linq-2-Sql, в Nhinbernate, в EF и уже очень многих ORM.

            Мне видится, что если бы проект был написан с использование NHibernate или EF, то проблемы перевода практически не было.

              0
              >>Через какой LINQ провайдер?
              LINQ-2-SQL
                +1
                L2S — это ORM. У которого есть LINQ-провайдер. Как и у EF, и у NH, и у BLToolkit. У каждого свой.

                Все совершенно верно заметил A1lfeG, реализация некоторых общих extension-методов LINQ может отличаться у разных провайдеров. Плюс, отдельно взятые провайдеры могут поддерживать свои, уникальные методы. Все это, кстати, в общем случае делает LINQ «протекшей абстракцией», благодаря чему часто в работающем проекте LINQ-провайдер нельзя заменить простой заменой реализации интерфейса IQueryProvider, а можно только портировать код, использующий LINQ.
                  0
                  И так, еще к слову о «Работа с базой MSSQL в нашем случае осуществляется через LINQ провайдер».

                  Вся работа с базой не может осуществляться через LINQ-провайдер. Только чтение. LINQ-провайдер формирует только SELECT-запрос.
                0
                Не сочтите за рекламу, но в той же ситуации я подумал и предложил купить лицензию на www.devart.com/dotconnect/mysql/

                Код на дотнете менять не пришлось вообще. Хранимые процедуры переписывали, потому что синтаксис другой и оптимизация кривая.
                  0
                  Следующим неприятным сюрпризом может стать тот факт, что BLToolkit не экранирует названия полей, поэтому не стоит использовать в ваших таблицах для имени поля всякие ключевые слова типа Key. При трансляции LINQ-выражения в SQL-запрос он упадет с синтаксической ошибкой.
                  На текущий неактуально. Когда я встретился с сей проблемой, то вместо поиска обходных путей прикрутил к BLToolkit экранирование для MySQL.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое