Русский
Русский
English
Статистика
Реклама

Pyhon

Перевод Красивая и подробная геологическая карта Марса, сделанная на Python, GDAL

14.06.2020 18:20:27 | Автор: admin
image

На этой неделе творческое переосмысление геологической (или все же ареологической?) карты Марса на основе карты, сделанной USCS. Использовалась те же геологические данные, что и для оригинальной, но было добавлено больше топонимов и подписей, проведен редизайн визуального стиля, а термины из легенды карты упрощены для общего понимания.



Одной из самых сложных частей проекта было сделать перевод условных обозначений на язык не специалистов. Оригинальная карта была сделана для геологов, поэтому большую часть вокабуляра пришлось пересмотреть. Например, новая расшифровка значения края кальдеры Граница пустой магматической камеры, оставшейся после извержения вулкана. В оригинале было Овоидный бугорок, очерчивающий одну или несколько слитых частично или полностью закрытых впадин; коллапс вулкана, связанный с извержениями и, возможно, взрывными извержениями.

Во многих случаях переведенные обозначения были более неточными или менее информативными, чем оригинальные, поэтому также включили оригинальные аббревиатуры для каждого типа геологического элемента. Эти метки могут содержать ссылки на исходные данные, если вдруг захочется узнать больше о каждом типе геологического образования в научных терминах.

image
Некоторые из особо-примечательных геологических объектов на Марсе. 1: Гора Олимп, самый большой вулкан в Солнечной системе. 2: Долины Маринер, система глубоких каньонов длиной более 4000 км. 3: Равнина Эллада, самый большой видимый ударный кратер в Солнечной системе. 4: Марс геологически разделен на Северную низменность (бледно-зеленую) и Южную горную местность (коричневая). Ударные кратеры, образованные падающими астероидами и кометами (неоново-желтые), разбросаны по всей планете.

image
Для карты использовались следующие отдельные слои. 1-3: Геологические элементы, геологические контакты и геологические особенности из набора данных USGS. 4-5: уровень наклона и из двух источников USGS. 6: Номенклатура от IAU (Международный Астрономический Союз). 7-8: линии сетки и 3D-эффект, разработанный в Photoshop.

Много времени ушло на выбор проекции для проекта. Чтобы точно показать, какая часть планеты состояла из каждого геологического образования, в итоге использовалась проекция Эккерта IV. Этот тип проекции карты искажает контуры объекта, но сохраняет относительную площадь фигур по всей поверхности планеты. Такая проекция не подходит для визуализации полярных регионов, поэтому также добавили четыре карты-врезки в углы, чтобы показать каждое полушарие Марса (северное, южное, восточное и западное).

Чтобы сравнить эти различные проекции карты, вы можете использовать индикатрису Тиссо набор кругов одинакового размера, нанесенных в разных местах земного шара. Все проекции карты искажают пространство, но вы можете видеть, что эффекты сильно различаются в зависимости от проекции.

image
1: Равнопромежуточная. 2: Экерт IV. 3-5: Ортографические проекции с центром на разных долготах и широтах


Рассказ о реализации



В данном проекте использовались открытые данные USGS, IAU и NASA.
Стек технологий: Python 3.7.1, GDAL 2.4.1, Illustrator CC 2019 и Photoshop CC 2019 (можно заменить на бесплатные Gimp и Inkscape, например).
Также нужно установить пакеты: matplotlib numpy, pandas, cartopy, jupyter.


Сбор и обработка данных



Создание модели рельефа (DEM)



Модель рельефа это данные, содержащие информацию о высоте различных точек на планете. В проекте использовались данные, полученные из Геологической Службы США. Каждый пиксель в файле GeoTIFF это 16-битное число, которое описывает высоту точки.

Примечание: многие программы не могут нормально прочитать этот файл, так что то, что он выглядит странно в формате предпросмотра или в других неспециализированных программах это нормально. В рамках проекта работа с ними шла через GDAL (англ. Geospatial Data Abstraction Library библиотека абстракции гео-пространственных данных).

Создание проекций карты



Как уже говорили выше, для основной карты использовали проекцию Эккерт IV, которая сохраняет неизменной площадь, а для врезанных карт полюсов использовалась ортографическая проекция.

Изменяем проекцию в DEM-файле: в исходном файле использовалась равнопромежуточная проекция, поэтому нужно ввести следующий код в командную строку, чтобы осуществить перевод. Код использует оригинальный файл intif и создает новый outtif, в формате eck4 (Эккерт IV) проекции.

gdalwarp -t_srs "+proj=eck4" ./path_to_intif.tif ./path_to_outtif.tif


Потом уменьшаем разрешение DEM, просто сокращая размер каждого пикселя до 1500x1500 метров, используя метод average. Это позволит сократить время обработки, да и уменьшение разрешения в этот момент сделать проще, чем потом.

gdalwarp -tr 1500 1500 -r average ./path_to_intif.tif ./path_to_outtif.tif


Отмывка и карта склонов



Дальше мы используем получившийся файл для создания карты с отмывкой и карты склонов для каждого полушария планеты.

Карта после отмывки показывает тени из воображаемого источника света, свет падает на карту сверху. Он воображаемый, потому что в реальном мире так не бывает одиночный источник света создаст тени под разными углами в разных частях шара. Встроенная функция hillshade в GDAL устанавливает угол падения света одинаковый для всей карты. Для данного проекта z, вертикальное увеличение, поставили равным 20. Это увеличивает каждое значение высоты в 20 раз, чтобы сделать рельеф более контрастным и обеспечить отображение теней.

gdaldem hillshade -z 20 ./path_to_intif.tif ./path_to_hillshade.tif


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

gdaldem slope ./path_to_intif.tif ./path_to_slope.tif

image

Ортографические проекции



В дополнение к карте в проекции Эккерт IV также сделаны четыре карты полушарий Марса. Проекция Эккерт IV плохо подходит для отображения Северного и Южного полюсов, поэтому эти две врезки очень полезны для их понимания. Для получения отмывки и карты склонов повторяем код с небольшими изменениями ortho вместо eck4 и обозначаем центр долготы и широты для каждой карты (North:+lat_0=90 +lon_0=90, South :+lat_0=-90 +lon_0=90, East:+lat_0=0 +lon_0=90, West:+lat_0=0 +lon_0=270).

gdalwarp -t_srs "+proj=ortho +lat_0=90 +lon_0=0" ./path_to_intif.tif ./path_to_outtif.tif
gdalwarp -tr 1500 1500 -r average ./path_to_intif.tif ./path_to_outtif.tif
gdaldem hillshade -z 20 ./path_to_intif.tif ./path_to_hillshade.tif
gdaldem slope ./path_to_intif.tif ./path_to_slope.tif

image

Легенда карты


Международный Астрономический Союз отвечает за присвоение имен внеземным объектам. Можно просто скачать файл в формате CSV, содержащий все объекты для каждой планеты прямо с их сайта. Для этого надо использовать функцию Advanced Search (расширенного поиска), чтобы скачать All Feature Types (все типы объектов) для вашей планеты Target (Mars), но только с Approval Status of Adopted by IAU одобренные МАС. В разделе Columns to Include section (столбцы для включения) выберите Feature ID, Feature Name, Clean Feature Name, Target, Diameter, Center Lat/Lon, Feature Type и Feature Type Code. Также можно включить Origin (происхождение), если хотите добавить в проект дополнительную информацию об объектах, такую как, например, в честь кого он был назван.

Геологические структуры и элементы


Геологическая карта показывает различные типы пород и другие особенности, такие как линии разломов и русла рек. У USGS есть красивая геологическая карта Марса, которую мы и используем в качестве исходной. Для работы с этими данными загрузите архив базы данных (790 МБ) с USGS Geologic Map. В следующем разделе, посвященному дизайну карты в Python, объясняется, как получить доступ и отобразить каждый тип данных в этой базе данных.

Дизайн карты в Python



Мы создаем шесть чертежей (plots) с геологическими элементами, структурами, особенностями, двумя видами текстовых подписей и сеткой. Часто стоит разделять данные для обработки, чтобы легко применять при необходимости эффекты из Photoshop или Illustrator. В matplotlib использовался gridspec, чтобы настроить все элементы так, чтобы каждый subplot занимал конкретное место в пределах декоративной рамки.

image

Геологические элементы это различные виды рельефа, составляющие поверхность планеты. Разные скальные элементы обычно представлены разными цветами. Набор данных USGS помечает каждую породу 23 буквенным кодом, обозначающим тип элемента. Назначаем цвет каждому буквенному коду, составив таблицу пар код-цвет в Mars_geologic_units.csv. Мы обращаемся к этому файлу при построении каждого элемента в cartopy и matplotlib. Сохранение графических параметров в отдельном файле облегчает использование различных цветовых схем и обособляет дизайн от кода.

image

Геологические контакты это границы между геологическими породами. Некоторые геологические границы являются приблизительными или скрыты под пылью. Как и для геологических пород, используем файл конфигурации Mars_geologic_boundaries.csv, чтобы отобразить каждый тип геологического контакта в другом цвете. На окончательной карте настраиваем отображение некоторых геологических контактов в виде пунктирных линий, просто открыв PDF в Illustrator и выбрав все объекты одного цвета (Select -> Same -> Stroke color).

image

Геологические особенности это другие видимые линии на поверхности планеты, такие как каналы, края кратеров или гребни гор. В датасете есть 13 различных типов. Чтобы отобразить каждый из них в особом стиле, каждый отобразили в другом цвете, а затем стилизовали линии в Illustrator. Это позволило применить специальных эффекты, такие как градиентные хвосты, чтобы показать направление движения каналов оттока и лавовых потоков.

image

Условные обозначения



Для карты использовалось два набора данных для меток. Первый это официальный от IAU. Размеры меток создаются в соответствии с размером объекта, хотя все равно финальную настройку размеров необходимо делать в Illustarator.

image

Второй набор меток добавляет для каждой крупного геологического элемента аббревиатуру. Это облегчает пользование легендой карты, поскольку некоторые цвета изменяются из-за теней. К коду здесь пришлось подойти творчески, а именно нарисовать каждую аббревиатуру в геометрическом центре геологической формы, что не для всех случаев идеально. Но для данного проекта это было нормально, поскольку потом по плану все равн эти подписи будут двигаться вручную, чтобы избежать наложения на будущие подписи.

image

Места высадки: чтобы отобразить места высадки марсоходов и других космических аппаратов, использовалось улучшенное цифровое изображение от NASA в качестве фона. Чтобы показывать все 4 полушария планеты, пришлось поменять проекцию landing_sites.ipynb.

Сохранение результатов из Matplotlib



Обычно результаты сохраняются в формате pdf, чтобы провести финальные правки текста и форм в Illustrator. Вот пара стандартных команд, которые облегчают последующую редактуруt:

import matplotlibimport matplotlib.pyplot as pltimport matplotlib.backends.backend_pdf as pdf# Выгружает текст в редактируемом формате, а не в формате формы:matplotlib.rcParams['pdf.fonttype'] = 42# Сохраняет вертикальное положение для картинокmatplotlib.rcParams['image.composite_image'] = False# Удаляет границы и тики из subplotax.axis('off')# Убирает padding и margins отовсюдуplt.margins(0,0)plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0)plt.gca().xaxis.set_major_locator(plt.NullLocator())plt.gca().yaxis.set_major_locator(plt.NullLocator())#Сохраняет в pdfpp = pdf.PdfPages('./savename.pdf', keep_empty=False)pp.savefig(fig)pp.close()# если не нужно сохранять в векторах# можно делать сразу в PNG и правит сразу в Photoshop:plt.savefig('./savename.png', format='png', dpi=600, pad_inches=0, transparent=True)


