Работа с данными через классы доступа к данным

Материал из Course Orchestra
Перейти к: навигация, поиск
Внимание! Вы просматриваете документацию к Celesta 6.x. Документация по Celesta 7.x доступна на courseorchestra.github.io/celesta.

1. Справочник Celesta

1.1 Введение и основные понятия
1.2 Запуск и авто-обновление
1.3 Базовая настройка
1.4 Системные таблицы
1.5 CelestaSQL
1.6 CelestaDoc
1.7 Контексты сессии и вызова
1.8 Курсоры
1.9 BLOB-поля
1.10 Option-поля
1.11 Защита от потерянных обновлений
1.12 Метаданные Celesta
1.13 CelestaUnit

2. Celesta и базы данных

2.1 Особенности работы Celesta с поддерживаемыми типами СУБД
2.2 Проектирование базы данных Celesta в DBSchema

3. Создание решений с использованием Celesta для ShowCase

3.1 Программа обучения Celesta
3.2 Подготовка рабочего места для работы с Celesta
3.2.1 Для разработчиков платформы
3.2.2 Для разработчиков решений
3.3 Системные гранулы Celesta
3.3.1 common
3.3.1.1 Экспорт/импорт данных
3.3.1.2 Навигатор
3.3.1.3 Серии номеров
3.3.1.4 Иерархия Дьюи
3.3.1.5 Системные функции
3.3.1.6 Реестр настроек
3.3.1.7 Mailsender
3.3.1.8 Common.filter
3.3.2 common.api
3.3.4 security
3.3.3 lyra
3.4 Стандартные гранулы Celesta
3.4.1 dirusing
3.4.2 workflow
3.4.3 File repository
3.5 Отрисовка элементов Showcase при помощи Celesta
3.5.1 Конвертер XML-JSON
3.5.2 Навигатор (Navigator)
3.5.3 Информационная панель (Datapanel)
3.5.4 Серверное действие (Server activity)
3.5.5 Вебтекст (WebText)
3.5.6 Грид (Grid)
3.5.6.1 Панель инструментов (ToolBar)
3.5.7 XForms
3.5.7.1 Селекторы
3.5.7.2 Submission
3.5.7.3 Загрузка/Выгрузка файлов (Upload/Download)

5. Решение проблем

5.1 Проблемы с кодировкой jython-файлов

Классы доступа и их стандартные методы

По количеству таблиц и представлений, объявленных в Celesta, генерируются классы доступа к данным.

Каждый экземпляр класса доступа к данным (его мы также будем именовать «курсор») в каждый момент времени хранит информацию об одной записи (строке) в базе данных. Курсор можно передвигать по записям с учётом фильтров и сортировок. Если курсор создан для таблицы, его также можно использовать для вставки, модификации и удаления данных. В курсоре, созданном для представления, доступны только методы чтения записей.

Cursor.png

На UML-диаграмме показана иерархия классов доступа к данным. В основе иерархии стоит класс BasicDataAccessor. Каждый класс курсоров наследуется от класса BasicCursor, класс Sequence от BasicDataAccessor:

  • Cursor — предназначен для работы с таблицами. Наследует все методы BasicCursor, а также добавляет ряд собственных методов для возможности модификации данных.
  • ViewCursor — предназначен для работы с представлениями, никаких собственных методов к BasicCursor не добавляет.
  • MaterializedViewCursor - предназначен для работы с материализованными представлениями. Наследует все методы BasicCursor, а также добавляет ряд собственных методов для возможности получения данных по первичному ключу.
  • ParameterizedViewCursor - предназначен для работы с функциями(параметризованными представлениями). Никаких собственных методов к BasicCursor не добавляет, однако имеет отличный от базового класса конструктор.
  • ReadOnlyTableCursor — предназначен для работы с таблицами, объявленными с опцией WITH READ ONLY, никаких собственных методов к BasicCursor не добавляет.
  • Sequence — предназначен, для работы с последовательностями. Наследует все методы класса BasicDataAccessor и добавляет метод nextValue.

Ниже описываются методы класса Cursor, но те методы, которые унаследованы от BasicCursor (и могут применяться при работе с представлениями и неизменяемыми таблицами) отмечены значком Glass.png.

Конструктор курсора

Конструктор каждого курсора принимает в себя параметр context, который, в свою очередь, выдаётся каждому скрипту при начале работы. Использование context-а позволяет работать с разными таблицами системы в одной транзакции и затем единым образом коммитить все изменения, кроме того, переменная context содержит информацию о текущем пользователе, используемую системами логирования и разграничения прав доступа.

Так, для курсоров типов Cursor, ViewCursor, MaterializedViewCursor, ReadOnlyTableCursor конструктор вызывается следующим образом.

a = aCursor(context)
Ограничение столбцов в выборке

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

Допустим, что в БД имеется заполненная данными таблица table1:

create table table1 (
  id int identity not null primary key,
  numb int not null,
  numb2 int,
  varr varchar(2) not null
);

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

tableCursor = table1Cursor(context, ['numb, numb2'])

Теперь при любом запросе данных из БД celesta не будет выбирать столбец var, а в курсоре tableCursor поле varr всегда будет иметь значение None.

Некоторые особенности ограничения столбцов в выборке:

  1. Колонки, являющиеся частью первичного ключа, всегда будут попадать в курсор из БД, независимо от того, указаны они в списке необходимых полей или нет. Это сделано для корректной работы метода navigate курсоров при ограничении колонок.
  2. Колонки, являющиеся частью group by выражения материализованных представлений (materialized view) всегда будут попадать в курсор из БД.
  3. При передаче пустого списка полей или при его отсутствии будут выбираться все колонки.
  4. Использование столбцов с типом [BLOB-поля|blob] не изменяется. Данные из этих столбцов можно получать, как и раньше, отдельным методом.
Передача параметров в функции

Стоит отметить, что курсор ParameterizedViewCursor имеет собственную версию конструктора, принимающую набор именованных аргументов - параметров функции.

Допустим имеется таблица и функция для выборки из нее.

CREATE table t1 (
  id INT NOT NULL IDENTITY PRIMARY KEY,
  f1 int,
  f2 int,
  f3 VARCHAR (2)
);

CREATE FUNCTION pView1(p int) AS
  select sum (f1) as sumv, f3 as f3
  from t1 as t1
  where f2 = $p
  group by f3;

Тогда для создания курсора для функции с параметром p = 5 необходимо выполнить следующий код.

pView1 = pView1Cursor(context, 5)

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

Изменение полей курсора

По количеству объявленных полей в классе курсора имеются атрибуты, позволяющие читать и записывать информацию в поля курсора. Так, если таблица foo определена следующим образом

CREATE TABLE foo (
  a INT NOT NULL PRIMARY KEY,
  b VARCHAR(10),
  c DATETIME,
  d BIT 
)


то для того, чтобы вставить запись в таблицу foo, можно использовать следующий код:

import datetime

foo = fooCursor(context)
foo.a = 1
foo.b = 'text'
# 1 февраля 2014
foo.c = datetime.datetime(2014, 2, 1)
foo.insert()

Обратите внимание на использование класса datetime для записи значений даты. При необходимости заполнить поле "с" текущей датой и временем, это можно было бы сделать при помощи выражения

foo.c = datetime.datetime.now()


То, как изменить значение BLOB-поля, описано в статье BLOB-поля.

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

Каждый курсор имеет следующие методы (значком Glass.png обозначены методы, унаследованные от BasicCursor, которые могут применяться при работе с представлениями и таблицами «только на чтение»):

Закрытие курсора

  • Glass.png close() — закрытие курсора. Данный метод высвобождает все JDBC-ресурсы, аллоцированные во время существования курсора. Обращение к методам закрытого курсора приведёт к ошибке. Данный метод вызывать не обязательно, т. к. он вызывается автоматически после выполнения Celesta-процедуры на всех курсорах, созданных во время её выполнения. Вообще, предпочтительной практикой программирования является создание как можно меньшего числа курсоров в процедуре и повторное их использование. Тем не менее, если есть необходимость в создании большого числа курсоров, то возникает необходимость и в использовании метода close() в тот самый момент, когда экземпляр курсора становится ненужным.

