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

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

Для каждой из таблиц и представлений, объявленных в CelestaSQL, генерируются классы доступа к данным.

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

cursors

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

Cursor

предназначен для работы с таблицами. Наследует все методы BasicCursor, а также добавляет ряд собственных методов для возможности модификации данных.

ViewCursor

предназначен для работы с представлениями, никаких собственных методов к BasicCursor не добавляет.

MaterializedViewCursor

предназначен для работы с материализованными представлениями. Наследует все методы BasicCursor, а также добавляет ряд собственных методов для возможности получения данных по первичному ключу.

ParameterizedViewCursor

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

ReadOnlyTableCursor

предназначен для работы с таблицами, объявленными с опцией WITH READ ONLY, никаких собственных методов к BasicCursor не добавляет.

Sequence

предназначен для работы с последовательностями. Наследует все методы класса BasicDataAccessor и добавляет метод nextValue.

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

1.1. Метаданные о полях курсора

Для вызова ряда методов работы с данными курсора, а также для удобства получения информации о том, какие поля присутствуют в каждом из курсоров, на каждый из классов -курсоров также кодогенерируется класс-компаньон Columns. Класс-компаньон является статическим вложенным классов курсора. Например, для класса FooCursor компаньоном является FooCursor.Columns.

Каждому полю курсора соответствует метод в классе Columns, возвращающий метаданные соответствующей колонки, как показано на диаграмме:

cursormeta

В каждом экземпляре курсора соответствующий экземпляр класса Columns доступен через public final поле COLUMNS. Кроме того, класс Columns каждого из курсоров может быть проинстанцирован независимо от курсора — его конструктор принимает параметр ICelesta.

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

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

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

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

ACursor a = new ACursor(context);

Максимальное количество курсоров, которое можно создать с помощью одного экземпляра CallContext, равно 1023. При превышении этого значения возникает ошибка "Too many data accessors".

Это не ограничивает разумные сценарии работы с данными в базе данных, но предотвращает утечку JDBC-ресурсов, если, например, курсоры создаются в цикле. При создании курсора внутри цикла, его необходимо явным образом закрывать.

1.2.1. Ограничение столбцов в выборке

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

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

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

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

//инстанцируем объект-компаньон с информацией о столбцах
Table1Cursor.Columns columns = new Table1Cursor.Columns(context.getCelesta());
//передаём в дополнительных аргументах конструктора перечень желаемых столбцов
Table1Cursor tableCursor = new Table1Cursor(context, columns.numb(), columns.numb2());

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

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

  1. Колонки, являющиеся частью первичного ключа, всегда будут попадать в курсор из БД, независимо от того, указаны они в списке необходимых полей или нет. Это сделано для корректной работы метода navigate курсоров при ограничении колонок.

  2. Колонки, являющиеся частью group by выражения материализованных представлений (materialized view) всегда будут попадать в курсор из БД.

  3. При передаче пустого списка полей или при его отсутствии будут выбираться все колонки.

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

1.2.2. Передача параметров в функции

Стоит отметить, что курсор 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 необходимо выполнить следующий код:

PView1Cursor pView1 = new PView1Cursor(context, 5);

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

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

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

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

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

FooCursor foo = new FooCursor(context);
foo.setA(1);
foo.setB("text");
foo.setC(new GregorianCalendar(year, month, day).getTime());
foo.insert();

Соответствие между типами данных CelestaSQL и Java-типами приведено в таблице.

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

foo.setC(new Date());

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

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

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

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

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

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

  • tryGet(…​) Осуществляет поиск записи по ключевым полям, возвращает true, если запись найдена, и false, если записи с таким первичным ключом нет в таблице. В аргументах этого метода должны быть перечислены значения полей первичного ключа, количество аргументов должно быть равно количеству полей первичного ключа таблицы.

  • get(…​) То же, что tryGet, но выбрасывает исключение, если запись не найдена.

  • tryGetCurrent() Извлекает из базы данных запись по текущим значениям полей первичного ключа.

Методы get, tryGet and tryGetCurrent не учитывает никаких фильтров, наложенных на таблицу. Если вам необходимо найти запись с учётом фильтров, то используйте метод [try]First.

  • navigate(command) — осуществляет переход по записям относительно текущего положения. Возвращает true, если переход удачный, и false — если записи не оказалось. При этом строка command может представлять собой произвольный набор из следующих символов-команд, выполняемых одна за другой до тех пор, пока запись не нашлась (или набор команд не исчерпался):

    • - (минус) — переход к первой записи, удовлетворяющей условиям сортировки и фильтрации,

    • + (плюс) — переход к последней записи,

    • > — переход к следующей записи, относительно текущей, удовлетворяющей условиям сортировки и фильтрации,

    • < — переход к предыдущей записи,

    • = — обновление текущей записи, если она попадает в текущий фильтр.

  • tryFirst() — то же, что navigate('-').

  • first() — то же, что tryFirst(), но вызывает ошибку, если запись не найдена.

  • tryLast() — то же, что navigate('+').

  • last() — то же, что tryLast(), но вызывает ошибку, если запись не найдена.

  • next() — то же, что navigate('>').

  • previous() — то же, что navigate('<').

  • tryFindSet() — открывает набор записей (ResultSet) и устанавливает курсор в его начало. Возвращает true, если открывшийся набор не пуст, false — если записей в наборе нет.

  • findSet() — то же, что tryFindSet(), но вызывает ошибку в случае, если переход неудачен.

  • nextInSet() — переход к следующей записи в текущем наборе записей. Если набор не открыт, вызов этого метода эквивалентен вызову tryFindSet(). Возвращает true, если переход состоялся, false — если достигнут конец набора.

  • iterator() — возвращает итератор, позволяющий осуществить полную итерацию по набору записей с первой до последней. Реализует соответствующий метод интерфейса java.lang.Iterable. Например, если переменная rec содержит экземпляр курсора, то полная итерация с использованием метода iterate() может быть осуществлена следующим образом:

 for (FooCursor r: rec): {
         /* здесь внутри цикла всё,
          что вы хотите сделать с записями r */
 }

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

if (cursor.tryFindSet()) {
    while (cursor.nextInSet()) {
        //цикл
    }
}
В чём разница между [try]First() и [try]FindSet()?

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

Чем navigate("=") отличается от tryGetCurrent()?

Метод navigate() учитывает текущие фильтры, а get-методы — не учитывают. Запись с текущим значением первичного ключа может не попасть в фильтр, поэтому navigate('=') может вернуть false в ситуации, когда tryGetCurrent() возвращает true.

Что значит navigate("=><")?

Эта команда предписывает следующий алгоритм: "Попытайся найти текущую запись. Если запись нашлась, выйди и верни true. Если записи уже нет (удалили), сходи вперёд. Если запись нашлась, выйди и верни true. Если впереди ничего нет, сходи назад. Если запись нашлась, верни true, если нет — false.

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

  • setRange(ColumnMeta<?> column) Сброс любого фильтра на поле.

  • setRange(ColumnMeta<? super T> column, T value) Установка диапазона из единственного значения на поле. Передача значения null в качестве аргумента приводит к установке фильтра 'IS NULL' на данное поле.

  • setRange(ColumnMeta<? super T> column, T valueFrom, T valueTo) Установка диапазона «от..до включительно» на поле (на уровне языка SQL соответствует оператору BETWEEN). Использование null в качестве аргумента не допускается.

  • setFilter(ColumnMeta<?> column, String value) Установка фильтра на поле, описание выражений сложных фильтров приведено ниже.

  • setComplexFilter(String value) Установка сложного фильтра на таблицу. Аргумент соответствует условию WHERE запроса на языке CelestaSQL.

  • setIn(BasicCursor auxiliaryCursor) Установка фильтра с вложенным запросом. Описание использования setIn приведено ниже.

  • limit(long skip, long limit) Установка ограничений на возвращаемый диапазон строк. В качестве параметров должны быть неотрицательные целые числа. Параметр skip означает количество строк, которое будет пропущено перед тем, как начнётся выдача (skip = 0 — выдача с самого начала), limit — максимальное число возвращаемых строк, при этом limit = 0 означает возврат всех строк. Вызов limit(0, 0) сбрасывает ограничения на возвращаемый диапазон набора строк. Ограничения, установленные методом limit(), не учитываются при вызове метода count().

  • orderBy(ColumnMeta<?>…​ columns) Установка сортировки. Параметры — перечень полей для сортировки. Чтобы указывать сортировку по возрастанию или по убыванию, необходимо для соответствующего поля воспользоваться методом asc() или desc(). Если метод asc() или desc() не был вызван явно, сортировка идёт по возрастанию. Допускается вызов orderBy() без аргументов, чтобы сбросить все установленные ранее сортировки на сортировку по умолчанию. Поле можно указать не более чем в одном из аргументов метода orderBy(…​).

Следует помнить, что в Celesta не бывает не отсортированных наборов данных: по умолчанию наборы данных в Celesta всегда сортируются по полям первичного ключа, а к любому набору полей, заданному через orderBy(…​), Celesta автоматически добавляет в конец те поля первичного ключа, которые не были перечислены в аргументах. Для представлений и WITH READ ONLY таблиц, у которых поля первичного ключа не заданы, Celesta использует для сортировки по умолчанию первое поле. Всё это реализовано для того, чтобы итерация по записям курсора была детерминированной.

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

  • reset() Сброс фильтров и сортировки, с сохранением значений полей буфера.

  • clear() Сброс фильтров, сортировки и полная очистка буфера, включая ключевые поля.

  • init() Очистка всех полей буфера, кроме ключевых.

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

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

  • copyOrderFrom(BasicCursor c) Перенос настроек сортировки из курсора с тем же типом в текущий курсор.

  • copyFieldsFrom(BasicCursor c) Перенос значений всех полей из курсора с тем же типом в текущий курсор.

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

  • insert() Вставка содержимого курсора в БД. При этом если запись с таким первичным ключом уже существует, возникает ошибка.

  • tryInsert() Вставка содержимого курсора в БД. true если получилось, false если запись с таким первичным ключом уже существует

  • update() Сохранение содержимого курсора в БД, выбрасывая исключение в случае, если запись с такими ключевыми полями не найдена.

  • tryUpdate() Сохранение содержимого курсора в БД, true если получилось, false если запись с таким первичным ключом не существует.

  • delete() Удаление текущей записи.

  • deleteAll() Удаление всех записей, попадающих в фильтр. Внимание: триггер onDelete при этом не вызывается.

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

  • canRead(), canInsert(), canModify(), canDelete() Возвращает булевское значение, указывающее на наличие прав у текущей сессии на выполнение соответствующей операции.

  • count() Возвращает количество записей в отфильтрованном наборе. В частности, если фильтров на курсор не установлено, возвращает полное количество записей в таблице. Ограничения на набор записей, установленные методом limit(), не учитываются при вызове метода count().

  • callContext() Возвращает контекст вызова, на котором создан данный курсор.

  • meta() Возвращает описание таблицы или представления (метаинформацию, экземпляр класса org.javacc.test.celesta.Table/View).

  • asCSVLine() Возвращает значение полей курсора в виде CSV-строки с разделителями-запятыми.

  • getMaxStrLen(ColumnMeta<String>) Возвращает длину текстового поля (в символах). Метод необходим для определения длины, до которой необходимо обрезать строку, передаваемую в базу данных. Возвращает -1, если поле определено как TEXT.

  • getXRec() Возвращает копию буфера, содержащую значения, полученные при последнем чтении данных из базы.

Обратите внимание, что методы 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-методов», использование которых в подавляющем большинстве случаев оправдано только если предполагается явное использование возвращаемых значений этих методов.

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

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

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

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

  • числовых либо текстовых литералов (в зависимости от типа поля),

  • литерала null,

  • логических операторов &, |, !,

  • операторов сравнения <, >, ..,

  • группирующих скобок (, ),

  • специальных операторов @ и % для текстовых полей.

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

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

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

! null bit_blob_filter

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

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

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

! term ( filter ) & | filter

Здесь

  • & — знак логического И,

  • | — знак логического ИЛИ,

  • ! — знак логического НЕ,

  • (, ) — группирующие скобки.

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

null numeric literal .. numeric literal .. > < numeric literal numeric_term

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

(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

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

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

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

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

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

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

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

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

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

@ null text literal .. text literal .. > < text literal % text literal % % text_term

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

('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

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

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

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

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

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

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

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

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

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

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

  1. устанавливаются фильтры на целевом и вспомогательных курсорах (в приведённом выше примере myCursor играет роль целевого, а city — вспомогательного),

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

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

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

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

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

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

TargetCursor a = new TargetCursor(context);
a.setRange(a.COLUMNS.afoo(), "aBar");
AuxiliaryCursor b = new AuxiliaryCursor(context);
b.setRange(b.COLUMNS.bFoo(), "bBar");
Auxiliary2Cursor c = new Auxiliary2Cursor(context);
c.setRange(c.COLUMNS.cFoo(), "cBar");
a.setIn(b)
     .add(a.COLUMNS.a1(), b.COLUMNS.b1())
     .add(a.COLUMNS.a2(), b.COLUMNS.b2());
.and(c)
     .add(a.COLUMNS.a1(), c.COLUMNS.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:

a.setIn(b).add(a.CURSORS.a2(), b.CURSORS.b2());

4. Класс Sequence

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

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

CREATE SCHEMA sequences version '1.0';
CREATE SEQUENCE idNumerator START WITH 3;
IdNumeratorSequence sq  = new IdNumeratorSequence(ctx);
//выводит следующее значение, начиная с 3.
System.out.println(sq.nextValue());