После сохранения PDF его нужно отредактировать так, чтобы каждый объект можно было редактировать независимо. В Illustrator выберите все в файле и идите в Object --> Clipping Mask --> Release. В этот момент стоит также удалить бэкграунд и границы, если вы их делали раньше.

Дизайн в Illustrator и Photoshop



Экспортирование из Python в PDF позволяет использовать некоторые функции графических редакторов, которых или нет, или их реализация гораздо более трудозатратна.
В этом проекте не так много особых текстовых эффектов, так что редактирование было минимально.

Создание теней под текстом в Photoshop


Для создания этого эффекта нужно скопировать слой с подписями и зайти в Filter --> Blur Gallery --> Field Blur. Для теней хорошо создавать два слоя с blur на 20% прозрачности один с Blur равным 4px, а другой 10px.

Цвета и шрифты


Чтобы карты выглядела продумано, для карты выбирались цвета из заранее продуманной палитры на 70 цветов. Два шрифта (Redflowers and Moon).
image
image

Разработка цветовой схемы


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

Декоративные иллюстрации


В проекте хотелось комбинировать большие датасеты и ручные элементы в стиле художников William Morris или Alphonse Mucha. Для этого перед разработкой была собрана большая коллекция картин для вдохновения.

image

На старте хотелось попробовать разные вариации рамок для карты. Была создана коллекция из 18 разных паттернов.

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

Итоговый результат:
image

Ссылка на github: github.com/eleanorlutz/mars_geology_atlas_of_space

Реферальные ссылки:
Astronomy. Andrew Fraknoi, David Morrison, Sidney C. Wolff et al. OpenStax 2016.
Gazetteer of Planetary Nomenclature. International Astronomical Union (IAU) Working Group for Planetary System Nomenclature (WGPSN) 2019.
Planetary Symbology Mapping Guidelines. Federal Geographic Data Committee.
Mars HRSC MOLA Blended DEM Global 200m v2. NASA PDS and Derived Products Annex. USGS Astrogeology Science Center 2018.
Geologic Map of Mars SIM 3292. Kenneth L. Tanaka, James A. Skinner, Jr., James M. Dohm, Rossman P. Irwin, III, Eric J. Kolb, Corey M. Fortezzo, Thomas Platz, Gregory G. Michael, and Trent M. Hare. USGS 2014.
Viking Global Color Mosaic 925m v1. NASA PDS, 2019
Missions to Mars. The Planetary Society.
Fonts: Moon by Jack Harvatt and RedFlower by Type & Studio.
Advice: Thank you to Henrik Hargitai, Oliver Fraser, Thomas Mohren, Chris Liu, Chloe Pursey, and Leah Willey for their helpful advice in making this map.
Подробнее..

SQLAlchemy а ведь раньше я презирал ORM

05.06.2021 22:17:23 | Автор: admin

Так вышло, что на заре моей карьеры в IT меня покусал Oracle -- тогда я ещё не знал ни одной ORM, но уже шпарил SQL и знал, насколько огромны возможности БД.

Знакомство с DjangoORM ввело меня в глубокую фрустрацию. Вместо возможностей -- хрена с два, а не составной первичный ключ или оконные функции. Специфические фичи БД проще забыть. Добивало то, что по цене нулевой гибкости мне продавали падение же производительности -- сборка ORM-запроса не бесплатная. Ну и вишенка на торте -- в дополнение к синтаксису SQL надо знать ещё и синтаксис ORM, который этот SQL сгенерирует. Недостатки, которые я купил за дополнительную когнитивную нагрузку -- вот уж где достижение индустрии. Поэтому я всерьёз считал, что без ORM проще, гибче и в разы производительнее -- ведь у вас в руках все возможности БД.

Так вот, эта история с SQLAlchemy -- счастливая история о том, как я заново открыл для себя ORM. В этой статье я расскажу, как я вообще докатился до такой жизни, о некоторых подводных камнях SQLAlchemy, и под конец перейду к тому, что вызвало у меня бурный восторг, которым попытаюсь с вами поделиться.

