середа, 20 червня 2012 р.

Синхронізація даних в MSSql і MySql на основі NNibernate

На етапі послідовного переходу з MS SQL Server на MySql постала задача синхронізації даних. Необхідно було узгоджувати окремі таблиці, вибірки і визначені записи. Найзручнішим продуктом для вирішення задачі на початку був продукт Cross-Database Studio (trial) від DBBalance, але згодом необхідно було автоматично узгоджувати записи.

Проект СУНП "Універсум" від звичайних stored procedure повільно переходив на NHibernate. Ця ORM виявилася необхідним і достатнім інструментом для посталеної задачі.

Всі об`єкти, які представляють таблиці даних, реалізовують інтерфейс IItem:

/// <summary>
/// Інтерфейс найпростішого запису
/// </summary>
public interface IItem
{
    /// <summary>
    /// Ідентифікатор
    /// </summary>
    int Id { get; set; }
    /// <summary>
    /// Назва
    /// </summary>
    string Name { get; set; }
    /// <summary>
    /// Стан запису
    /// </summary>
    ItemState State { get; set; }
}
/// <summary>
/// Види бази даних
/// </summary>
public enum DbType
{
    MySql,
    MsSql
}
/// <summary>
/// Стан запису
/// </summary>
public enum ItemState : byte
{
    /// <summary>
    /// Невідомо
    /// </summary>
    Unknown,
    /// <summary>
    /// Для видалення
    /// </summary>
    ForDelete,
    /// <summary>
    /// Для вставки або оновлення
    /// </summary>
    ForUpdate,
    /// <summary>
    /// Видалений
    /// </summary>
    Deleted,
    /// <summary>
    /// Відмічений
    /// </summary>
    Checked,
    /// <summary>
    /// Знайдений, існуючий
    /// </summary>
    Existed,
    /// <summary>
    /// Збережений
    /// </summary>
    Saved,
    /// <summary>
    /// Новий запис
    /// </summary>
    New,
    /// <summary>
    /// Доданий
    /// </summary>
    Added
}
/// <summary>
/// Синхронізує об`єкт типу T
/// </summary>
/// <typeparam name="T">Тип об`єкта (назва таблиці)</typeparam>
/// <param name="type">База даних, яка є призначенням (в яку відбувається синхронізація)</param>
public static void Sync<T>(DbType type) where T : IItem
{
    //
    IDictionary<int, T> source = new Dictionary<int, T>();
    IDictionary<int, T> destination = new Dictionary<int, T>();

    //вибираємо всі властивості об`єкта (записа таблиці даних)
    var allProps = typeof(T).GetProperties().ToList();
    //вибираємо лише властивості, які змінюються (властивості,  які відповідають полям у базі даних)
   var props = allProps.Where(i => i.PropertyType.Attributes.HasFlag(TypeAttributes.Sealed) && !i.PropertyType.IsEnum).ToList();

    IList<T> data = new List<T>();
    //сесія бази, яка є джерелом
    NHibernate.ISession sourceDb = null;
    //сесія бази, яка є призначенням
    NHibernate.ISession destinDb = null;

    if (type == DbType.MySql)
    {
        sourceDb = DbServer.GetMsSqlSession();
        destinDb = DbServer.GetMySqlSession();
    }
    else
    {
        sourceDb = DbServer.GetMySqlSession();
        destinDb = DbServer.GetMsSqlSession();
    }
    //завантажуємо дані в словник джерела
    source = sourceDb.Query<T>().ToDictionary(i => i.Id, i => i);

    if (source.Count == 0)
        return;

    destination = destinDb.Query<T>().ToDictionary(i => i.Id, i => i);

    try
    {
        DateTime start = DateTime.Now;
        source.Values.AsParallel().ForAll(item =>
        {
            if (!destination.ContainsKey(item.Id))
            {
                //якщо запис відсутній, позначаємо, що він буде новий
                item.State = ItemState.New;
                data.Add(item);
            }
            else
            {
                //знаходимо записи, що відрізняються
                foreach (var p in props)
                {
                    var svalue = p.GetValue(item, null);
                    var dvalue = p.GetValue(destination[item.Id], null);
                    //порівнюємо властивості (поля) джерела і призначення
                    if (!Equals(svalue, dvalue))
                    {
                        if (svalue != null && dvalue != null && svalue.Equals(0) && dvalue.Equals(0))
                        {

                        }
                        else
                        {
                            //позначаємо запис
                            item.State = ItemState.ForUpdate;
                            data.Add(item);
                            break;
                        }
                    }
                }
            }
        });

        //пошук відсутніх записів та записів, що відрізняються
        destination.Values.AsParallel().ForAll(item =>
        {
            if (!source.ContainsKey(item.Id))
            {
                //зайві записи
                item.State = ItemState.ForDelete;
                data.Add(item);
            }
        });
        DateTime end = DateTime.Now;
        var duration = (end - start);
    }
    catch (Exception ex)
    {
        ex.Save();
    }
    destinDb.Dispose();
    sourceDb.Dispose();

    if (data.Count == 0)
        return;

    //відкриваємо сесію для синхронізації
    if (type == DbType.MySql)
        destinDb = DbServer.GetMySqlSession();
    else
        destinDb = DbServer.GetMsSqlSession();

    data = data.OrderBy(i => i.Id).ToList();
    using (var trn = destinDb.BeginTransaction())
    {
        try
        {
            foreach (var item in data)
            {
                if (item.State == ItemState.ForDelete)
                    destinDb.Delete(item);
                else if (item.State == ItemState.ForUpdate)
                    destinDb.Update(item);
                else if (item.State == ItemState.New)
                    destinDb.Save(item, item.Id);
            }
            trn.Commit();
        }
        catch (Exception ex)
        {
            trn.Rollback();
            ex.Save();
        }
    }
    destinDb.Dispose();
}

