Вопрос о том, держать ли логику работы с данными в базе или в приложении, делит мир на два лагеря.
В первом смотрят на систему и видят в ней данные. Не важно, какие приложения запускают в них свои грязные руки сегодня, ведь данные всех переживут. Если, конечно, как следует позаботиться об их целостности и сохранности.
Во втором - превыше всего приложение, чудесное, высокотехнологичное, в которое пользователи влюбляются с первого клика. Конечно, оно использует данные, так что следует вооружиться 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, кажется, ни у кого не вызывает сомнений.
Зачем же нужен такой язык, на котором можно сформулировать далеко не любой запрос? Ведь сколько сил было потрачено на реализацию, страшно подумать. Не просто же так? Вроде бы есть два аргумента, но оба не выдерживают критики с моей точки зрения:
Независимость от СУБД.
Но если диалекты SQL хоть как-то, да совместимы друг с другом, то уж язык запросов Django точно не совместим ни с чем. И вместо зависимости от СУБД разработчики получают зависимость от фреймворка. К чему это приведет? К тому, что пытаясь всеми силами избежать SQL, ограничения Django будут обходиться с помощью Питона. Избыточные запросы к базе, соединение таблиц в коде приложения, вот это вот все.
Особенно забавно в этом контексте выглядит модуль django.contrib.postgresql, который таки привязывает проект к PostgreSQL.
Абстрагирование от СУБД.
В том смысле, что незачем разработчику на Питоне знать про базу данных и учить SQL. Но ведь все равно придется учить какой-то язык запросов. SQL не самый сложный язык на свете, знание его может пригодиться и для других задач. А знание языка запросов Django ни для чего, кроме собственно Django, не нужно.
Можно посмотреть и с другой стороны. Как известно, все нетривиальные абстракции дырявы. Однажды к разработчикам придет недовольный администратор базы данных и спросит: А отчего вот такой-то запрос SQL выполняется десять тысяч раз подряд? А разработчики его даже не поймут, они же этот запрос в глаза не видали.
Спрашивается, не проще ли научиться писать запросы SQL, и - если уж необходимо - делать это так, чтобы можно было перейти на другую СУБД без титанических усилий? К слову, Django позволяет вполне удобно работать с SQL-запросами.
Еще кажется, что могут помочь представления, за которыми легко скрыть сложную логику независимым от СУБД образом. Но ведь это уже прямая дорога из второго лагеря в первый?