Использование Django ORM отдельно от Django

Понадобилось использовать Django ORM вне контекста самой Django. Конечно, проще и правильнее было бы использовать SQLAlchemy т.к. она рассчитана на такое использование, но мне нужно было написать паука, сохраняющего данные в БД и веб-интерфейс к этим данным. Веб-интерфейс точно решил делать на Django, а в пауке использовать те-же самые модели, чтобы не проделывать одну и ту же работу дважды. Т.к. паук был первым этапом работ а админка – вторым, появилась необходимость создавать таблицы из моделей без использования “syncdb”.

В качестве основы использовалкод модуля django-standalone и статью Django ORM без Джанги – эта статья практически полностью решает проблему, но есть и несколько недостатков:

  • Модели для создания таблиц нужно перечислять вручную
  • Не создаются внешние ключи
  • По – мелочи – нужно каждой модели вручную задавать app_label

Попытаемся исправить эти недостатки.

Сразу предупреждаю, что от необходимости устанавливать Django нас статья не освобождает. Так что первым шагом устанавливаем Django:

sudo pip install Django

Собственно, приведу сразу код с комментариями

#/usr/bin/env python
# -*- coding: utf8 -*-

from django.conf import settings
settings.configure(
    # задаем параметры подключения к БД
    DATABASES={
        'default': {
            'ENGINE': 'postgresql_psycopg2', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
            'NAME': '***', # Or path to database file if using sqlite3.
            'USER': '***', # Not used with sqlite3.
            'PASSWORD': '***', # Not used with sqlite3.
            'HOST': '****', # Set to empty string for localhost. Not used with sqlite3.
            'PORT': '****', # Set to empty string for default. Not used with sqlite3.
        }
    },
    # этот префикс будет добавляться к именам таблиц вместо имени приложения
    # его нужно указывать только при использовании в скрипте. При использовании в
    # Django этот параметр лучше убрать.
    STANDALONE_APP_NAME='some_prefix'
)

from django.db import models


class StandAloneMetaclass(models.base.ModelBase):
    """Метакласс, который задает app_label моделям используя
    значение из STANDALONE_APP_NAME"""
    def __new__(cls, name, bases, attrs):
        app_name = settings.STANDALONE_APP_NAME or None
        if app_name:
            if 'Meta' in attrs:
                if not hasattr(attrs['Meta'], 'app_label'):
                    # устанавливаем app_label только если он уже не указан в самой модели
                    setattr(attrs['Meta'], 'app_label', app_name)
        return super(StandAloneMetaclass, cls).__new__(cls, name, bases, attrs)


class BlogNote(models.Model):
    __metaclass__ = StandAloneMetaclass  # всем моделям нужно прописать этот метакласс
    text=models.TextField()
    # необходимо добавить атрибут Meta всем моделям для корректной работы метакласса
    class Meta:
        pass


class BlogComment(models.Model):
    __metaclass__ = StandAloneMetaclass
    text=models.TextField()
    note=models.ForeignKey(BlogNote)  # внешние ключи тоже работают

    class Meta:
        pass


def create_tables(tables):
    from django.db import connection, transaction
    from django.core.management.color import no_style
    pending_references = {}  # список внешних колючей, ожидающих создания связываемых таблиц
    seen_models = set()  # список созданных
    style = no_style()  # указывает, что не нужно раскрашивать синтаксис сгенерированного SQL
    for model in tables:  # проходим по списку моделей
        sql, references = connection.creation.sql_create_model(model, style) # генерируем SQL и объекты внешних ключей
        seen_models.add(model)
        for refto, refs in references.items():
            pending_references.setdefault(refto, []).extend(refs)
            if refto in seen_models:
                sql.extend(  # если все таблицы внешнего ключа существуют, то добавляем сам ключ
                    connection.creation.sql_for_pending_references(
                            refto,
                            style,
                            pending_references))
        sql.extend(
            connection.creation.sql_for_pending_references(
                model,
                style,
                pending_references))
        cursor = connection.cursor()
        for stmt in sql:  # выполняем запросы к БД
            print stmt
            try:
                cursor.execute(stmt)
            except Exception, e:
                print e
                pass
        print
    transaction.commit_unless_managed()  # завершаем транзакцию
    for model in tables:  # создаем индексы
        index_sql = connection.creation.sql_indexes_for_model(model, style)
        if index_sql:
            try:
                for stmt in index_sql:
                    print stmt
                    cursor.execute(stmt)
            except Exception, e:
                transaction.rollback_unless_managed()
            else:
                transaction.commit_unless_managed()


if __name__ == "__main__":
    # находим все классы Django моделей в локальной области видимости и создаем для них
    # таблицы в БД
    create_tables(model for model in locals().values() if type(model) == StandAloneMetaclass)

Большая часть содержимого create_table() скопировано из файла django/core/management/commands/syncdb.py. По сравнению с оригинальной статьей отпала необходимость вручную указывать для каждой модели app_label и вручную передавать список моделей для создания. Но появилась необходимость прописывать __metaclass__. Успокаивает то, что он один и тот же для всех классов. Ну и необходимо всем прописывать class Meta: pass. Можно это поправить, если создать базовый класс абстрактной модели с этими атрибутами и наследовать все модели от него.

Ну и общее впечатление – Django ORM не самое удачное решение для использования отдельно от  Django. Если есть возможность, стоит использовать более подходящие библиотеки, например SQLAlchemy. Она хоть и более сложна для освоения, но и более мощная, поддерживает пуллинг соединений, более управляемая и рассчитана на такое использование.

10 thoughts on “Использование Django ORM отдельно от Django

  1. Роман Ворушин

    Я для таких задач пишу django management command, которая либо запускается через cron, либо запускается при старте системы и в цикле что-то делает.

    Reply
    1. P.S. Post author

      Да, так несомненно в большинстве случаев так проще и правильнее. Просто в моем случае был веб-паук на движке Scrapy у которого свои management_команды/интерфейс_командной_строки и т.п. и, в завершение, – он работает поверх Twisted, который c Django ну никак не пересекается. В этом случае использовать в качестве точки входа джанговский manage.py было бы довольно затруднительно.

      Еще интересный момент в этом проекте – нужно было создать и поддерживать не одно подключение к БД а целый пул (10-15 одновременных соединений) т.к. данные приходят быстро, запросов к БД много и в EventLoop твистеда это уже не запихнешь. Расстроило то, что в Django ORM я не нашел никаких средств для управления пулом соединений, единственная возможность – это в каждом потоке заново выполнять
      import models
      тогда для каждого потока создается свое подключение.

      Reply
  2. Pavel

    Здравствуйте, всё бы хорошо, да только не работают ManyToManyField.
    взял сначала этот пример http://imbolc.name/2009/12/django-orm.html
    долго не мог понять куда девается ManyToManyField, потом ваш пример, тоже самое.. =(

    Reply
    1. P.S. Post author

      Хм, очень может быть кстати, т.к. ManyToMany создает отдельную таблицу.
      А каким образом не работает? Выдает ошибку? Если да то какую?

      Reply
      1. Pavel

        Да, как раз, не создаёт эти самые дополнительные таблицы.

        Reply
        1. P.S. Post author

          Хм, очень может быть, т.к. функцию create_tables я не проверял на ManyToMany связях. Попробуйте пока задавать отдельную модельку для связи. Я попробую найти решение, если получится – напишу.

          
          class Model12relation(models.Model):
              model1 = models.ForeignKey('Model1')
              model2 = models.ForeignKey('Model2')
          
          class Model1(models.Model):
              #...
          
          class Model2(models.Model):
              model1 = models.ManyToManyField(Model1, through=Model12relation)
              #...
          
          Reply
  3. alex

    есть же PEEWEE. Django ORM отдельно от джанго. Не требует никаких костылей, и все отлично работает.

    Reply
    1. P.S. Post author

      В первом же абзаце есть ответ на ваш комментарий.
      Но на всякий случай повторю:
      1) Если бы и решил использовать какую-то другую ORM вместо Django, то это была бы SQLAlchemy
      2) Решил использовать Django т.к. все модели уже были задекларированы с помощью Django ORM.
      3) Про peewee до вашего комментария ничего раньше не слышал. На вид – гораздо мощнее Django ORM, но пока будет не хватать библиотеки для миграции схемы.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *