Автоматизация изменений БД в .NET

    Здравствуйте!
    Я хотел бы рассказать о проектах Migrator.Net и ECM7.Migrator.

    Migrator.Net — это механизм контроля версий базы данных, похожий на Migrations в Ruby on Rails. Migrator позволяет автоматизировать выполнение операций изменения БД и автоматически ведет учет версий.

    Migrator написан на C# и будет удобен, в первую очередь, при использовании в проектах под.NET.

    Сначала я сделаю небольшой обзор указанных проектов, а потом постараюсь выделить их «плюсы» и «минусы».



    Основные принципы

    Для изменения БД вы создаете сборку, содержащую «миграции». «Миграции» — это классы, каждый из которых описывает небольшое изменение БД.

    При написании классов-«миграций» необходимо унаследоваться от специального базового класса Migration и перекрыть 2 метода:
    • метод Up задает операции над БД, необходимые для перехода к следующей версии БД (например, создание таблицы);
    • метод Down задает операции, необходимые для отмены изменений, выполненных методом Up (например, если в методе Up создается новая таблица, то в методе Down она должна удаляться).


    Действия, которые необходимо выполнить над БД, задаются на том языке, на котором вы пишете ваш .NET проект. Имеется специальный framework, содержащий классы и методы для выполнения основных операций над БД. Также есть возможность выполнить произвольный SQL-запрос.

    Пример миграции

    [Migration(7)]
    public class AbstractTestMigration : Migration
    {
        override public void Up()
        {
          Database.AddTable("CustomerAddress",
            new Column("customer_id",
                DbType.Int32, ColumnProperty.PrimaryKey),
            
            new Column("address",
                DbType.AnsiString, ColumnProperty.NotNull));
        }

        override public void Down()
        {
          Database.RemoveTable("CustomerAddress");
        }
    }

    * This source code was highlighted with Source Code Highlighter.


    Давайте разберем, что тут происходит:
    1. Мы видим, что наш класс-«миграция» помечен атрибутом [Migration(7)]. Параметр атрибута — значение типа long, задающее номер версии БД, на который произойдет переход при выполнении изменений из метода Up.
    2. Как мы уже говорили, класс-«миграция» должен быть унаследован от базового класса Migration. Также класс-«миграция» должен переопределять методы Up и Down базовго класса.
    3. Базовый класс Migration имеет свойство Database. Это свойство содержит объект, реализующий интерфейс ITransformationProvider. Вызывая методы интерфейса ITransformationProvider у объекта Database можно выполнять операции над БД. Например, в методе Down используется Database.RemoveTable для удаления таблицы.
      Интерфейс ITransformationProvider содержит метод int ExecuteNonQuery(string sql), позволяющий выполнить произвольный SQL-запрос.

    Способы выполнения изменений

    Migrator предоставляет несколько способов запуска на выполнение ваших классов-«миграций».
    1. Консольное приложение
    2. Task для NAnt
    3. Task для MsBuild
    4. Программное выполнение — выполнение «миграций» из вашей программы через API

    Каждый из этих способов получает в качестве параметров тип используемой СУБД, строку подключения и номер версии БД, которую необходимо получить.


    Поддерживаемые СУБД

    Migrator.NET может работать с различными СУБД. При запуске «миграций» на выполнение вместе с параметрами подключения к БД мигратору указывается класс-«диалект», при помощи которого формируются SQL-запросы для конкретной СУБД.

    В проекте Migrator.NET есть «диалекты» для следующих СУБД: MySQL, Oracle, SQLite, MS SQL Server (в т.ч. CE), PostgreSQL.

    Отличия проектов Migrator.Net и ECM7.Migrator

    В начале этого поста я упомянул вместе с Migrator.Net еще один проект: ECM7.Migrator. Сейчас я расскажу о нем подробнее.

    Весной этого года мы с коллегами узнали про Migrator.NET. Мы попробовали его использовать и нам очень понравилось. Сейчас я использую мигратор на обеих моих работах (в сумме 3 довольно больших проекта, уже работающих в реальных условиях).
    К сожалению, оказалось, что Migrator.NET имеет некоторые неудобства, а также его провайдер для Oracle содержит много ошибок (об этом, кстати, написано на главной странице проекта в google code).

    За период с мая по сентябрь в Migrator.NET нами было внесено большое количество изменений: переписано несколько классов т.н. «ядра», исправлено много ошибок в оракловом провайдере, несколько «косметических» доработок, делающих использование мигратора более удобным.

    В результате, код, который мы дорабатывали настолько разошелся с кодом оригинального проекта, что решено было сделать отдельный проект. Именно он и выложен в google code под названием ECM7.Migrator. Также была частично переведена (и исправлена под новый проект) документация.

    Кроме того, за ненадобностью была удалена поддержка PostgreSQL.

    Чем мигратор может вам помочь

    1. С мигратором изменения БД удобно распространять. Нет папки с кучей скриптов, вместо нее единственная dll. Думаю, вы оцените это, если вам нужно будет обновить windows-приложение у нескольких сотен пользователей.
    2. Миграции удобно писать: проверка на этапе компиляции, intellisense и т.д. Никто не пишет SQL вручную. Обычно его генерируют через GUI. Часто при этом генерируется куча «левых» команд. С мигратором вы можете полностью все контролировать, не затрачивая на это много времени.
    3. Миграции удобно выполнять. Как я уже писал, для мигратора имеются таски для NAnt и MSBuild. С ними вы легко сможете выполнять миграции во время автоматизированной сборки вашего проекта. Кроме того, средства программного выполнения миграций делают возможными такие вещи как обновление структуры БД при старте приложения или создание чистой БД для модульных тестов.
    4. Миграции не зависят от СУБД. Если честно, мне ни разу еще не приходилось работать над проектом, который должен поддерживать несколько СУБД. Но, возможно, независимость от СУБД тоже кому-то нужна.
    5. В миграции легко добавлять произвольную логику. Например, вы можете в цикле создать кучу таблиц или добавить к названию каждой таблицы некоторый префикс, определяемый в настройках приложения.

    Недостатки

    За время моей работы с мигратором я почувствовал два его недостатка:
    1. Если человек не любит работать с программами через командную строку (как и я), то для него будет недостатком отсутствие программы с GUI, позволяющей запустить миграции на выполнение. К счастью, эта проблема быстро решилась написанием bat — файла, запускающего консольное приложение мигратора с нужными параметрами.
    2. Второе неудобство заключается в том, что когда я пишу миграцию, я не помню названия уже созданных объектов БД. Эту проблему так и не удалось устранить. При написании миграций смотрю названия в GUI или в файлах hbm (у нас во многих проектах используется NHibernate).


    Чего не может мигратор

    Мигратор не избавляет вас от необходимости продумывать структуру БД. Он не генерирует никаких изменений БД автоматом. Это просто более удобный способ вручную задать все действия над БД, а также, более удобный способ их выполнить.

    Заключение

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

    Я буду очень рад, если кто-то заинтересуется мигратором. Это будет означать, что труды его разработчиков (а значит, и мои тоже) не пропали зря. Я постараюсь всячески помогать таким людям в использовании мигратора — отвечать на вопросы и оперативно исправлять найденные баги.

    Всем спасибо за внимание.

    — Примечание
    Некоторое время назад здесь уже обсуждался проект octalforty Wizardby. Насколько я понял, это еще один мигратор.
    К сожалению, очень кратко ознакомился с данным проектом. Основное отличие, которое бросается в глаза — там «миграции» пишутся на специальном языке.
    Если вы использовали Wizardby в реальных условиях, напишите мне. Было бы очень интересно узнать о его достоинствах/недостатках и сравнить его с Migrator.NET.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 46

      0
      Написано от души, спасибо за статью. И за ECM7
        0
        А Вам спасибо за проявленный интерес! ;)

        Сейчас есть идея относительно еще одного поста про мигратор. В ближайшее время постараюсь написать его.
          0
          некоторое время назад я написал небольшую библиотеку, которая на основе linq-прототипов «конструирует» БД и вносит изменения в неё. единственный недостаток пока — работает только с MS SQL. да и наверное всегда только с ней и работать будет. отсюда вопрос — интересно ли это хабралюдям, писать статью о библиотеке с примерами или нет?
            0
            Есть хороший способ проверить это — написать ;)
            0
            здравствуйте :) ну как там еще один пост про мигратор? :) или уже и не ждать?
              0
              ждите! обязательно будет!

              сейчас завал по работе… поэтому временная пауза.
                0
                меня тут как раз напрягли разобраться с мигратором (после вашей статьи кстати) :))) так что новая была бы очень полезна :)
                  0
                  это был не упрек ))) а то я как-то странно написала ))
                    0
                    если у Вас появятся вопросы — Вы пишите… обязательно постараюсь помочь.
                      0
                      спасибо за предложение :) вполне возможно что понадобится ваша помощь :)
            0
            Спасибо за материал. Новые веяния в дотнете радуют больше других новостей.
              0
              Спасибо! Добавлю в закладки чтоб не потерять. =)
                0
                Добавил себе в закладки, поскольку сейчас параллельно с основной веткой разработки нашего проекта на Delphi начата работа над .Net вариантом.

                К слову, наш проект работает с двумя базами: Oracle и Firebird. И если изменения в физической структуре БД Oracle почти всегда осуществимы, в худшем случае зависимые объекты станут (возможно, на время, до последующих команд миграции) инвалидными — это всегда можно поправить, перекомпилировав по окончании миграции основные схемы своей БД, то в Firebird все не так просто. Наличие зависимых объектов просто не позволит выполнить ряд операций (например, у нас предустматриваются по три view на каждую таблицу (для выборки, обновления и удаления, и уже на эти view раздаются права пользователям) и в подавляющем числе случаев выглядят как select * from ). В описанном мной случае нельзя будет поменять тип существующих полей таблицы, удалить существующие поля. Поэтому в нашей системе обновлений таблиц предусматривается полное устранения зависимостей перед началом операций и затем попытка их воссоздания (с перераздачей сохраненных перед удалением прав).

                Возможно, по мере развития вашего мигратора вам придется столкнуться с Firebird. Рад буду помочь!
                  0
                  Спасибо за интересный комментарий!

                  По поводу Firebird:
                  Доработки в ECM7.Migrator в основном выполнялись «для себя». На работе мы используем Oracle и MS SQL Server. Поэтому основные усилия были направлены на улучшение работы с этими СУБД. По этой же причине был удален провайдер для PostgreSQL — нет реальных проектов, использующих эту СУБД, мы не сможем нормально протестировать этот провайдер и не сможем его поддерживать.

                  И по этой же причине нет провайдера и для Firebird.
                  Если я столкнусь в работе с Firebird, то обязательно добавлю соответствующий провайдер и обращусь к Вам за помощью ;)

                  С другой стороны, если Вы захотите использовать Migrator при работе с Firebird, я готов создать для Вас «шаблон» нового диалекта, который вы сможете изменять на основе своего опыта. Также буду рад ответить на любые Ваши вопросы.
                  0
                  Некоторое время назад искал подобные вещи под .net-стек, но не смог найти, радует, что появились сейчас, т.к. это очень полезный контроль над структурой бд.
                    0
                    Что касается версионности БД в проектах на .NET, то у меня все обстояло следующим образом:

                    Сначала была просто табличка в БД, в которую каждый скрипт писал о своем выполнении. Польза — при ее просмотре можно было определить, что выполнялось, а что нет. Скрипты запускали руками через GUI (Management Studio).

                    Потом была написана программа с GUI, которая сравнивала номера скриптов в папке (номер из названия файла) с выполненными скриптами, а потом по очереди запускала не выполненные скрипты. Программа была довольно кривой и не универсальной. Работала только на MS SQL Server.

                    Далее была написана другая программа, на этот раз для проекта под Oracle. Она имела более четкую структуру, лучше парсила скрипты. GUI не было, зато имелось консольное приложение и таск для NAnt. Эта программа до сих пор используется на нескольких проектах (веб-приложения), в т.ч. при выкладки на «боевую» систему.

                    Собственно, после этого открыли для себя мигратор. Пока не спешим переводить все проекты на него, т.к. сейчас нет времени и старая программа выполнения скриптов более-менее устраивает. Но что-то новое пишем уже с использованием мигратора. Пока все очень нравится.
                    0
                    А почему нет диалекта для MS SQL?)
                      0
                      Есть. Просто автор написал вместо MS SQL — SQL Server :)
                        0
                        почему же?
                        все есть
                        >> В проекте Migrator.NET есть «диалекты» для следующих СУБД:
                        >> MySQL, Oracle, SQLite, SQL Server, PostgreSQL

                        сейчас исправлю на «MS SQL Server»
                        0
                        Сурово как-то, чтобы мигрировала БД нужно писать код, программу. Мне удобнее использовать мержилку для БД, которая сравнивает БД и генерит скрипты для перехода от dev версии к production(предположим, или от версии 1 к 2), с возможностью контроля, разумеется.
                          +1
                          Мержилка для БД — просто другой подход. У него свои достоинства и свои недостатки.

                          Пример недостатка — такой подход в результате формирует папку с большим количеством скриптов, которую менее удобно выполнять и распространять пользователям.

                          Про достоинства писать не буду — они очевидны.

                          Каждый выбирает тот подход, который ему больше нравится.
                            0
                            Кстати, вы возможно работали с мержилками, есть что-нибудь лучшее для Oracle, чем продукт от EMS?
                              0
                              К сожалению, большого опыта работы с мержилками у меня нет :(

                              Кратко смотрел программы от EMS для MS SQL Server и для Oracle. Посмотрел их очень кратко, т.к. не было времени. С другими подобными программами, к сожалению, не сталкивался.

                              Наверно это из за того, что привык другим способом работать с БД.
                              0
                              Нет, студийные утилиты из DataDude генерят один файл, который содержит все изменения. Еще там все завернуто в транзакции так, что ошибка в одном скрипте приведет к откату всех последующих.

                              Насчет удобства распространения пользователям — это да, не все наверное смогут выполнить SQL скрипт, более того если в бд были внесены конфликтующие изменения — сложнее отследить эту ситуацию ( хотя тут пользователь — ссзб).
                                0
                                Забыл добавить — студийный DataDude проводит верификацию всяких хранимых процедур и функций на предмет правильности вызовов — совпадения имен, параметров.
                                Т.е. если в вызове ХП будет использован столбец которого нт в БД — поднимется крик. Этакой компилятор для SQL структур.

                                А ваши продукты такое умеют?
                                  0
                                  Мигратор не умеет проверять корректность названий объектов БД.
                                  Действительно, для этого будет лучше выбрать студийные утилиты.

                                  Единственное, я не совсем понял вот это предложение:
                                  >> утилиты из DataDude генерят один файл, который содержит все изменения

                                  Я думаю, любая программа, генерирующая diff для двух БД может сохранить свои результаты в один общий файл. Проблема не в этом. Один общий файл выполняет перевод БД из состояния А в состояние Б. Как быть, если нужно перевести БД в состояние Б не из состояния А, а из какого-то промежуточного состояния В?

                                  Как вариант — сгенерировать еще один общий файл, переводящий базу из В в Б. Но нельзя же сгенерировать такие файлы для любой комбинации начального и конечного состояния.

                                  Второй вариант — иметь такие файлы, переводящие БД последовательно из состояния V в состояние V+1. В этом случае мы как раз и получаем папку с кучей скриптов.

                                  — И еще по поводу того, что все скрипты завернуты в транзакции. К сожалению, в Oracle это не поможет, т.к. Oracle выполняет каждую команду DDL в отдельной транзакции. Мне приходится работать с Oracle и его работа с транзакциями стала еще одной причиной, почему я использую мигратор.

                                  Если Вы работаете, например, с MS SQL Server, то вы находитесь в другой ситуации и свободны выбрать любое другое средство.
                                    0
                                    вот такой вот он особенный Оракл!
                                    на самом деле откат всегда можно сделать — собирать комманды уже выполненные, с сохранением состояний которые были, но здесь всегда возникнет проблема хранения таких данных, если например stored procedure можно как нибудь сохранить, то с удаляемыми столбцами сложнее — нужно искать Unique/Pk индекс и сохранять в отдельных таблицах данные, но возможно =)

                                    в любом случае такое усложнение логики может привести к невозможности использования утилиты как таковой.

                                    про один файл — как правило если продукт делается для конторы, то там есть свои DBA, которые ни в коем случае не дадут вам модифицировать структуру, так что генерировать один файл вариант. к тому же если приложение само при запуске обновляет базу данных необходимо задуматься о синхронизации нескольких экземпляров, что бы не накатить дважды. с другой стороны если вы делаете руками обновление базы, то в вашем приложении необходимо проверять всегда версию базы данных. то есть варианта только два файлик апдейта, и селф db checking.

                                    думаю все таки будущее за автоматическими обновлениями.
                                      0
                                      >> в любом случае такое усложнение логики может привести
                                      >> к невозможности использования утилиты как таковой.

                                      Вы правы. Мы тоже думали об этом и пришли к аналогичному мнению.

                                      Про один файл: в примере, который Вы привели, действительно лучше общий файл. В случае, когда нужно обновить кучу пользователей, у каждого из которых может быть своя версия БД, такой вариант не подойдет, т.к. мы не знаем заранее версию БД.

                                      Думаю, универсального решения нет и действия должны зависеть от каждой конкретной ситуации.
                                      0
                                      Результат работы диффа — сравнение базы и проекта\двух проектов\двух баз.
                                      Соответственно для баз или проект-база генерится скрипт (в виде одного файла) который при выполнении приводит target db к source Db (source project). Если надо перевести A->Б — это один скрипт, если надо В-> Б — это другой.
                                      Собственное если вы пишете на C# и есть 2008 студия — можете попробовать сами — создание проекта бд, импорт в него существующей базы и диффы (в том числе по данным) делаются достаточно просто. Но это конечно только для SQL Server =) Извините, MS-Stack.

                                      PS Я не стараюсь критиковать ваше решение, просто привел пример еще одного удачного решения проблем с проектом БД. Кстати можете позаимствовать оттуда идею верификации полей в ХП — это вроде реально сделать если есть интерфейс извлечения данных о структуре самой БД.
                                        0
                                        Полностью с Вами согласен. На счет диффов — я именно это и имел в виду. Мы с Вами говорили об одном и том же. Я так увлекся доказыванием своей точки зрения, что не заметил ее совпадение с Вашей.

                                        >> Я не стараюсь критиковать ваше решение, просто привел
                                        >> пример еще одного удачного решения проблем с проектом БД.

                                        Согласен с Вами. Вариантов очень много. Мигратор лишь один из них, причем далеко не в каждой ситуации его использование будет удачным решением проблемы.
                                0
                                Попиарюсь и я. Точнее, напомню. Принцип у Wizardby немного другой: специальный язык, с помощью которого описываются изменения схемы БД.
                                  0
                                  Ах ты ж ё-моё. Невнимательно прочел заключительный пассаж. Прошу прощения.
                                    0
                                    Раз уж Вы увидели этот пост, можете сравнить Ваш проект с проектом Migrator.NET?
                                    Готов ответить на любые Ваши вопросы.
                                      0
                                      Попробую что-то внятное сообразить. Все нижеследующее — мое субъективное мнение.

                                      Прежде всего, мне не нравится сама идея писать миграции в C#-коде: очень «неинтерактивный» получается цикл. Открыть в студии класс, написать миграции вверх-вниз, скомпилировать, запустить. У Wizardby как-то всё пошустрее: есть консоль, есть Notepad++. Там поправил, переключил, wizardby u/redo/down — и все.

                                      Второе: непонятна ситуация с назначением таймстемпов миграций. Руками его сочинять грустно; гораздо удобнее, когда само средство за тебя подставляет всё, что надо.

                                      Третье: синтаксис слегка побогаче (но тут сложнее — у вас-то вообще C#-код, который, очевидно, Тюринг-полный и делать там можно вообще все, что захочется). Алиасы типов, шаблоны таблиц, «вывод типов» и вывод названий столбцов.
                                        0
                                        Спасибо!

                                        А можете представить в виде таблицы?
                                        В строках критерии оценки, в столбцах — мигратор и wizardby.

                                        Если у Вас сейчас нет возможности, то я позже сам составлю такую. Очень интересно было бы сравнить.

                                          0
                                          Нет, сейчас абсолютно нет времени. Но если что — спрашивайте, буду рад ответить.
                                    +1
                                    круто! за идею мега плюс!

                                    есть только один момент пусть в версии V использовался столбец невычислимый f в таблице t, при переходе на V+1 он удаляется, в таком случае в методе down нужно предусматривать восстановление.
                                    тулза может сама архивировать такие данные, но с другой стороны получится куча мусора, и при добавлении кортежей в t при работе в версии V+1 мы не сможем создать невычислимое f, хотя — не часто откат проиходит в продакшене
                                      0
                                      На счет удаления и восстановления столбцов Вы правы. Тут единственный выход — аккуратно писать миграции и продумывать их работу в таких случаях.

                                      Еще вариант — делать резервные копии БД при переходе на следующую версию. Все-таки, мигратор вряд ли самостоятельно сможет обеспечить полноценную версионность БД с данными. Скорее он обеспечивает версионность структуры БД.

                                      По поводу идеи — очень рад, что Вам понравилось! Только идея не моя, я всего лишь принял участие в ее реализации.

                                      Если решите использовать мигратор, буду рад ответить на любые Ваши вопросы ;)
                                        0
                                        ну вопросы просто беглые как всегда
                                        1. он сохраняет каррент версию бд?
                                        2. Migration X всегда мигрирует с X-1 на X?
                                        3. как дела с транзакционностью? то есть наверное если что то упало, то нужно за собой почистить, и судя по интерфейсу об этом нужно вспомнить самому разработчику?
                                          0
                                          1. Если под словами «каррент версия» вы имеете в виду «номер каррент версии», то дело обстоит следующим образом: есть таблица (при отсутствии генерируется автоматом при первом запуске миграций), в которую записаны номера выполненных миграций. Максимальный номер — и есть номер текущей версии.

                                          2. Нет. Мигратор выполняет миграции в порядке возрастания номера версии, но не обязательно, чтобы они отличались на 1. На сайте оригинального проекта приводят пример, когда в качестве номера версии указывается дата создания миграции: 20081104133052 — для времени 13:30:52 4 ноября 2008 года (номер версии — Int64).

                                          3. Мигратор оборачивает каждую миграцию в транзакцию. К сожалению, это прокатывает не во всех СУБД. Например, Oracle при выполнении команд DDL каждую такую команду оборачивает в отдельную транзакцию, а если транзакция уже была открыта, она принудительно коммитится. В этом случае разработчику нужно внимательно следить за выполнением миграций. Как вариант — можно еще размещать в каждой миграции ровно по одной команде DDL. В этом случае будет много мелких классов, зато гарантированно будут откатываться изменения в случае ошибок.

                                          Вообще, чем меньше изменение в каждой отдельной миграции — тем лучше.
                                      0
                                      Спасибо за статью. Хотелось бы более тонко прочувствовать миграции на примерах диаграмм и графиков, но даже так вполне понятно.
                                        0
                                        Полностью согласен! Информация в графическом виде намного нагляднее.

                                        Единственное, поясните, пожалуйста, что именно вы хотели бы узнать из диаграмм. Если получится, постараюсь собрать статистику и нарисовать их.
                                          0
                                          Хочется видеть визуально как это мердженье работает, то есть например написованую схему 1, схему 2, какой код написан в обоих случаях, что реально происходит в момент «перехода», и т.д.
                                            0
                                            Есть идея записать скринкаст, в котором с нуля написать несколько миграций.
                                            Мне кажется, такой вариант будет еще более наглядным и будет восприниматься лучше, чем куча текста.

                                            Как появится свободное время — займусь этим.
                                        +1
                                        Интересно, а есть ли практическая польза от даунгрейда базы? Кроме общепрограммисткого «сделали, потому что можем».

                                        Если без даунгрейда, то простой набор SQL в отдельном файле выглядит явно проще и понятнее, чем такая вот штука. Причем версию можно тоже хранить в БД и каждый блок SQL предварять проверкой версии. Тогда просто берешь один файл и выполняешь его в базе.

                                        Ну и да, конечно, файл придется собрать ручками. Можно при помощи «сравнивалок».
                                          0
                                          Лично я использую возможность отката до предыдущих версий при пересоздании БД в модульных тестах. Кроме того, на предыдущем месте работы было несколько ситуаций, когда такая возможность очень бы пригодилась.

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

                                          — По поводу «простого набора SQL в отдельном файле» я уже писал предыдущим комментаторам.

                                          Мигратор, как и SQL-файл — просто разные решения одной проблемы. У них обоих есть свои достоинства и недостатки.

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

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

                                        Only users with full accounts can post comments. Log in, please.