Хранимые объекты без головной боли: простой пример работы с объектами Caché на языках ObjectScript и Python
Замок Нойшванштайн
В июне 2020 года ровно 50 лет табличным хранилищам данных или говоря формально — реляционной модели данных. Вот официальный документ – та самая знаменитая статья. За что говорим огромное спасибо доктору Эдгару Фрэнку Кодду. И, между прочим, реляционная модель данных входит в список важнейших мировых инноваций последних 100 лет по версии Форбса.
С другой стороны, как ни странно, Кодд считал реляционные базы данных и язык SQL искаженной реализацией своей теории. В качестве ориентира, он даже разработал 12 правил, которым должна удовлетворять каждая система управления реляционными базами данных (на самом деле это 13 правил). И, по правде говоря, на сегодня, в мире не найти СУБД удовлетворяющих хотя бы «Правилу 0» Кодда и, следовательно, никто не может называть свою СУБД на 100% реляционной :) Может есть исключения, подскажите?
Реляционная модель не очень сложна и изучена вдоль и поперёк. Может быть даже слишком глубоко изучена. А между тем, в этом 2019 году году отметим ещё и другой юбилей – ровно 10 лет назад хештег #NoSQL он же впоследствии «not only SQL» появился в твиттере и начал своё стремительное проникновение в практики разработки моделей баз данных.
К чему такое долгое предисловие? К тому, что вселенная программирования состоит из данных (и алгоритмов конечно), а монопольное положение реляционной модели приводит к, как это повежливее сказать, раздвоению сознания программиста. Потому что рабочие объекты в голове разработчика (ООП тоже тотально, правда?), все эти списки, очереди, деревья, кучи, словари, потоки и так до бесконечности, далеко не таблицы.
А если продолжить и вспомнить про архитектуру хранения в современных СУБД? Давайте говорить прямо, никто в здравом уме данные в виде таблиц не хранит. Разработчики СУБД, чаще всего используют разновидности B-дерева (в PostrgeSQL например) или, что происходит гораздо реже, хранилище на основе словаря. С другой стороны баррикад, разработчики, использующие для хранения СУБД, оперируют тоже не таблицами. И это вынуждает программистов постоянно ликвидировать семантический разрыв с помощью неуклюжего промежуточного слоя данных. И, тем самым, вызывать у себя внутреннее дихотомическое напряжение, системный дискомфорт и отладочную бессонницу.
То есть, если коротко, налицо противоречие – данные пакуем в подходящие под задачу объекты, а при сохранении должны заботится о каких-то таблицах.
Безнадёга? Нет :) А как же объектно-реляционное отображение, оно же в простонародье ORM? Оставим эту священную войну Егору Бугаенко со товарищи. Да и вся эта история из прошлого века, как по версии дядюшки Боба, не должна нас волновать.
Безусловно стоит упомянуть, что «мешок с байтами» ( Роберт Мартин «Чистая Архитектура») можно сериализовать и скинуть в файл или толкнуть в какой-нибудь другой подходящий поток. Но, во-первых, это нас сразу ограничит в языке, а во-вторых, мы сейчас будем волноваться только о хранении в СУБД.
В этих перипетиях с «мешками с байтами» есть приятное исключение – СУБД Intersystems Caché (а ныне и платформа данных InterSystems IRIS). Это, возможно, единственная в мире СУБД, которая не скрывает от разработчика очевидное и даже идёт дальше — освобождает от мыслей «как это всё правильно хранить». достаточно сказать, что класс продолжает род Persistent и дело в шляпе, то есть в глобалах (не путать с глобальными переменными!).
Хранить можно все типы данных, включая символьные и бинарные потоки. Вот простейший пример:
// класс для сохраняемого объекта с одним свойством-строкой
// сюрприз: именовать свойства можно любой строкой и, в этом примере потребовались кавычки, для идентификаторов без пробелов они, конечно, не требуются
Class FW.Events Extends %Persistent {
Property "My name" As %String;
}
// пробуем в работе через терминал
// создаём новый «чистый» объект
set haJS = ##class(FW.Events).%New()
// сохраняем его
write haJS.%Id()
Причём, и это замечательно, с хранимыми объектами можно общаться не только на ObjectScript, родном для Caché, а получая и сохраняя их непосредственно в программах на Python, Java, JavaScript, C++, C#, Perl. И даже, о ужас :). черпать информацию из этих же объектов напрямую через SQL запросы, да и вызывать собственные методы объектов тоже возможно. Точнее, методы в этом случае сами собой (и волшебного слова SqlProc) превращаются в хранимые процедуры. Вся магия уже есть под капотом СУБД Caché.
Как получить бесплатный тестовый доступ к СУБД Intersystems Caché?
Это абсолютно реально, что бы не утверждали злые языки! :) Скачать и установить однопользовательскую полнофункциональную версию Caché можно здесь (потребуется пройти бесплатную регистрацию). Доступны сборки для MacOS, Windows и Linux.
Удобнее всего работать с кодом на ObjectScript и прямым доступом непосредственно к серверу СУБД Caché (и платформе InterSystems IRIS тоже) используя IDE Atelier, которая основана на Eclipse. Все инструкции по загрузке и установке здесь.
Кому удобнее и привычнее, можно использовать комфортный и простой Visual Studio Code, дополнив его разрабатываемым сообществом плагином ObjectScript.
А теперь несколько практических примеров. Попробуем создать пару из связанных объектов и поработать с ними на ObjectScript и на Python. Интеграция с другими языками реализована очень похоже. Python выбран из соображений «максимального родства» с ObjectScript – оба языка скриптовые, поддерживают ООП и не имеют строгой типизации :)
За идеями для примеров обращаемся к бодрым хабаровским (не путать с хабровскими!) проектам «Фреймворк-посиделок». Идейный исходный код лежит на github.com/Hajsru/framework-weekend А наш исходный код ниже по тексту.
Важный нюанс для пользователей macOS. При запуске модулей поддержки для Python необходимо помнить, что требуется указать путь DYLD_LIBRARY_PATH к каталогу, где у вас установлены Caché. Например так:
export DYLD_LIBRARY_PATH=/application/Cache/bin:$DYLD_LIBRARY_PATH
В документации это указано особо.
Создаём хранимые классы на ObjectScript
Итак поехали. Классы в Caché у нас будут очень простые. Можно обойтись и без IDE – скопировать код классов прямо через портал вашего экземпляра платформы Caché (да, СУБД Caché далеко не только СУБД): Обозреватель системы > Классы > Импорт (Namespace USER).
Объекты после сохранения появятся в глобалах с именами совпадающими с названиями соответствующих классов. Искать так же в портале управления Caché: Обозреватель системы > Глобалы (Namespace USER).
// объект событие включает название, описание, дату проведения и список участников
Class FW.Event Extends %Persistent
{
Property title as %String;
Property description as %String;
Property date as %Date;
Property visitors as list of FW.Attendee;
}
// объект участник имеет имя/ник
Class FW.Attendee Extends %Persistent
{
Property name As %String;
}
Получаем доступ к объектам в Caché из Питона
Вначале подключаемся к базе данных СУБД Caché. Повторяем так, как и в документации.
Просто полезный факт для работы. Бесплатная, она же учебная, версия СУБД Caché позволят делать всё что доступно в полнофункциональной версии, но разрешает только два активных подключения. Поэтому одновременно держать соединение из IDE и пытаться запустить другой код для взаимодействия с сервером не удастся. Самое простое найденное решение – закрывайте IDE на время запуска Python кода.
# импорт модуля Caché для интеграции с Python3
import intersys.pythonbind3
# соединение с сервером
conn = intersys.pythonbind3.connection()
conn.connect_now("localhost[1972]:USER","_SYSTEM","SYS", None)
# проверка дескриптора подключения
print ("conn = %d " % conn.handle)
# подключение к базе данных
database = intersys.pythonbind3.database(conn)
А сейчас мы сделаем объектную базу данных об ИТ-мероприятиях и их участниках. Очень-очень простую. Первым создадим класс для регистрации и хранения информации об участнике мероприятия. Для простоты к классе только имя участника.
# класс для объектов с информацией о зарегистрированных участниках
class Attendee:
# инициализация нового пустого объекта в оперативной памяти
def __init__ (self):
self.att = database.create_new("FW.Attendee", None)
# запись имени участника и сохранение объекта в базе данных с присвоением уникального id
def new (self, name):
self.att.set("name", name)
self.att.run_obj_method("%Save",[])
# загрузка объекта по id из базы данных участников
def use (self, id):
self.att = database.openid("FW.Attendee",str(id),-1,-1)
# удаление объект из базы данных участников
def clean (self):
id = self.att.run_obj_method("%Id",[])
self.att.run_obj_method("%DeleteId", [id])
Как можете заметить, используем готовые функции-обёртки для методов доступа к полям объектов в Caché: set и в параметрах передаём имя свойства в кавычках и openid с именем пакета и класса. Про аналогичную функцию get есть примеры ниже. Для доступа в любым другим методам, включая унаследованные классом от предков используется функция run_obj_method() с именем метода и параметрами вызова, если они необходимы.
Самая важная магия в строчке: self.att.run_obj_method("%Save",[])
Именно так мы имеем возможность сохранять объекты прямым указанием и без необходимости применять дополнительные библиотеки и каркасы/фреймворки, вроде вездесущих и неприглядных ORM.
Кроме того, учитывая объектно-ориентированную природу ObjectScript вместе с методами своего класса (в нашем примере мы их не делали) бонусом получаем доступ из Python ко всему набору методов унаследованных от класса Persistent и его предков. Вот полный список, если что.
Создадим первого участника:
att = Attendee()
att.new("Аким")
После запуска этого кода в базе данных появится глобал с именем FW.AttendeeD и содержимым только что сохранённого объекта как на скриншоте:
У этого объекта после сохранения появился собственный id (с номером 1). Поэтому можно загружать его в нашу программу по этому id:
att = Attendee()
att.use(1)
print (att.att.get("name"))
И теперь, опять же зная id, при необходимости можно удалить объект из базы данных участников:
att = Attendee()
att.use(1)
att.clean()
Проверьте, после запуска этого примера, запись об объекте должна исчезнуть в глобале. Хотя загруженные данные всё ещё остаются «в памяти» вашего объекта до завершения работы программы.
Сделаем следующий шаг. Создадим собственно записи о мероприятиях.
# класс для объектов с информацией о мероприятиях
class Event:
# инициализация нового пустого объекта в оперативной памяти
def __init__ (self):
self.event = database.create_new("FW.Event", None)
# наполнение объекта информацией и сохранение в базе данных с присвоением уникального id
def new (self, title, desc, date):
self.event.set("title", title)
self.event.set("description", desc)
self.event.set("date", date)
self.event.run_obj_method("%Save",[])
# загрузка объекта по id из базы данных
def use (self, id):
self.event = database.openid("FW.Event",str(id),-1,-1)
# добавление участника в список участников мероприятия
def addAttendee (self, att):
eventAtt = self.event.get("visitors")
eventAtt.run_obj_method("Insert", [att])
self.event.set("visitors", eventAtt)
self.event.run_obj_method("%Save",[])
# удаление объекта из базы данных
def clean (self):
id = self.event.run_obj_method("%Id",[])
self.event.run_obj_method("%DeleteId", [id])
Структура класса почти такая же как было выше у класса для участника. Самое главное, появился метод добавления участников в список участников этого мероприятия addAttendee(att).
Пробуем создать объект-запись о новом мероприятии и сохранить его в базе данных:
haJS = Event()
haJS.new("haJS", "Фронтенд митап", "2019-01-19")
Должно получится примерно так (заметьте, что дата автоматически конвертирована в формат ObjectScript и при загрузке обратно в объект Python вернётся в исходно заданном формате):
Осталось добавить участника в мероприятие:
# загружаем ранее сохранённое мероприятие
haJS = Event()
haJS.use(1)
# создаём нового участника
att = Attendee()
att.new("Марк")
# добавляем участника в наше мероприятие
haJS.addAttendee(att.att)
Итак, на этих примерах видно, что не обязательно думать одновременно о своей модели данных и её табличной схеме хранения. Можно применять более очевидные инструменты для своих задач.
Подробные инструкции по подключению и использованию Caché с Python и другими языками всегда доступны вам в документации и на портале сообщества разработчиков InterSystems – это ни много не мало, а 5000 участников community.intersystems.com
Справка: мультимодельная СУБД InterSystems Caché остаётся бессменным мировым лидером объектных баз данных