Опыт и как результат субъективная система взглядов

Я занимался оптимизацией SQL-запросов. Мне удавалось добиться стократного и более уменьшения cost запросов, в основном для Oracle и Firebird. Я проводил исследования, экспериментировал с индексами. Я видел в жизни много схем БД: среди них были как некоторое дерьмо, так и продуманные гибкие и расширяемые инженерные решения.

Этот опыт сформировал у меня систему взглядов касательно БД:

  • ORM не позволяет забыть о проектировании БД, если вы не хотите завтра похоронить проект

  • Переносимость -- миф, а не аргумент:

    • Если ваш проект работает с postgres через ORM, то вы на локальной машине разворачиваете в докере postgres, а не работаете с sqlite

    • Вы часто сталкивались с переходом на другую БД? Не пишите только "Однажды мы решили переехать..." -- это было однажды. Если же это происходит часто или заявленная фича, оправданная разными условиями эксплуатации у разных ваших клиентов -- милости прошу в обсуждения

    • У разных БД свои преимущества и болячки, всё это обусловлено разными структурами данных и разными инженерными решениями. И если при написании приложения мы используем верхний мозг, мы пытаемся избежать этих болячек. Тема глубокая, и рассмотрена в циклах лекций Базы данных для программиста и Транзакции от Владимира Кузнецова

  • Структура таблиц определяется вашими данными, а не ограничениями вашей ORM

Естественно, я ещё и код вне БД писал, и касательно этого кода у меня тоже сформировалась система взглядов:

  • Контроллер должен быть тонким, а лучший код -- это тот код, которого нет. Код ORM -- это часть контроллера. И если код контроллера спрятан в библиотеку, это не значит, что он стал тонким -- он всё равно исполняется

  • Контроллер, выполняющий за один сеанс много обращений к БД -- это очень тонкий лёд

  • Я избегаю повсеместного использования ActiveRecord -- это верный способ как работать с неконсистентными данными, так и незаметно для себя сгенерировать бесконтрольное множество обращений к БД

  • Оптимизация работы с БД сводится к тому, что мы не читаем лишние данные. Есть смысл запросить только интересующий нас список колонок

  • Часть данных фронт всё равно запрашивает при инициализации. Чаще всего это категории. В таких случаях нам достаточно отдать только id

  • Отладка всех новых запросов ORM обязательна. Всегда надо проверять, что там ORM высрала (тут пара сочных примеров), дабы не было круглых глаз. Даже при написании этой статьи у меня был косяк как раз по этому пункту

Идея сокращения по возможности количества выполняемого кода в контроллере приводит меня к тому, что проще всего возиться не с сущностями, а сразу запросить из БД в нужном виде данные, а выхлоп можно сразу отдать сериализатору JSON.

Все вопросы данной статьи происходят из моего опыта и системы взглядов

Они могут и не найти в вас отголоска, и это нормально

Мы разные, и у нас всех разный фокус внимания. Я общался с разными разработчиками. Я видел разные позиции, от "да не всё ли равно, что там происходит? Работает же" до "я художник, у меня справка есть". При этом у некоторых из них были другие сильные стороны. Различие позиций -- это нормально. Невозможно фокусироваться на всех аспектах одновременно.

Мне, например, с большего без разницы, как по итогу фронт визуализирует данные, хотя я как бы фулстэк. Чем я отличаюсь от "да не всё ли равно, что там происходит"? Протокол? Да! Стратегия и оптимизация рендеринга? Да! Упороться в WebGL? Да! А что по итогу на экране -- пофиг.

Знакомство в SQLAlchemy

Первое, что бросилось в глаза -- возможность писать DML-запросы в стиле SQL, но в синтаксисе python:

order_id = bindparam('order_id', required=True)return \    select(        func.count(Product.id).label("product_count"),        func.sum(Product.price).label("order_price"),        Customer.name,    )\    .select_from(Order)\    .join(        Product,        onclause=(Product.id == Order.product_id),    )\    .join(        Customer,        onclause=(Customer.id == Order.customer_id),    )\    .where(        Order.id == order_id,    )\    .group_by(        Order.id,    )\    .order_by(        Product.id.desc(),    )

Этим примером кода я хочу сказать, что ORM не пытается изобрести свои критерии, вместо этого она пытается дать нечто, максимально похожее на SQL. К сожалению, я заменил реальный фрагмент ORM-запроса текущего проекта, ибо NDA. Пример крайне примитивен -- он даже без подзапросов. Кажется, в моём текущем проекте таких запросов единицы.

Естественно, я сразу стал искать, как тут дела с составными первичными ключами -- и они есть! И оконные функции, и CTE, и явный JOIN, и много чего ещё! Для особо тяжёлых случаев можно даже впердолить SQL хинты! Дальнейшее погружение продолжает радовать: я не сталкивался ни с одним вопросом, который решить было невозможно из-за архитектурных ограничений. Правда, некоторые свои вопросы я решал через monkey-patching.

Производительность

Насколько крутым и гибким бы ни было API, краеугольным камнем является вопрос производительности. Сегодня вам может и хватит 10 rps, а завтра вы пытаетесь масштабироваться, и если затык в БД -- поздравляю, вы мертвы.

Производительность query builder в SQLAlchemy оставляет желать лучшего. Благо, это уровень приложения, и тут масштабирование вас спасёт. Но можно ли это как-то обойти? Можно ли как-то нивелировать низкую производительность query builder? Нет, серьёзно, какой смысл тратить мощности ради увеличения энтропии Вселенной?

В принципе, нам на python не привыкать искать обходные пути: например, python непригоден для реализации числодробилок, поэтому вычисления принято выкидывать в сишные либы.

Для SQLAlchemy тоже есть обходные пути, и их сразу два, и оба сводятся к кэшированию по разным стратегиям. Первый -- применение bindparam и lru_cache. Второй предлагает документация -- future_select. Рассмотрим их преимущества и недостатки.

bindparam + lru_cache

Это самое простое и при этом самое производительное решение. Мы покупаем производительность по цене памяти -- просто кэшируем собранный объект запроса, который в себе кэширует отрендеренный запрос. Это выгодно до тех пор, пока нам не грозит комбинаторный взрыв, то есть пока число вариаций запроса находится в разумных пределах. В своём проекте в большинстве представлений я использую именно этот подход. Для удобства я применяю декоратор cached_classmethod, реализующий композицию декораторов classmethod и lru_cache:

from functools import lru_cachedef cached_classmethod(target):    cache = lru_cache(maxsize=None)    cached = cache(target)    cached = classmethod(cached)    return cached

Для статических представлений тут всё понятно -- функция, создающая ORM-запрос не должна принимать параметров. Для динамических представлений можно добавить аргументы функции. Так как lru_cache под капотом использует dict, аргументы должны быть хешируемыми. Я остановился на варианте, когда функция-обработчик запроса генерирует "сводку" запроса и параметры, передаваемые в сгенерированный запрос во время непосредственно исполнения. "Сводка" запроса реализует что-то типа плана ORM-запроса, на основании которой генерируется сам объект запроса -- это хешируемый инстанс frozenset, который в моём примере называется query_params:

class BaseViewMixin:    def build_query_plan(self):        self.query_kwargs = {}        self.query_params = frozenset()    async def main(self):        self.build_query_plan()        query = self.query(self.query_params)        async with BaseModel.session() as session:            respone = await session.execute(                query,                self.query_kwargs,            )            mappings = respone.mappings()        return self.serialize(mappings)
Некоторое пояснение по query_params и query_kwargs

В простейшем случае query_params можно получить, просто преобразовав ключи query_kwargs во frozenset. Обращаю ваше внимание, что это не всегда справедливо: флаги в query_params запросто могут поменять сам SQL-запрос при неизменных query_kwargs.