Для синхронізації вибірок, необхідно створити список даних IList<T> data, додати в нього записи, які відрізняються і передати його методу Sync<T>:

/// <summary>
/// Синхронізовує список даних у двох базах
/// </summary>
/// <typeparam name="T">Тип даних</typeparam>
/// <param name="data">Список записів, які відрізняються у базах даних</param>
/// <param name="type">База даних</param>
public static bool Sync<T>(IList<T> data, DbType type = DbType.MySql) where T : IItem
{
    bool result = false;
    if (data == null || data.Count == 0)
        return true;

    ISession db = null;
    if (type == DbType.MySql)
        db = DbServer.GetMySqlSession();
    else
        db = DbServer.GetMsSqlSession();

    using (var trn = db.BeginTransaction())
    {
        try
        {
           foreach (var item in data)
           {
                if (item.State == ItemState.ForDelete)
                {
                    var t = item.GetType();
                    var row = db.Get(t, item.Id);
                    if (row != null)
                       db.Delete(row);
                }
                else if (item.State == ItemState.ForUpdate)
                    db.Update(item);
                else if (item.State == ItemState.New)
                    db.Save(item, item.Id);
                else if (item.State == ItemState.Saved)
                    db.SaveOrUpdate(item);
            }
            trn.Commit();
            result = true;
        }
        catch (Exception ex)
        {
            trn.Rollback();
            ex.Save();
        }
    }
    db.Dispose();
    return result;
}

Наприклад, синхронізувати таблицю спеціальностей:
Sync<ProfessionsRow>(DbType.MySql);

Розпаралелення процесу порівняння трохи додає швидкодії. Порівняння близько 400 тис. записів оцінок студентів відбувалося орієнтовно 6 секунд на ноутбуці Intel Core i3. Це у 1,5 рази швидше послідовного перебору даних.

СУНП "Універсум" повністю переведено на MySql і потреби в синхронізаторі вже немає. Наведений текст програм достаній для локального вирішення задачі синхронізації даних в двох базах.

неділя, 19 вересня 2010 р.

Видалення рядків із таблиці в MySql. Error Code: 1093, 1175

Маю значний досвід програмування в MS SQL Server 2000. Зараз вивчаю MySql. Опишу проблему, яка виникла під час видалення рядків із таблиці.

Є таблиця:

CREATE TABLE `menu` (
`MenuID` int(11) NOT NULL AUTO_INCREMENT,
`MenuIDP` int(11) NOT NULL,
`Menu` varchar(100) NOT NULL,
PRIMARY KEY (`MenuID`),
KEY `IX_MenuIDP` (`MenuIDP`),
) ENGINE=InnoDB AUTO_INCREMENT=407 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

В СУБД MS SQL Server вкладений запит виконувався без проблем:

delete from menu where `MenuIDP` not in(select `MenuID` from menu);

"Видали, будь ласка, всі рядки із таблиці menu, в яких значення поля `MenuIDP` не співпадають із жодним із значень поля `MenuID`". Погодьтеся, що інструкція лаконічна і зрозуміла.

MySql на такий запит чемно повернув

Error Code: 1093
You can't specify target table 'menu' for update in FROM clause

Вивчаючи досвід колег в Internet, дізнався, що MySql не може видалити рядки із таблиці, яка тимчасово блокується командою select. Очевидно в цьому є деякий смисл.
Если вам нужно чтобы заработал ваш запрос - могу сказать, что mysql можно перехитрить делая селект не из самой таблицы, а присоединяя ее к чему либо (например к select 1 as k) по right outer join.(Удаление данных из таблицы MySql)
Хитрити із MySql я не став, а скористався декількома командами із використанням тимчасової таблиці:

   1:  /* створити тимчасову таблицю */
   2:  create temporary table ForRowsDelete(`MenuID` int not null);
   3:  /* вставити код рядків, які потрібно видалити, у тимчасову таблицю */
   4:  insert into ForRowsDelete(`MenuID`)
   5:  select `MenuID` from menu where `MenuIDP` not in(select `MenuID` from menu);
   6:  /* видалити із таблиці menu */
   7:  delete from menu where `MenuID` in(select `MenuID` from ForRowsDelete);
   8:  /* видалити тимчасову таблицю */
   9:  drop temporary table ForRowsDelete;

