Блог - Linux, программирование, Я!

PythonИспользование 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. Она хоть и более сложна для освоения, но и более мощная, поддерживает пуллинг соединений, более управляемая и рассчитана на такое использование.

  1. 2011-07-12 11:19:58 | #

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

    • 2011-07-12 11:47:44 | #

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

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

  2. Pavel
    2012-03-16 20:56:52 | #

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

    • 2012-03-16 23:32:33 | #

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

      • Pavel
        2012-03-16 23:40:29 | #

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

        • 2012-03-16 23:54:45 | #

          Хм, очень может быть, т.к. функцию 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)
              #...
          
                                        
    • 2012-03-16 23:39:53 | #

      Но на крайний случай можно создать отдельную модельку а в ManyToMany задать параметр through https://docs.djangoproject.com/en/1.3/ref/models/fields/#django.db.models.ManyToManyField.through

  3. alex
    2013-10-10 16:19:25 | #

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

    • 2013-10-10 17:42:58 | #

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