Довериться Кодду или своим объектам? +15


Хранимые объекты без головной боли: простой пример работы с объектами Cache на языках ObjectScript и Python



Замок Нойшванштайн

В июне 2020 года ровно 50 лет табличным хранилищам данных или говоря формально — реляционной модели данных. Вот официальный документ – та самая знаменитая статья. За что говорим огромное спасибо доктору Эдгару Фрэнку Кодду. И, между прочим, реляционная модель данных входит в список важнейших мировых инноваций последних 100 лет по версии Форбса.

С другой стороны, как ни странно, Кодд считал реляционные базы данных и язык SQL искаженной реализацией своей теории. В качестве ориентира, он даже разработал 12 правил, которым должна удовлетворять каждая система управления реляционными базами данных (на самом деле это 13 правил). И, по правде говоря, на сегодня, в мире не найти СУБД удовлетворяющих хотя бы «Правилу 0» Кодда и, следовательно, никто не может называть свою СУБД на 100% реляционной :) Может есть исключения, подскажите?

Реляционная модель не очень сложна и изучена вдоль и поперёк. Может быть даже слишком глубоко изучена. А между тем, в этом 2019 году году отметим ещё и другой юбилей – ровно 10 лет назад хештег #NoSQL он же впоследствии «not only SQL» появился в твиттере и начал своё стремительное проникновение в практики разработки моделей баз данных.

К чему такое долгое предисловие? К тому, что вселенная программирования состоит из данных (и алгоритмов конечно), а монопольное положение реляционной модели приводит к, как это повежливее сказать, раздвоению сознания программиста. Потому что рабочие объекты в голове разработчика (ООП тоже тотально, правда?), все эти списки, очереди, деревья, кучи, словари, потоки и так до бесконечности, далеко не таблицы.

А если продолжить и вспомнить про архитектуру хранения в современных СУБД? Давайте говорить прямо, никто в здравом уме данные в виде таблиц не хранит. Разработчики СУБД, чаще всего используют разновидности B-дерева (в PostrgeSQL например) или, что происходит гораздо реже, хранилище на основе словаря. С другой стороны баррикад, разработчики, использующие для хранения СУБД, оперируют тоже не таблицами. И это вынуждает программистов постоянно ликвидировать семантический разрыв с помощью неуклюжего промежуточного слоя данных. И, тем самым, вызывать у себя внутреннее дихотомическое напряжение, системный дискомфорт и отладочную бессонницу.

То есть, если коротко, налицо противоречие – данные пакуем в подходящие под задачу объекты, а при сохранении должны заботится о каких-то таблицах.

Безнадёга? Нет :) А как же объектно-реляционное отображение, оно же в простонародье ORM? Оставим эту священную войну Егору Бугаенко со товарищи. Да и вся эта история из прошлого века, как по версии дядюшки Боба, не должна нас волновать.

Безусловно стоит упомянуть, что «мешок с байтами» ( Роберт Мартин «Чистая Архитектура») можно сериализовать и скинуть в файл или толкнуть в какой-нибудь другой подходящий поток. Но, во-первых, это нас сразу ограничит в языке, а во-вторых, мы сейчас будем волноваться только о хранении в СУБД.

В этих перипетиях с «мешками с байтами» есть приятное исключение – СУБД Intersystems Cache (а ныне и платформа данных InterSystems IRIS). Это, возможно, единственная в мире СУБД, которая не скрывает от разработчика очевидное и даже идёт дальше — освобождает от мыслей «как это всё правильно хранить». Достаточно сказать, что класс продолжает род Persistent и дело в шляпе, то есть в глобалах (не путать с глобальными переменными!).

Хранить можно все типы данных, включая символьные и бинарные потоки. Вот простейший пример:

// класс для сохраняемого объекта с одним свойством-строкой
// сюрприз: именовать свойства можно любой строкой и, в этом примере потребовались кавычки, для идентификаторов без пробелов они, конечно, не требуются

Class FW.Events Extends %Persistent { 
    Property "My name" As %String;
}

