?

Log in

No account? Create an account

Чт, 7 янв, 2016, 23:58
Django

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

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

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

Сам я всегда принадлежал (и принадлежу) первому лагерю, но интересно же посмотреть, как другие живут. Поэтому из любопытства поковырял немного Django — на StackOverflow периодически попадаются вопросы типа Как бы мне сделать то-то и то-то, имеющие понятный ответ на SQL.

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

Возьмем простой пример с поставками деталей:

Модель выглядит довольно изящно:

class Part(models.Model):
    pass

class Vendor(models.Model):
    name = models.CharField(max_length=100)
    parts = models.ManyToManyField(Part)

И превращается (в случае PostgreSQL), заменяя отношение многие-ко-многим на промежуточную таблицу, в такой примерно код:

BEGIN;
CREATE TABLE "db_part" ("id" serial NOT NULL PRIMARY KEY);
CREATE TABLE "db_vendor" ("id" serial NOT NULL PRIMARY KEY, "name" varchar(100) NOT NULL);
CREATE TABLE "db_vendor_parts" ("id" serial NOT NULL PRIMARY KEY, "vendor_id" integer NOT NULL, "part_id" integer NOT NULL);
ALTER TABLE "db_vendor_parts" ADD CONSTRAINT "db_vendor_parts_vendor_id_5de36e57_fk_db_vendor_id" FOREIGN KEY ("vendor_id") REFERENCES "db_vendor" ("id") DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "db_vendor_parts" ADD CONSTRAINT "db_vendor_parts_part_id_b4687c2a_fk_db_part_id" FOREIGN KEY ("part_id") REFERENCES "db_part" ("id") DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "db_vendor_parts" ADD CONSTRAINT "db_vendor_parts_vendor_id_d443052f_uniq" UNIQUE ("vendor_id", "part_id");
CREATE INDEX "db_vendor_parts_96b1f972" ON "db_vendor_parts" ("vendor_id");
CREATE INDEX "db_vendor_parts_b4e61b8d" ON "db_vendor_parts" ("part_id");
COMMIT;

Префикс db — это просто имя приложения (логичнее было бы использовать схемы, но ладно).

Здесь здорово то, что дальнейшие изменения модели можно автоматически превратить в скрипты (так называемые миграции) для изменения уже существующей структуры таблиц. А это значит, что модель можно осмысленно держать под версионным контролем.

Например, добавление имени в модель Part приводит к генерации таких команд:

BEGIN;
ALTER TABLE "db_part" ADD COLUMN "name" varchar(100) DEFAULT '' NOT NULL;
ALTER TABLE "db_part" ALTER COLUMN "name" DROP DEFAULT;
COMMIT;

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

  • Получить поставщиков, поставляющих хоть что-нибудь:

    Vendor.objects.filter(parts__isnull=False).distinct()
    

    API устроен так, что позволяет «гулять» по ссылкам. Встретив упоминание parts в контексте класса Vendor, механизм понимает, что db_vendor надо соединять с db_part. Таким образом, генерируется следующий SQL-код:

    SELECT DISTINCT "db_vendor"."id", "db_vendor"."name"
    FROM "db_vendor"
    INNER JOIN "db_vendor_parts"
    ON ("db_vendor"."id" = "db_vendor_parts"."vendor_id")
    WHERE "db_vendor_parts"."part_id" IS NOT NULL
    

  • Поставщики, не поставляющие ни одной детали:

    Vendor.objects.filter(parts__isnull=True)
    

    Здесь генератор догадывается, что для isnull=True нужно левое соединение:

    SELECT "db_vendor"."id", "db_vendor"."name"
    FROM "db_vendor"
    LEFT OUTER JOIN "db_vendor_parts"
    ON ("db_vendor"."id" = "db_vendor_parts"."vendor_id")
    WHERE "db_vendor_parts"."part_id" IS NULL
    

  • Поставщики, поставляющие деталь 15:

    Vendor.objects.filter(parts__id=15)
    

    Тут все очевидно:

    SELECT "db2_vendor"."id", "db2_vendor"."name"
    FROM "db2_vendor"
    INNER JOIN "db2_vendor_parts"
    ON ("db2_vendor"."id" = "db2_vendor_parts"."vendor_id")
    WHERE "db2_vendor_parts"."part_id" = 15
    

  • Поставщики, поставляющие все детали.

    А вот здесь все плохо. Простые запросы на Django писать легко (несмотря на диковатый синтаксис), но нетривиальное оказывается попросту невозможным. Не исключено, что я просто не сумел придумать запрос, но коллективный разум пока тоже не предложил ответа.

    Так или иначе, неполноценность языка запросов Django, кажется, ни у кого не вызывает сомнений.

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

  1. Независимость от СУБД.

    Но если диалекты SQL хоть как-то, да совместимы друг с другом, то уж язык запросов Django точно не совместим ни с чем. И вместо зависимости от СУБД разработчики получают зависимость от фреймворка. К чему это приведет? К тому, что пытаясь всеми силами избежать SQL, ограничения Django будут обходиться с помощью Питона. Избыточные запросы к базе, соединение таблиц в коде приложения, вот это вот все.

    Особенно забавно в этом контексте выглядит модуль django.contrib.postgresql, который таки привязывает проект к PostgreSQL.

  2. Абстрагирование от СУБД.

    В том смысле, что незачем разработчику на Питоне знать про базу данных и учить SQL. Но ведь все равно придется учить какой-то язык запросов. SQL не самый сложный язык на свете, знание его может пригодиться и для других задач. А знание языка запросов Django ни для чего, кроме собственно Django, не нужно.

    Можно посмотреть и с другой стороны. Как известно, все нетривиальные абстракции дырявы. Однажды к разработчикам придет недовольный администратор базы данных и спросит: А отчего вот такой-то запрос SQL выполняется десять тысяч раз подряд? А разработчики его даже не поймут, они же этот запрос в глаза не видали.

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

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

Пт, 8 янв, 2016 01:57 (UTC)
pigdeon

Твой вопрос про месторождение логики, несмотря на сугубо прикладной контекст, поднял муть из глубин философии: интерпретацию. В БД этот вопрос на фундаментальном уровне решен: заголовок таблицы задает интерпретацию колонок. Но в более широком смысле, например в случае абстрактной компьютерной программы со своими данными, возникают интересные эффекты. Программный код имеет фиксированную интерпретацию, заданную архитектурой процессора. Часть данных в этой программе также частично "предопределена" - это адресные операнды в командах исполнимого кода, но большая часть данных интерпретируется исключительно посредством самой программы. И вот эта интерпретация может меняться во времени.
К чему это я: вечными является не БД, а весь ансамбль БД+СУБД+приложения. И если мы считаем СУБД первичной, то БД+приложения логично держать поблизости, разве что у нас есть специальные причины так не делать.
Мне тут разонравился классичесий подход к реализации ИИ посредством базы данных, и я решил совершить переход к "программам" для повышения гибкости в организме, но пока не вполне понимаю как.

Пт, 8 янв, 2016 12:01 (UTC)
egorius

Эк ты закрутил... из глубин философии.
Мне нравится, когда интерпретация задана на уровне БД. Но в реальном мире она, увы, не ограничивается ни заголовком таблицы, ни ограничениями целостности. Тогда возникают где-то как-то описанные договоренности, что приложения должны интерпретировать данные так-то и так-то, иначе другие приложения поломаются (ну или начинаются пляски с уровнями абстракции_от). И в этом смысле получается, что приложения действительно примазываются к ансамблю вечности.

А насчет гибкости в организме... Данные легко менять; легче, чем программы. Неужели ты смотришь в сторону самомодифицирующегося кода?

Пт, 8 янв, 2016 12:23 (UTC)
hardsign