Багато тексту, зате покроково.

Інструкція 7 чемно повернула:

Error Code: 1175
You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column

Виявляється, що MySql просить, щоб після WHERE було вказано індексоване поле незалежно від того, чи потрібно воно для запиту. Але ж поле `MenuID` є PRIMARY?!

В інструкцію 7 додав після WHERE "and `MenuIDP`>0":

delete from menu where `MenuID` in(select `MenuID` from ForRowsDelete) and `MenuIDP`>0;

Такий підхід до видалення даних в MySql забезпечує насамперед високу швидкодію і безпеку інформації. Не сперечаюся.

Update 20.09.2010

Щодо Error Code: 1175. Як виявилося, консольний режим пропускав запити на видалення. Помилку видавала MySql WorkBench 5.2.27CE. Вивчаючи цю IDE, знайшов те, що потрібно.


Достатньо відключити у Edit -> Preferences ... -> SQL editor опцію Forbid UPDATE and DELETE statements without a WHERE clause (safe updates). Але за безпеку відповідати будете самостійно.

неділя, 12 вересня 2010 р.

Паттерн для перевірки правильності запису електронної пошти

Вивчаю регулярні вислови. Процедура перевірки правильності запису електронної адреси є без сумніву необхідна.

В код програми я включив паттерн, який пропонує бібліотека MSDN:

string _pattern = "^(?("")("".+?""@)|(([0-9a-zA-Z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-zA-Z])@))(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,6}))$"

Пропозиція Microsoft на перший погляд універсальна. Шаблон аналізує не лише ім`я домена, але і IP-адреси на зразок j_9@[129.126.118.1]. Але виявилося, що паттерн вважає неправильними адреси поштового сервісу www.i.ua (my-email@i.ua). Очевидно, назва домену із однієї літери є нестандартною. Але ж сервіс існує?

Проблемною можна вважати передостанню частину паттерну: "...([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+...". Спочатку відбувається пошук одного символа із діапазону "[0-9a-zA-Z]" (тобто від "0" до "9", або від "a" до "z", або від "A" до "Z"). Наступними можуть бути-не-бути дефіси або символи з діапазону "[0-9a-zA-Z]" (частина "[-\w]*"). Завершує будь який символ із "[0-9a-zA-Z]". Пошуковий текст повинен закінчитися крапкою.

Дивно, але за таким паттерном правильними вважаються адреси вигляду myemail@server-----x.com.

Передостанню частину я переписав як "...([0-9a-zA-Z]+[-]?[0-9a-zA-Z]*\.)+...". Пропоную один або декілька символів "[0-9a-zA-Z]+", потім відсутність або один дефіс "[-]?", за яким слідує відсутність або декілька символів "[0-9a-zA-Z]*". Шаблон завершується крапкою "\.". Весь вираз повторюється один або декілька разів - для цього є знак "+" в кінці рядка.

Загальний вигляд удосконаленого мною патерну перевірки правильності електронної пошти:

string _pattern = @"^(?("")("".+?""@)|(([0-9a-zA-Z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-zA-Z])@))(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z]+[-]?[0-9a-zA-Z]*\.)+[a-zA-Z]{2,6}))$";

Отже, не порушуючи особливо логіки базового шаблону, поштові адреси із сервісу www.i.ua вважаються правильними, домени із n-ою кількістю дефісів - неправильними.


З повагою, Юрій Оснадчук.

Література:
  1. Практическое руководство. Проверка строк на соответствие формату электронной почты.
  2. Регулярные выражения.
  3. Сервіс тестування регулярних висловів realcode.ru

неділя, 15 серпня 2010 р.

Інша сторона життя ...

Цей блог міститиме інформацію про особливості розробки та впровадження системи управління навчальним процесом "Універсум", - програми, яка з 2004 року активно допомагає керувати навчальним процесом на соціально-психологічному факультеті Житомирського державного університету імені Івана Франка.

Автори програми:
  • Олександр Леонідович Музика, - кандидат психологічних наук, доцент, декан соціально-психологічного факультету ЖДУ імені Івана Франка, мудрий керівник, активний психолог-дослідник і просто чудова людина.
  • Оснадчук Юрій Олегович (тобто я), асистент кафедри соціальної та практичної психології, неофіційний програміст факультету.