// пробуем в работе через терминал
// создаём новый «чистый» объект

set haJS = ##class(FW.Events).%New()

// сохраняем его

write haJS.%Id()

Причём, и это замечательно, с хранимыми объектами можно общаться не только на ObjectScript, родном для Cache, а получая и сохраняя их непосредственно в программах на Python, Java, JavaScript, C++, C#, Perl. И даже, о ужас :). черпать информацию из этих же объектов напрямую через SQL запросы, да и вызывать собственные методы объектов тоже возможно. Точнее, методы в этом случае сами собой (и волшебного слова SqlProc) превращаются в хранимые процедуры. Вся магия уже есть под капотом СУБД Cache.
Как получить бесплатный тестовый доступ к СУБД Intersystems Cache?
Это абсолютно реально, что бы не утверждали злые языки! :) Скачать и установить однопользовательскую полнофункциональную версию Cache можно здесь (потребуется пройти бесплатную регистрацию). Доступны сборки для MacOS, Windows и Linux.
Удобнее всего работать с кодом на ObjectScript и прямым доступом непосредственно к серверу СУБД Cache (и платформе InterSystems IRIS тоже) используя IDE Atelier, которая основана на Eclipse. Все инструкции по загрузке и установке здесь.

Кому удобнее и привычнее, можно использовать комфортный и простой Visual Studio Code, дополнив его разрабатываемым сообществом плагином ObjectScript.

А теперь несколько практических примеров. Попробуем создать пару из связанных объектов и поработать с ними на ObjectScript и на Python. Интеграция с другими языками реализована очень похоже. Python выбран из соображений «максимального родства» с ObjectScript – оба языка скриптовые, поддерживают ООП и не имеют строгой типизации :)

За идеями для примеров обращаемся к бодрым хабаровским (не путать с хабровскими!) проектам «Фреймворк-посиделок». Идейный исходный код лежит на github.com/Hajsru/framework-weekend А наш исходный код ниже по тексту.
Важный нюанс для пользователей macOS. При запуске модулей поддержки для Python необходимо помнить, что требуется указать путь DYLD_LIBRARY_PATH к каталогу, где у вас установлены Cache. Например так:
export DYLD_LIBRARY_PATH=/application/Cache/bin:$DYLD_LIBRARY_PATH
В документации это указано особо.

Создаём хранимые классы на ObjectScript


Итак поехали. Классы в Cache у нас будут очень простые. Можно обойтись и без IDE – скопировать код классов прямо через портал вашего экземпляра платформы Cache (да, СУБД Cache далеко не только СУБД): Обозреватель системы > Классы > Импорт (Namespace USER).

Объекты после сохранения появятся в глобалах с именами совпадающими с названиями соответствующих классов. Искать так же в портале управления Cache: Обозреватель системы > Глобалы (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;
}

Получаем доступ к объектам в Cache из Питона


Вначале подключаемся к базе данных СУБД Cache. Повторяем так, как и в документации.
Просто полезный факт для работы. Бесплатная, она же учебная, версия СУБД Cache позволят делать всё что доступно в полнофункциональной версии, но разрешает только два активных подключения. Поэтому одновременно держать соединение из IDE и пытаться запустить другой код для взаимодействия с сервером не удастся. Самое простое найденное решение – закрывайте IDE на время запуска Python кода.
# импорт модуля Cache для интеграции с 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])

Как можете заметить, используем готовые функции-обёртки для методов доступа к полям объектов в Cache: 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)

Итак, на этих примерах видно, что не обязательно думать одновременно о своей модели данных и её табличной схеме хранения. Можно применять более очевидные инструменты для своих задач.

Подробные инструкции по подключению и использованию Cache с Python и другими языками всегда доступны вам в документации и на портале сообщества разработчиков InterSystems – это ни много не мало, а 5000 участников community.intersystems.com
Справка: мультимодельная СУБД InterSystems Cache остаётся бессменным мировым лидером объектных баз данных





К сожалению, не доступен сервер mySQL