Имеем 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, но есть два подводных камня
- После экспорта нужно инициализировать все значения timestamp (например SET NULL для timestamp поля), т.к. после экспорта там будут сплошные нули, которые BLToolkit не понимает
- Задать
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!
Автор: gmlexx