Методы переходов по записям

  • tryGet(Object... values) Осуществляет поиск записи по ключевым полям, возвращает true, если запись найдена, и false, если записи с таким первичным ключом нет в таблице. В аргументах этого метода должны быть перечислены значения полей первичного ключа, количество аргументов должно быть равно количеству полей первичного ключа таблицы. ВНИМАНИЕ! Данный метод не учитывает никаких фильтров, наложенных на таблицу. Если вам необходимо найти запись с учётом фильтров, то используйте метод [try]First.
  • get(Object... values) То же, что tryGet, но выбрасывает исключение, если запись не найдена.
  • Glass.png navigate(command) — осуществляет переход по записям относительно текущего положения. Возвращает True, если переход удачный, и False — если записи не оказалось. При этом строка command может представлять собой произвольный набор из следующих символов, каждый из которых выполняется до тех пор, пока запись не нашлась:
    • - (минус) — переход к первой записи, удовлетворяющей условиям сортировки и фильтрации,
    • + (плюс) — переход к последней записи,
    • > — переход к следующей записи, относительно текущей, удовлетворяющей условиям сортировки и фильтрации,
    • < — переход к предыдущей записи,
    • = — обновление текущей записи, если она попадает в текущий фильтр.
  • Glass.png tryFirst() — то же, что navigate('-').
  • Glass.png first() — то же, что tryFirst(), но вызывает ошибку, если запись не найдена.
  • Glass.png tryLast() — то же, что navigate('+').
  • Glass.png last() — то же, что tryLast(), но вызывает ошибку, если запись не найдена.
  • Glass.png next() — то же, что navigate('>').
  • Glass.png previous() — то же, что navigate('<').
  • Glass.pngtryFindSet() — открывает на сервере набор записей (recordset) и устанавливает курсор в его начало. Возвращает True, если открывашийся набор не пуст, False — если записей в наборе нет.
  • Glass.pngfindSet() — то же, что tryFindSet(), но вызывает ошибку в случае, если переход неудачен.
  • Glass.pngnextInSet() — переход к следующей записи в текущем наборе данных на сервере. Если набор не открыт, вызов этого метода эквивалентен вызову tryFindSet(). Возвращает True, если переход состоялся, False — если достигнут конец набора.
  • Glass.pngiterate() — метод-python-генератор, позволяющий осуществить полную итерацию по набору записей с первой до последней. Например, если переменная rec содержит экземпляр курсора, то полная итерация с использованием метода iterate() может быть осуществлена следующим образом:
    for rec in rec.iterate():
         # здесь внутри цикла всё, 
         # что вы хотите сделать с записями rec

что будет полностью эквивалентно следующему коду:

    if rec.tryFindSet():
        while True:
            # здесь внутри цикла всё, 
            # что вы хотите сделать с записями rec      
            if not rec.nextInSet():
                break


В чём разница между [try]First() и [try]FindSet()? — Разница в отправляемом на БД запросе. [try]First() (а также navigate(), next(), last()...) выполняет запрос вида SELECT TOP 1, запрашивают одну запись и сразу закрывают на сервере recordset. Метод findSet() открывает на сервере recordset и держит его для того, чтобы его можно было бы обойти при помощи метода iterate().

Чем navigate('=') отличается от tryGet(_currentKeyValues())? — Метод navigate() учитывает текущие фильтры, а get() — не учитывает. Запись с текущим значением первичного ключа может не попасть в фильтр, поэтому navigate('=') может вернуть False в ситуации, когда tryGet(_currentKeyValues()) возвращает True.

Что значит navigate('=><')? — Эта команда предписывает следующий алгоритм: "Попытайся найти текущую запись. Если запись нашлась, выйди и верни True. Если записи уже нет (удалили), сходи вперёд. Если запись нашлась, выйди и верни True. Если впереди ничего нет, сходи назад. Если запись нашлась, верни True, если нет — False".

Методы сортировки и фильтрации

  • Glass.png setRange(String name) Сброс любого фильтра на поле.
  • Glass.png setRange(String name, Object value) Установка диапазона из единственного значения на поле. Передача значения None в качестве аргумента приводит к установке фильтра 'IS NULL' на данное поле.
  • Glass.png setRange(String name, Object valueFrom, Object valueTo) Установка диапазона «от..до включительно» на поле (на уровне языка SQL соответствует оператору BETWEEN). Использование None в качестве аргумента не допускается.
  • Glass.png setFilter(String name, String value) Установка сложного фильтра на поле, описание выражений сложных фильтров приведено ниже.
  • Glass.png limit(skip, limit) Установка ограничений на возвращаемый диапазон строк. В качестве параметров должны быть неотрицательные целые числа. Параметр skip означает количество строк, которое будет пропущено перед тем, как начнётся выдача (skip = 0 — выдача с самого начала), limit — максимальное число возвращаемых строк, при этом limit = 0 означает возврат всех строк. Вызов limit(0, 0) сбрасывает ограничения на возвращаемый диапазон набора строк. Ограничения, установленные методом limit(), не учитываются при вызове метода count().
  • Glass.png orderBy(String... names) Установка сортировки. Параметры — перечень имён полей для сортировки. Чтобы указывать сортировку по возрастанию или по убыванию, после каждого имени поля допускается добавлять ключевые слова ASC и DESC (ключевые слова могут быть в любом регистре, могут быть отделены от имени поля одним или несколькими пробелами). Если ключевое слово ASC/DESC не указано явно, сортировка идёт по возрастанию. Допускается вызов orderBy() без аргументов, чтобы сбросить все установленные ранее сортировки на сортировку по умолчанию. Имя поля можно указать не более чем в одном из аргументов метода orderBy(...).
Важная информация
Следует помнить, что в Celesta не бывает не отсортированных наборов данных: по умолчанию наборы данных в Celesta всегда сортируются по полям первичного ключа, а к любому набору полей, задаванному через orderBy(...), Celesta автоматически добавляет в конец те поля первичного ключа, которые не были перечислены в аргументах. Для представлений и «WITH READ ONLY» таблиц, у которых поля первичного ключа не заданы, Celesta использует для сортировки по умолчанию первое поле. Всё это реализовано для того, чтобы итерация по записям курсора была детерминированной.

Методы инициализации

  • Glass.png reset() Сброс фильтров и сортировки, с сохранением значений полей буфера.
  • Glass.png clear() Сброс фильтров, сортировки и полная очистка буфера, включая ключевые поля.
  • init() Очистка всех полей буфера, кроме ключевых.


Методы клонирования

  • Glass.png copyFiltersFrom(c) Перенос значений всех фильтров, включая значения limit (skip и limit), из курсора с тем же типом в текущий курсор.
  • Glass.png copyOrderFrom(c) Перенос настроек сортировки из курсора с тем же типом в текущий курсор.
  • Glass.png copyFieldsFrom(c) Перенос значений всех полей из курсора с тем же типом в текущий курсор.

Методы модификации данных

  • insert() Вставка содержимого курсора в БД. При этом если запись с таким первичным ключом уже существует, возникает ошибка.
  • tryInsert() Вставка содержимого курсора в БД. true если получилось, false если запись с таким первичным ключом уже существует
  • update() Сохранение содержимого курсора в БД, выбрасывая исключение в случае, если запись с такими ключевыми полями не найдена.
  • tryUpdate() Сохранение содержимого курсора в БД, true если получилось, false если запись с таким первичным ключом не существует.
  • delete() Удаление текущей записи.
  • deleteAll() Удаление всех записей, попадающих в фильтр. NB. Триггер onDelete при этом не вызывается.

Вспомогательные методы

  • Glass.png canRead(), canInsert(), canModify(), canDelete() Возвращает булевское значение, указывающее на наличие прав у текущей сессии на выполнение соответствующей операции.
  • Glass.png count() Возвращает количество записей в отфильтрованном наборе. В частности, если фильтров на курсор не установлено, возвращает полное количество записей в таблице. Ограничения на набор записей, установленные методом limit(), не учитываются при вызове метода count().
  • Glass.png callContext() Возвращает контекст вызова, на котором создан данный курсор.
  • Glass.png _tableName() Возвращает имя таблицы или представления.
  • Glass.png meta() Возвращает описание таблицы или представления (метаинформацию, экземпляр класса ru.curs.celesta.score.Table/View).
  • Glass.png asCSVLine() Возвращает значение полей курсора в виде CSV-строки с разделителями-запятыми.
  • getMaxStrLen(String name) Возвращает длину текстового поля (в символах). Выбрасывает ошибку, если в параметр передано имя несуществующего или не текстового поля. Возвращает -1, если поле определено как NVARCHAR(MAX).
  • getXRec() Возвращает копию буфера, содержащую значения, полученные при последнем чтении данных из базы.
  • resetIdentity(int newValue) Устанавливает текущее значение счётчика IDENTITY на таблице (если он есть). Параметр newValue задаёт значение, которое должно принять поле IDENITITY при следующей вставке. Этот метод предназначен для реализации механизмов экспорта-импорта данных из таблицы. Его следует применять с осторожностью, т.к. сбой в отсчёте IDENTIY-счётчика может привести к нарушению первичного ключа. Кроме того, как минимум в Oracle, в силу особенностей реализации, не гарантируется надёжная работа этого метода в условиях конкурретного доступа к таблице.

Обратите внимание, что методы get, first, insert, update имеют два варианта:

  • без приставки try (просто get(...) и т. д.)
  • с приставкой try (tryGet(...), tryFirst() и т. д.).

Методы без приставки try вызывают исключение, если в базе данных нет подходящих данных для выполнения действия. К примеру, first() вызовет исключение, если в установленный на курсор фильтр не попадёт ни одной записи (или, в вырожденном случае, если таблица окажется пуста). Методы get и update вызовут исключение в случае отсутствия соответствующей записи, а метод insert — если запись с таким набором значений полей первичного ключа уже существует. В то же время методы с приставкой try исключения не вызывают, а вместо этого возвращают булевское значение, сигнализирующее об успешности или неуспешности соответствующей операции.

Важная информация
Правильной практикой при разработке кода бизнес-логики является использование методов БЕЗ приставки try везде, где это возможно. Таким образом создаётся «самотестирующийся» код, вовремя сигнализирующий об ошибках в логике и/или в данных базы данных. К примеру, если при разработке процедуры мы предполагаем, что если приложение работает верно, то в переменной idFoo содержится идентификатор записи, существующей в таблице foo, то для получения самой записи следует писать foo.get(idFoo). В этом случае, если где-то в программе есть ошибка, приводящая к тому, что idFoo может принимать значение несуществующего идентификатора, об этом будут проинформированы разработчики и пользователи в самый момент возникновения данной ситуации. «Маскировка» возможных проблем путём использования try...-метода без явной нужды в возвращаемом значении этого метода является грубой ошибкой, которая приведёт к общему запутыванию отладки и дестабилизации кода. Разумеется, иногда в коде нужно выяснить — есть ли запись с таким идентификатором? Для этого — и только для этого — предназначен tryGet, аналогичное справедливо для других «try-методов», использование которых в подавляющем большинстве случаев оправдано только если предполагается явное использование возвращаемых значений этих методов.

Использование метода setFilter

В большинстве практических случаев фильтрацию курсоров по значению поля можно выполнять при помощи методов setRange(...) с двумя или тремя параметрами, отфильтровывающих значения по условию вида «поле = значение» либо по условию вида «поле between значение1 and значение2».

В случаях, когда простого сравнения или условия between недостаточно, метод setFilter позволяет наложить сложное логическое условие на значения в одном из полей курсора. Первым аргументом метода setFilter является имя поля, а вторым — выражение фильтра.

Правильное выражение фильтра может состоять из:

  • числовых либо текстовых литералов (в зависимости от типа поля),
  • литерала null,
  • логических операторов &, |, !,
  • операторов сравнения <, >, ..,
  • группирующих скобок (, ),
  • специальных операторов @ и % для текстовых полей.

Выражение фильтра не может быть null или пустой строкой, для сброса фильтра с поля следует вызывать метод setRange с одним параметром. Пробелы между литералами и операторами игнорируются. Выражение фильтра напрямую, без предварительной оптимизации, транслируется в условие для выражения WHERE языка SQL.

Выражения фильтра для полей с типами BIT и BLOB

Для полей с типами BIT и BLOB допустимо использование выражения фильтров вида null и !null, отфильтровывающие значения «поле is null» и «not (поле is null)»:

01.simple filter.png

Иные виды фильтрации для типа BLOB смысла не имеют, а для битового типа условие на True или False накладывается с помощью метода setRange(...).

Выражения фильтра для полей с типами INTEGER и REAL

Для полей с типами INTEGER и REAL допутимо использование выражений фильтров по следующим синтаксическим правилам:

02.filter.png

Здесь

  • & — знак логического И,
  • | — знак логического ИЛИ,
  •  ! — знак логического НЕ,
  • (, ) — группирующие скобки.

Выражение term для числовых полей имеет следующий синтаксис:

03.numeric term.png

Например, выражение фильтра

(10|<5)&>0 
для поля с именем "foo" будет переведено в условие
("foo" = 10 or "foo" < 5) and "foo" > 0

Знаки ">" и "<", ествественно, задают условия «строго больше» и «строго меньше», а использование символа ".." позволяет задавать условия «больше или равно» и «меньше или равно». Так, фильтр

..0|5..7|10.. 
будет траслирован в условие
"foo" <= 0 or "foo" between 5 and 7 or "foo" >= 10

(напоминаем, что оператор between в SQL задаёт диапазон с включением границ).

Важная информация
Обратите внимание, что выражение фильтра требует явной группировки скобками разных логических операторов, т. е. корректными являются выражения
(10|<5)&>0
10|(<5&>0)
10|<5|>0

но вызовет ошибку выражение