На всякий случай предупреждаю: не стоит слепо копировать код. Разберитесь с ним, адаптируйте под свой проект. Даже у меня данный код на самом деле выглядит немного иначе, он намеренно упрощён, из него выкинуты некоторые несущественные детали.

Сколько же памяти я заплатил за это? А немного. На все вариации запросов я расходую не более мегабайта.

future_select

В отличие от дубового первого варианта, future_select кэширует куски SQL-запросов, из которых итоговый запрос собирается очень быстро. Всем хорош вариант: и высокая производительность, и низкое потребление памяти. Читать такой код сложно, сопровождать дико:

stmt = lambdas.lambda_stmt(lambda: future_select(Customer))stmt += lambda s: s.where(Customer.id == id_)

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

Наброски фасада, решающего проблему дикого синтаксиса

По идее, future_select через FutureSelectWrapper можно пользоваться почти как старым select, что нивелирует дикий синтаксис:

class FutureSelectWrapper:    def __init__(self, clause):        self.stmt = lambdas.lambda_stmt(            lambda: future_select(clause)        )        def __getattribute__(self, name):        def outer(clause):            def inner(s):                callback = getattr(s, name)                return callback(clause)                        self.stmt += inner            return self        return outer

Я обращаю ваше внимание, что это лишь наброски. Я их ни разу не запускал. Необходимы дополнительные исследования.

Промежуточный вывод: низкую производительность query builder в SQLAlchemy можно нивелировать кэшем запросов. Дикий синтаксис future_select можно спрятать за фасадом.

А ещё я не уделил должного внимания prepared statements. Эти исследования я проведу чуть позже.

Как я открывал для себя ORM заново

Мы добрались главного -- ради этого раздела я писал статью. В этом разделе я поделюсь своими откровениями, посетившими меня в процессе работы.

Модульность

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

Собственные типы

Если данные обладают хитрым поведением, или же хитро преобразуются, совершенно очевидно, что их надо выкинуть на уровень модели. Я столкнулся с двумя вопросами: хранение цвета и работа с ENUM. Погнали по порядку.

Создание собственных простых типов рассмотрено в документации:

class ColorType(TypeDecorator):    impl = Integer    cache_ok = True    def process_result_value(self, value, dialect):        if value is None:            return        return color(value)    def process_bind_param(self, value, dialect):        if value is None:            return        value = color(value)        return value.value

Сыр-бор тут только в том, что мне стрельнуло хранить цвета не строками, а интами. Это исключает некорректность данных, но усложняет их сериализацию и десериализацию.

Теперь про ENUM. Меня категорически не устроило, что документация предлагает хранить ENUM в базе в виде VARCHAR. Особенно уникальные целочисленные Enum хотелось хранить интами. Очевидно, объявлять этот тип мы должны, передавая аргументом целевой Enum. Ну раз String при объявлении требует указать длину -- задача, очевидно, уже решена. Штудирование исходников вывело меня на TypeEngine -- и тут вместо примеров использования вас встречает "our source code is open 24/7". Но тут всё просто:

class IntEnumField(TypeEngine):    def __init__(self, target_enum):        self.target_enum = target_enum        self.value2member_map = target_enum._value2member_map_        self.member_map = target_enum._member_map_    def get_dbapi_type(self, dbapi):        return dbapi.NUMBER    def result_processor(self, dialect, coltype):        def process(value):            if value is None:                return            member = self.value2member_map[value]            return member.name        return process    def bind_processor(self, dialect):        def process(value):            if value is None:                return            member = self.member_map[value]            return member.value        return process

Обратите внимание: обе функции -- result_processor и bind_processor -- должны вернуть функцию.

Собственные функции, тайп-хинты и вывод типов

