Mar 15, 2014

Перенос БД с sqlite на postgres

ORM позволяет быстро переключатся между БД не учитывая их диалект (практически). Но данные хранятся физически в разных местах и естественно их надо переносить, например при переключении с sqlite на PostgreSQL. В Django есть встроенный функционал в виде:

# Выгрузка в JSON
$ python manage.py dumpdata myapp.A > a.json

# Загрузка из JSON
$ python manage.py loaddata a.json

Т.е. мы выгружаем данные из sqlite в JSON формат, затем меняем строку подключения на postgres и выполняем загрузку из JSON. Очень удобно, но почему то этот метод не работает, либо работает только при переносе из sqlite -> sqlite, что в принципе не очень интересно, точнее бессмысленно. Есть какие то решений с бубном как это вот:

Эти методы не универсальны, потому что имеют привязку к моделям (ORM), требуют для переноса проект на Django и ручные действия вроде создания схемы и выполнения миграций (далеко не всегда миграции созданы правильно).

Я написал небольшой пример как можно перевести данные не имея фреймворков, не привязываясь к моделям, указав только две строки подключения откуда переносить и куда (вроде было что то похожее на руби). За основы взят пример из этой статьи http://www.tylerlesmann.com/2009/apr/27/copying-databases-across-platforms-sqlalchemy/. Где предлагается указать дополнительно названия таблиц.

В sqlalchemy с весии 9.1 появилась встроенная возможность автоматического определения схемы БД http://docs.sqlalchemy.org/en/rel_0_9/orm/extensions/automap.html. Правда намного раньше появились сторонние решения:

Первое что мы сделаем, это получим схему БД:

from sqlalchemy.ext.automap import automap_base

def get_metadata(self, engine):
   # produce our own MetaData object
   metadata = MetaData()

   # we can reflect it ourselves from a database, using options
   # such as 'only' to limit what tables we look at...
   # only = ['news_news', 'pages_page']
   if self.only:
       metadata.reflect(engine, only=self.only)
   else:
       metadata.reflect(engine)

   # we can then produce a set of mappings from this MetaData.
   Base = automap_base(metadata=metadata)

   # calling prepare() just sets up mapped classes and relationships.
   Base.prepare()
   return metadata, Base

Дальше получаем все таблицы:

tables = Base.classes

Создаем такую же структуру БД в postgres:

metadata.create_all(self.engine_dst)

По очереди проходим каждую таблицу и переносим из нее данные в новую БД:

for table in tables:
    columns = table.__table__.c.keys()
    print 'Transferring records to %s' % table.__table__.name
    for record in self.session.query(table).all():
        data = dict(
            [(str(column), getattr(record, column)) for column in columns]
        )
        NewRecord = quick_mapper(table.__table__)
        self.session_dst.merge(NewRecord(**data))
        self.session_dst.commit()

Если возникли конфликты можно их решить переопределением типов полей. Например при переносе из sqlite в postgres тип полей DATETIME нужно заменить на DateTime:

dialect = self.engine.dialect.name
dialect_dst = self.engine_dst.dialect.name
if dialect == dialect_dst:
    return
for table in self.tables:
    columns = table.__table__.c
    for column in columns:
        if dialect_dst == 'postgresql':
            # DATETIME->DateTime
            if isinstance(column.type, DATETIME):
                column.type = DateTime()

Пример запуска:

$ python convertdbdata.py -f "sqlite:///fromMydb.sqlite" -t "postgresql://postgres:postgres@localhost/toMydb" -i "auth_user,news_news"

-i параметр указывает какие таблицы нужно запускать первыми, например в такой ситуации:

DETAIL:  Ключ (user_id)=(1) отсутствует в таблице "auth_user".
 'INSERT INTO django_admin_log (id, action_time, user_id, content_type_id, object_id, object_repr, action_flag,
 change_message) VALUES (%(id)s, %(action_time)s, %(user_id)s, %(content_type_id)s, %(object_id)s, %(object_repr)s,
 %(action_flag)s, %(change_message)s)' {'action_flag': 1, 'action_time': datetime.datetime(2014, 2, 5, 13, 15, 27, 948000),
 'user_id': 1, 'content_type_id': 39, 'object_repr': u'dfgsdfg', 'object_id': u'1', 'change_message': u'', 'id': 1}

Код полностью https://github.com/ITCase/convertdbdata

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


Comments

comments powered by Disqus