Самомодифицирующийся код - это как-то волосато звучит, а вот "генетические алгоритмы"...

Пт, 8 янв, 2016 12:39 (UTC)
egorius

Волосато, ага. Но генетические алгоритмы (как и нейронные сети), это просто некоторые фиксированные (пусть и хитрые) алгоритмы работы с данными. Базе данных они вроде как не противоречат.

Пт, 8 янв, 2016 15:49 (UTC)
pigdeon

Мне нравится ход ваших мыслей. Только не "самомодифицирующийся код", а поиск [решения] в пространстве алгоритмов. "Обычное" программирование к этому уже почти пришло через концепцию ядерных драйверов, плагинов и экстеншенов. Теперь "всего лишь" осталось научить программу саму порождать себе плагины и все заверте...

Пт, 8 янв, 2016 19:12 (UTC)
egorius

Ну искать решение, видимо, придется теми же генетическими алгоритмами, путем внесения изменений в то, что уже хоть как-то работает. Так что «самомодифицирующийся» — почти то же самое.

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

Пт, 8 янв, 2016 19:39 (UTC)
pigdeon

Самомодифицирующийся программно-аппаратный комплекс из программиста и компьютера. Вот и у меня та же фигня: я легко представляю себе проблему и построение методов ее решения, но практически не понимаю как именно все это происходит. Похоже, придется прокрутить всю эволюцию с-начала: "смысла"-то у нее тоже нет. Там и до формулировки главного вопроса рукой подать. И ответ уже есть - 42.

Пт, 8 янв, 2016 10:17 (UTC)
hardsign

А представь, было бы классно, чтобы абсолютно весь код содержался в БД. Или данные лежали бы в приложении, безо всякой БД. Собственно, к этому мы и идём :-)

Пт, 8 янв, 2016 12:18 (UTC)
egorius

Да фиг его знает, классно это или нет. На практике не очень приживается такая идея.
Вот, например, есть у Оракла возможность легким движением руки поднять веб-сервисы прямо внутри СУБД. Но коллеги из Яндекса, знающие толк_в, вовремя дали нам по рукам, и правильно. Слишком много архитектурных вопросов возникает к такой схеме (не говоря уже о хреновой оракловой реализации).
Или взять OEBS. Так всегда почти весь код был внутри базы. А в новой инкарнации ораклоиды решили унести код на уровень приложения, хотя казалось бы. (Правда, ни хрена эта конструкция не взлетела.)
А в идеологии Постгреса так и вообще управлением транзакциями должно заниматься приложение, и только оно. А хранимые процедуры фиксировать и откатывать изменения не могут.

А ты видишь какие-то предпосылки_к?

Пт, 8 янв, 2016 12:22 (UTC)
hardsign

Конечно.
Единственная причина, почему до сих пор существует деление на приложение и СУБД, - недостаточная масштабируемость СУБД плюс монолитная архитектура приложений. Когда народ в полной мере освоит микросервисную архитектуру - тут-то коммунизм и наступит. А горизонтально масштабируемые СУБД уже есть, это такой сросшийся класс продуктов IMDG/IMDB. Посмотри, например, Tarantool - офигительная же Весчъ™

Пт, 8 янв, 2016 13:10 (UTC)
egorius

А в каком месте Тарантул сросся с IMDB? Наверное, действительно надо посмотреть на него...

Пт, 8 янв, 2016 13:12 (UTC)
hardsign

Тарантул™ - самая что ни на есть IMDB. А ты как думал?

Пт, 8 янв, 2016 13:47 (UTC)
egorius

Мутная терминология, на самом деле. Буду посмотреть.

Пт, 8 янв, 2016 15:34 (UTC)
hardsign

Если хочешь поговорить за терминологию¤, так можно и пообедать сходить :)

Пт, 8 янв, 2016 19:02 (UTC)
egorius

Поддерживаю (: