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

в 23:41, , рубрики: Новости

Имеем 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!

Автор: gmlexx

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js