10|<5&>0 

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

Выражения фильтра для полей с типом DATETIME

Выражения фильтра для полей с типом DATETIME имеют такой же синтаксис, что и для числовых полей, но вместо числового нумерала <numeric literal> следует использовать нумерал даты в виде 'YYYYMMDD' (апостроф, восемь цифр, апостроф). Таким образом, правильные выражения фильтров для поля даты выглядят так:

'20131124'
'20131124'..'20151211'|'20111111'
(>'20131124'&..'20151211')|'20111111'..

Разные типы СУБД по-разному работают с литералами дат, но Celesta транслирует выражения фильтра в условия, корректно отрабатывающиеся каждой из поддерживаемых СУБД.

Выражения фильтра для полей с типом VARCHAR(n) и TEXT

Выражения фильтра для текстовых полей в целом похожи на выражения фильтра для числовых полей, с той лишь разницей, что вместо чисел в выражениях термов надо указывать строковые литералы в одинарных кавычках. Например, на текстовом поле корректным является фильтр 'aa'|'bb'|'cc', который отфильтрует записи, в которых значения фильтруемого поля равны "aa", "bb" или "cc". При этом, если нужно отфильтровать текст, содержащий одинарную кавычку, то её в текстовом литерале (как и обычно в языке SQL) следует удвоить: для отбора значений "John's company" следует писать 'John''s company'. Как и все прочие типы полей, текстовые поля можно фильтровать по значению null/ not null при помощи термов null/!null.

Кроме того, текстовые поля можно фильтровать при помощи оператора LIKE, применяя специальный символ %, означающий любую комбинацию любых символов, а также при помощи специального символа @ указывать на независимость фильтра от регистра.

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

04.text term.png

Так, выражение

('aaa'&'bb')|(!'ddd'&!null) 
будет транслировано в
(("foo" = 'aaa' and "foo" = 'bb') or (not ("foo" = 'ddd') and not ("foo" is null))
(что естественно никогда не будет выполнено, этот и следующий примеры даны лишь для иллюстрации принципа трансляции фильтров в язык SQL).

Выражение

@'q'|@..'cC'|@'Ff'..|@'a'..'b'|@%'5a'|'abc'%|! @ %'ef'%|null 
использующее знаки @, транслируется в
UPPER("foo") = 'Q' or UPPER("foo") <= 'CC' or UPPER("foo") >= 'FF' or UPPER("foo") between 'A' and 'B' 
or UPPER("foo") like '%5A' or "foo" like 'abc%' or not (UPPER("foo") like '%EF%') or "foo" is null
.


Использование метода setIn

Метод setFilter позволяет фильтровать записи, некоторое поле которых принимает любое значение из заранее заданного набора. К примеру,

myCursor.setFilter('city_id', "'MSK'|'LON'")

отфильтровывает записи, поле «код города» которых принимает значение MSK или LON. Вызов

myCursor.setFilter('city_id', "'M'%")


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

Однако функциональности setFilter бывает недостаточно: что если необходимо отфильтровать записи, относящиеся к городам, полное название которых на русском языке начинается с буквы «Ф»?

Одним из способов решения такой задачи могло быть следующее: отфильтровать справочник городов по city.setFilter('name', "'Ф'%"), далее выгрузить из базы данных в память полный набор идентификаторов таких городов, объединить их в строку фильтра через вертикальную черту, и использовать это как фильтр на другом курсоре. Ясно, что такой подход плох, если попадающих в фильтр записей слишком много: это породит обмен лишними данными по сети и слишком длинный SQL-запрос к интересующей нас таблице.

Для этого случая применяется метод setIn, который позволяет установить фильтр с вложенным запросом по указанному набору полей. Доступен для наследников классов Cursor и ViewCursor.

Общая схема работы с setIn такова:

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

Связь полей задается при помощи вспомогательного класса FieldsLookup, возвращаемого в качестве результата из метода setIn целевого курсора. Метод setIn принимает в качестве единственного аргумента объект вспомогательного курсора, по которому ищется пересечение. Подготовка целевого курсора и аккумулирование пар столбцов с последующей установкой фильтра происходит следующим образом:

from _filters_orm import TargetCursor, AuxiliaryCursor

a = TargetCursor(context)
b = AuxiliaryCursor(context)
b.setRange('foo', 'bar')
lookup = a.setIn(b).add("a1", "b1").add("a2", "b2")

Для данного примера в PostgreSQL, например, для доступа к строкам курсора a будет сгенерированно следующее sql выражение:

SELECT ... FROM Target WHERE ( a1, a2 ) IN (SELECT b1, b2 FROM Auxiliary WHERE Auxiliary.foo = 'bar' )


К целевому курсору можно применить любое число вспомогательных курсоров через метод and класса FieldsLookup. При этом вспомогательные курсоры между собой никак не пересекаются. Пример задания нескольких вспомогательных курсоров ниже:

from _filters_orm import TargetCursor, AuxiliaryCursor, Auxiliary2Cursor

a = TargetCursor(context)
a.setRange('afoo', 'aBar')
b = AuxiliaryCursor(context)
b.setRange('bFoo', 'bBar')
c = Auxiliary2Cursor(context)
c.setRange('cFoo', 'cBar')
lookup = a.setIn(b).add("a1", "b1").add("a2", "b2")
lookup2 = lookup.and(c).add("a1", "c1")

Для данного примера в PostgreSQL, например, для доступа к строкам курсора a будет сгенерированно следующее sql выражение:

SELECT ... 
FROM Target 
WHERE aFoo = 'aBar' 
    AND ( a1, a2 ) IN (SELECT b1, b2 FROM Auxiliary WHERE Auxiliary.bFoo = 'bBar' )
    AND (a1) IN (SELECT c1 FROM Auxiliary2 WHERE Auxiliary2.cFoo = 'cBar' )


У данного фильтра имеется набор ограничений, несоблюдение которых приведёт к выбрасыванию исключения во время выполнения методов FieldsLookup.add или BasicCursor.setIn:

  • Типы данных у каждой пары сопоставляемых полей должны в точности совпадать.
  • В каждой из таблиц должен существовать индекс, включающий в себя все столбцы из набора сопоставляемых столбцов: для примера выше для таблицы Target должен иметься индекс I1(a1, a2,..), для Auxiliary - I2(b1, b2,...).
  • Для курсоров на таблицы соответствующие индексы должны содержать сопоставляемые столбцы в своём начале. Для нашего примера, если имеются индексы I1(a1, a2,..), I2(b1, b2,...), следующий код вызовет исключение, т. к. поля a2, b2 находятся не в начале индексов I1 и I2:
  • Класс FieldsLookup может принять в себя курсоры только одного происхождения, т.е. либо оба курсора для работы с таблицами, либо оба курсора для работы с представлениями.
lookup = setIn(b).add("a2", "b2")


Триггеры

Триггером называется написанная создателем решения функция, принимающая в качестве аргумента экземпляр курсора, присоединённая к классу курсора, автоматически вызываемая при вставке, удалении и модификации записи.

При действиях insert(), update() и delete() система вызывает определённые создателем решения pre- (выполняемые до модификации данных в базе) и post- (выполняемые после модификации данных в базе) триггеры. Таким образом, всего существует шесть типов триггеров:

pre-триггеры post-триггеры
onPreInsert onPostInsert
onPreUpdate onPostUpdate
onPreDelete onPostDelete
Важная информация
Чаще всего имеет смысл пользоваться pre-триггерами, чтобы выполнить некоторые действия до того, как изменение будет внесено в базу. Однако обратите внимание: т. к. триггер onPreInsert выполняется до отправки содержимого курсора в базу данных, то на момент его выполнения не заполняются значения полей, обладающих свойствами DEFAULT, GETDATE() и IDENTITY (для их автозаполнения средствами БД следует присваивать им значение None). В триггере onPostInsert эти поля уже будут заполнены.

В триггере onPreUpdate удобно воспользоваться объектом xRec, чтобы определить, какие именно поля собираются быть изменёнными в таблице. В триггере onPostUpdate объектом xRec воспользоваться уже нельзя, т. к. он становится равным текущему буферу.

Триггеров каждого типа на одной таблице может быть сколько угодно. Триггер для любой таблицы может быть определён в любом питоновском модуле любого пакета/гранулы, например, так:

 def testTrigger(rec):
     print 'Test trigger is run on record with field1 = %s' % rec.field1

(Единственным ограничением на процедуру триггера является то, что она должна принимать один аргумент, который может быть интерпретирован, как курсор нужного типа).

Рассмотрим пример того, как следует присоединять триггеры к классам таблиц.

Предположим, что в грануле g1 определена таблица aa — это значит, что в пакете g1 существует модуль _g1_orm.py с классом aaCursor. Пусть триггер с названием testTrigger определён в модуле hello.py из того же пакета. Значит, в модуле __init.py__ пакета g1 необходимо написать следующий код:

 from . import _g1_orm
 from . import hello
 _g1_orm.aaCursor.onPreInsert.append(hello.testTrigger)

Обратите внимание на конструкцию "from . import..." для импорта имён из текущего пакета. Только такая форма будет работоспособной в модуле __init.py__, явное использование имени g1 вместо точки может привести к ошибкам «имя не определено».

Атрибуты onPreInsert, onPostInsert и т. д. являются статическими атрибутами каждого класса-курсора, по умолчанию являющими собой пустой массив. В модуле __init.py__ эти массивы следует заполнять ссылками на функции-триггеры, это даст возможность системе в момент выполнения вызывать все присоединённые к таблицам триггеры, которых может быть несколько и которые могут быть определены в различных частях кода решения.

Системные курсоры также поддерживают данные триггеры. Но в силу того, что это Java классы, работать с ними нужно слегка иначе. Триггеры регистрируются через статические методы системных курсоров. Ниже приведен простой пример использования этого API.

 from ru.curs.celesta.syscursors import LogCursor
 from . import hello
 LogCursor.onPreInsert(hello.testTrigger)


Объект xRec

Объект xRec, получаемый с помощью метода getXRec(), предназначен преимущественно для использования в триггере onPreUpdate. Cравнивая поля xRec с текущими значениями полей, можно определить, что именно изменилось в записи.

xRec хранит значения полей, полученные при последнем чтении курсора из базы данных (в отличие от основного буфера, поля которого после чтения равны полям xRec, но затем изменяются, когда пользователь присваивает им новые значения). Обновление объекта xRec происходит только при следующих действиях:

  • [try]first(),
  • [try]get(),
  • next(),
  • [try]insert() (по сути после вставки система выполняет операцию get() для курсора, чтобы прочитать значения, выданные базой данных на поля IDENITY, GETDATE(), DEFAULT, обновляя и основной буфер, и xRec),
  • [try]update() (после обновления в БД xRec становится копией текущего курсора),
  • delete() (после обновления в БД xRec заполняется значением буфера, как он был до удаления).

Обратите внимание, что в pre- и post- триггерах значение xRec будет различным!

Класс Sequence

Класс Sequence позволяет работать с последовательностями. В отличие от остальных классов доступа при кодогенерации вместо суффикса Cursor используется суффикс Sequence. Класс Sequence имеет единственный метод nextValue, позволяющий получить следующее значение последовательности в виде типа long.

Ниже приведен пример использования класса доступа Sequence

CREATE GRAIN sequences version '1.0';

CREATE SEQUENCE idNumerator START WITH 3;


from sequences._sequences_orm import idNumeratorSequence

idNumerator = idNumeratorSequence(context)
id = idNumerator.nextValue()

Распределение прав доступа и протоколирование изменений

Работа с данными через классы доступа к данным даёт не только возможность писать универсальный, не зависящий от используемой СУБД код, но также и решить задачу централизованного распределения прав доступа к данным таблиц и протоколирования изменений.

Вызов ряда методов требует наличия соответствующих прав у пользователя на таблицы, прописанных в системных таблицах celesta.userroles и celesta.permissions, в противном случае возникает исключение PermissionDeniedException с сообщением вида "There is no ... permission for user ... on object ...".

Если протоколирование изменения таблицы настроено в таблице celesta.logsetup, то вызов некоторых методов будет приводить к созданиям записей в таблице celesta.log

Метод Требуемые права Протоколирование изменений
  • [try]first(),
  • [try]get(),
  • next().
требуют право на чтение (r) не протоколируются
[try]insert() требуют право на вставку (i) протоколируется, если включён флаг i.
  • oldvalues - пустое значение.
  • newvalues - вставляемая запись.
[try]update() требуют право на модификацию (m) протоколируется, если включён флаг m.
  • oldvalues - состояние записи до модификации.
  • newvalues - состояние записи после модификации.
delete[All]() требуют право на удаление (d) delete() протоколируется, если включён флаг d.
  • oldvalues - состояние записи до удаление.
  • newvalues - пустое значение.

deleteAll() не протоколируется и триггеры не выполняются.

Прочие методы не требуют никаких прав доступа к таблице и вызов их не протоколируется. Т. е. определить курсор, выставить на нём фильтры и даже подсчитать количество подпадающих под фильтры записей при помощи метода count() можно, даже не имея никаких прав на чтение таблицы.