Дальше больше. Я столкнулся со странностями реализации json_arrayagg в mariadb: в случае пустого множества вместо NULL возвращается строка "[NULL]" -- что ни под каким соусом не айс. Как временное решение я накостылил связку из group_concat, coalesce и concat. В принципе неплохо, но:

  1. При вычитывании результата хочется нативного преобразования строки в JSON.

  2. Если делать что-то универсальное, то оказывается, что строки надо экранировать. Благо, есть встроенная функция json_quote. Про которую SQLAlchemy не знает.

  3. А ещё хочется найти workaround-функции в объекте sqlalchemy.func

Оказывается, в SQLAlchemy эти проблемы решаются совсем влёгкую. И если тайп-хинты мне показались просто удобными, то вывод типов поверг меня в восторг: типозависимое поведение можно инкапсулировать в саму функцию, что сгенерирует правильный код на SQL.

Мне заказчик разрешил опубликовать код целого модуля!
from sqlalchemy.sql.functions import GenericFunction, register_functionfrom sqlalchemy.sql import sqltypesfrom sqlalchemy import func, literal_columndef register(target):    name = target.__name__    register_function(name, target)    return target# === Database functions ===class json_quote(GenericFunction):    type = sqltypes.String    inherit_cache = Trueclass json_object(GenericFunction):    type = sqltypes.JSON    inherit_cache = True# === Macro ===empty_string = literal_column("''", type_=sqltypes.String)json_array_open = literal_column("'['", type_=sqltypes.String)json_array_close = literal_column("']'", type_=sqltypes.String)@registerdef json_arrayagg_workaround(clause):    clause_type = clause.type    if isinstance(clause_type, sqltypes.String):        clause = func.json_quote(clause)    clause = func.group_concat(clause)    clause = func.coalesce(clause, empty_string)    return func.concat(        json_array_open,        clause,        json_array_close,        type_=sqltypes.JSON,    )def __json_pairs_iter(clauses):    for clause in clauses:        clause_name = clause.name        clause_name = "'%s'" % clause_name        yield literal_column(clause_name, type_=sqltypes.String)        yield clause@registerdef json_object_wrapper(*clauses):    json_pairs = __json_pairs_iter(clauses)    return func.json_object(*json_pairs)

В рамках эксперимента я также написал функцию json_object_wrapper, которая из переданных полей собирает json, где ключи -- это имена полей. Буду использовать или нет -- ХЗ. Причём тот факт, что эти макроподстановки не просто работают, а даже правильно, меня немного пугает.

Примеры того, что генерирует ORM
SELECT concat(  '[',  coalesce(group_concat(product.tag_id), ''),  ']') AS product_tags
SELECT json_object(  'name', product.name,  'price', product.price) AS product,

PS: Да, в случае json_object_wrapper я изначально допустил ошибку. Я человек простой: вижу константу -- вношу её в код. Что привело к ненужным bindparam на месте ключей этого json_object. Мораль -- держите ORM в ежовых рукавицах. Упустите что-то -- и она вам такого нагенерит! Только literal_column позволяет надёжно захардкодить константу в тело SQL-запроса.

Такие макроподстановки позволяют сгенерировать огромную кучу SQL кода, который будет выполнять логику формирования представлений. И что меня восхищает -- эта куча кода работает эффективно. Ещё интересный момент -- эти макроподстановки позволят прозрачно реализовать паттерн Стратегия -- я надеюсь, поведение json_arrayagg пофиксят в следующих релизах MariaDB, и тогда я смогу своё костылище заменить на связку json_arrayagg+coalesce незаметно для клиентского кода.

Выводы

SQLAlchemy позволяет использовать преимущества наследования и полиморфизма (и даже немного иннкапсуляции. Флеш-рояль, однако) в SQL. При этом она не загоняет вас в рамки задач уровня Hello, World! архитектурными ограничениями, а наоборот даёт вам максимум возможностей.

Субъективно это прорыв. Я обожаю реляционные базочки, и наконец-то я получаю удовольствие от реализации хитрозакрученной аналитики. У меня в руках все преимущества ООП и все возможности SQL.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru