SQLJ
SQLJ — підмножина стандарту SQL, спрямована на об'єднання переваг синтаксису мов SQL та Java заради зручності реалізації бізнес-логіки та роботи з даними. Цей стандарт розроблено консорціумом, що складається з компаній IBM, Micro Focus, Microsoft, Compaq (точніше, його підрозділ, що займаєтся СУБД, котрий, скоріш, можна віднести до придбаної компанії Tandem), Informix, Oracle, Sun та Sybase.
Зміст |
Передісторія [ред.]
На момент появи консорціуму JSQL (що згодом став однойменим зі розробленим ним стандартом) в 1997 році ідея про взаємодію реляційних СУБД і програм на Java була не нова. Компанією JavaSoft (дочірнім підрозділом компанії Sun) уже було розроблено інтерфейс JDBC (англ. Java DataBase Connectivity — «з'єднання з БД засобами Java»), включений у стандарт мови, починаючи з моменту випуска JDK 1.1. Однак унаслідок певних причин (див. «SQLJ і JDBC») можливостей, що надаються цим інтерфейсом, було недостатньо.
Специфікація стандарту SQLJ складається з трьох частин:
- Рівень 0 регламентує вбудовування SQL-операторів у текст програми на Java;
- Рівень 1 визначає зворотне включення, а саме, реалізацію в СУБД, що використовують SQL, збережених процедур і функцій на мові Java;
- Рівень 2 встановлює відповідність між типами даних.
До кінця 1998 року всі три рівні специфікації були завершені й подані до розгляду в ANSI як дополнення до стандарту SQL. Перші дві частини нового стандарту були включені відповідно в частини SQL/OLB й SQL/PSM стандарту SQL:1999; третя частина увійшла як окремий модуль SQL/JRT до стандарту SQL:2003
Зазвичай у застосуванні до розробки додатків, що працюють з БД, під SQLJ здебільшого розуміють саме рівень 0.
Приклад коду [ред.]
Наведемо простий приклад Java-класу, що використовує SQLJ для отримання результатів запиту з Oracle.
import java.sql.*; import oracle.sqlj.runtime.Oracle; public class SingleRowQuery extends Base { public static void main(String[] args) { try { connect(); singleRowQuery(1); } catch (SQLException e) { e.printStackTrace(); } } public static void singleRowQuery(int id) throws SQLException { String fullname = null; String street = null; #sql { SELECT fullname, street INTO :OUT fullname, :OUT street FROM customer WHERE ID = :IN id}; System.out.println("Customer with ID = " + id); System.out.println(); System.out.println(fullname + " " + street); } }
З розгляду наведеного коду зрозуміло, що в сам текст процедури singleRowQuery вбудовується SQL-запит, і це вбудовування організовано за певними правилами:
- Текст запиту знаходиться всередині директиви
#sql {...}; - Змінні, зовнішні відносно SQL-запиту, задаються в ньому всередині в певному форматі
Детально всі синтаксичні конструкції буде розглянуто далі.
SQLJ і JDBC [ред.]
В чому ж причини створення двох паралельних стандартів для реалізації технологій доступу до СУБД?
Для початку слід зазначити, що SQLJ і JDBC належать до різних родин стандартів і концептуально вони різні. JDBC є API, що входить у стандарт мови Java й орієнтований на передачу сформованої програмою SQL-конструкції в БД, а також обробку результату. SQLJ же є підмножиною стандарту SQL SQL/OLB — для нього первинним є поняття бази даних, а мова, в яку включаються SQL-конструкції, вторинний. Згідно з цим стандартом вбудовування SQL-операторів допускається не лише в Java, але й у мови програмування Ada, C, COBOL, Fortran, MUMPS, PL/1.
Далі, використання SQLJ насправді неявно означає виклик JDBC-методів, оскільки в даному випадку вони виконують роль відповідно високо- й низькорівневого API. Якщо заглибитися в подробиці реалізації технологій SQLJ і JDBC, то можна виявити, що будь-які SQLJ-директиви прозоро для програміста спеціальною підсистемою (SQLJ-препроцесором) транслюются в JDBC-виклики. Завдяки цьому можна спокійно поєднувати в одному фрагменті коду SQLJ- і JDBC-виклики, за необхідності використовуючи спільний контекст.
Насправді в кожному конкретному випадку, коли потрібне виконання SQL-оператора, вибір між SQLJ і JDBC слід робити, виходячи з характеру очікуваної операції. Якщо це складний пошуковий запит з можливими варіаціями за кількістю умов на пошук — тоді однозначно доцільніше формування текстового рядка запиту й подальше його виконання через JDBC; якщо ж потрібна просто підстановка якихось змінних або обчислюваних виразів — тоді ергономічніше з точки зору довжини коду буде написати SQLJ-директиву.
Синтаксис [ред.]
Для того, щоб ефективно використовувати синтаксичні нововведення, внесені стандартом SQLJ, необхідно попередньо розібратися в їх особливостях, пов'язаних з процесом розбору SQLJ-конструкцій.
Будь-які SQLJ-конструкції починаються з директиви #sql, зокрема, блоки, що містять власне SQL-запросы, задаються як #sql {…}.
Зовнішні змінні [ред.]
В термінології SQLJ зовнішньою змінною (англ. host variable) называється змінна SQLJ-конструкції, використовувана для отримання значень чи передачі їх у зовнішнє відносно конструкції програмне середовище. Наприклад:
int i, j; i = 1; #sql { SELECT field INTO :OUT j FROM table WHERE id = :IN i }; System.out.println(j);
Зовнішні змінні для уникнення неоднозначностей мають задаватися в певному вигляді, а саме:
:[IN|OUT|INOUT] <ім'я змінної>.
Модифікатори IN, OUT, INOUT опціональні й використовуються для вказання змінних, котрі, власне, передають значення з-за меж усередину SQLJ-конструкції; повертають значення значение назовні й виконують обидві функції. Дані ключові слова вживаються не лише для цього — також вони задають метод доступу до зовнішніх змінних усередині SQLJ-конструкції: за наявності модифікатора IN можливе лише читання значення змінної, за наявності OUT — лише запис, за наявності INOUT — повний доступ. За замовчуванням (за відсутності явно заданого модифікатора) змінні оголошуються з неявним модифікатором INOUT.
Зовнішні вирази [ред.]
Замість просто змінних в SQLJ-конструкціях можна використовувати вирази, що містять зовнішні змінні, що частіше називаються просто зовнішніми виразами (англ. host expressions). Вони мають певний синтаксис:
:( <выражение> )
Основний нюанс при використанні зовнішніх виразів полягає в тому, що їх використання може потягнути за собою певні наслідки, пов'язані з тим, що розбір SQLJ-конструкції препроцесором за наявності кількох зовнішніх виразів іде в визначеному порядку, а при використанні в виразах присвоювань результат присвоєння може передаватися в програмне середовище.
Для ілюстрації цих двох моментів розберімо простий приклад використання зовнішніх виразів:
int i = 1; #sql { SELECT result FROM table1 WHERE field1 = :(x[i++]) AND field2 = :(y[i++]) AND field3 = :(z[i++]) }; System.out.println(i);
Виходячи з досвіду програмування, можна зробити припущення, що
- Значення змінної
iв процесі розбору SQL-виразу не змінюватиметься; - Сформований запит буде мати вигляд
SELECT RESULT FROM table1 WHERE field1 = :(x[1]) AND field2 = :(y[1]) AND field3 = :(z[1])
Однак і перше, і друге твердження — хибні. Для перевірки цього складімо просту схему, що прояснить порядок розбору даної конструкції SQLJ-препроцесором:
i = 1
x[i++] > x[1], i = 2
y[i++] > y[2], i = 3
z[i++] > z[3], i = 4
Отже:
- Після виконання SQLJ-директиви матиме місце
i = 4; - Виконуватись буде запит
SELECT RESULT FROM table1 WHERE field1 = :(x[1]) AND field2 = :(y[2]) AND field3 = :(z[3])
Контексти [ред.]
В термінології SQLJ і JDBC контекстом підключення називається сукупність із трьох параметрів, що однозначно ними визначається:
- назва бази даних;
- ідентифікатор сесії;
- ідентифікатор активної транзакції.
Для будь-якої SQLJ-конструкції контекст, у якому вона буде виконуватись, можна визначити явно: #sql [<контекст>] {…}.
В рамках директиви #sql можна також створювати нові контексти для наступного використання: #sql context <контекст>. Якщо контекст явно не задано, то конструкція вважається виконуваною в контексті за замовчуванням (англ. default context). За необхідності контекст за замовчуванням може бути зміненим.
Ітератори [ред.]
Ітератором в термінології стандарта SQLJ називаєтся об'єкт для зберігання результату запиту, що повертає більше одного запису. За своєю суттю й реалізацією він являє собою не просто множину записів, а множину деяким упорядкуванням у ній, що дозволяє обробляти отримані записи послідовно. В цьому плані ітератор має багато спільного з курсором.
Стандартом передбачені два типи ітераторів — різниця між ними достатньо цікава: ітератори з прив'язкою за позицією — вимагають більш SQL-подібного синтаксису, на відміну від ітераторів з прив'язкою за стовпчиками, котрі дуже близькі за способом використання до об'єктів.
Ітератори з прив'язкою за позицією [ред.]
Першим типом ітератора є ітератор з прив'язкою за позиціями. Він оголошується так: #sql public iterator ByPos (String, int). Ясно видно, що в даному випадку прив'язка результатів запиту до ітератора здійснюється просто за збігом типів даних між ітератором і результатом запиту. Однак для цього потрібно, щоб типи данних у ітератора й результату запиту могли бути відображені один на одного відповідно до стандарту SQL/JRT.
Створімо просту таблицю:
CREATE TABLE people ( fullname VARCHAR(50), birthyear NUMERIC(4,0))
Тепер з допомогою ітератора першого типу й конструкції FETCH … INTO … проведімо вибір даних з результату запиту:
ByPos positer; String name = null; int year = 0; #sql positer = {SELECT fullname, birthyear FROM people}; for(;;) { #sql {FETCH :positer INTO :name, :year}; if (positer.endFetch()) break; System.out.println(name + " was born in " + year); }
Першою директивою здійснюється прив'язка результату запиту до ітератора; другою з допомогою конструкції FETCH … INTO … з результату послідовно зчитується по одному запису.
Ітератори з іменуванням стовпчиків [ред.]
Другим типом ітератора, більш наближеного за використанням до звичайних об'єктів, є ітератор з іменуванням стовпчиків. Для вказаної таблиці створення ітератора другого типу буде виглядати так:
#sql public iterator ByName ( String fullNAME, int birthYEAR);
Використовується він як звичайний об'єкт, а саме, доступ до полів здійснюється через відповідні акцесорні методи:
ByName namiter; #sql namiter = {SELECT fullname, birthyear FROM people}; String s; int i; while (namiter.next()) { i = namiter.birthYEAR(); s = namiter.fullNAME(); System.out.println(s + " was born in "+i); }
Однак існує правило, яке має виконуватись — імена полів ітератора мають збігатися (без урахування регістру) з іменами полів у запиті. Це по'язано з процесом розбору SQLJ-конструкції препроцесором. У випадку, якщо ім'я стовпчика в БД має назву, несумісну з правилами іменування змінних у Java, необхідно використовувати псевдоніми в запиті, що формує ітератор.
Виклики процедур і функцій [ред.]
Виклики процедур дуже просто записуються з використанням зовнішніх змінних:
#sql {CALL proc (:myarg)};
Функції, в свою чергу, викликаються з використанням конструкції VALUE
int i; #sql i = {VALUES(func(34))};
Взаємодія з JDBC [ред.]
Оскільки SQLJ-директиви при своєму використанні здійснюють JDBC-виклики, то цікавою є можливість використати ці технології разом. Досить легко перетворити ітератори в об'єкти ResultSet і навпаки.
Перетворення об'єкта ResultSet здійснюється дуже просто. Для цього спершу треба визначити ітератор з іменуванням стовпчиків (в нашому прикладі його буде позначено як Employees), а потім виконати операцію CAST:
#sql iterator Employees (String ename, double sal); PreparedStatement stmt = conn.prepareStatement(); String query = "SELECT ename, sal FROM emp WHERE "; query += whereClause; ResultSet rs = stmt.executeQuery(query); Employees emps; #sql emps = {CAST :rs}; while (emps.next()) { System.out.println(emps.ename() + " earns " + emps.sal()); } emps.close(); stmt.close();
Окремо варто зазначити, що після прив'язки результату запиту до ітератора окремо закривати результат запиту непотрібно — це за програміста зробить сам препроцесор.
Зворотний процес — перетворення ітератора в об'ект ResultSet здійснюється з допомогою ітераторів особливого типу, так званих слабо типізованих (англ. weakly typed) ітераторів.
sqlj.runtime.ResultSetIterator iter; #sql iter = {SELECT ename FROM emp}; ResultSet rs = iter.getResultSet(); while (rs.next()) { System.out.println("employee name: " + rs.getString(1)); } iter.close();
В цьому випадку зв'язок між ітератором і результатом запитутакож зберігається й закривати слід саме ітератор.
Плюси й мінуси SQLJ [ред.]
Як уже згадувалось раніше, порівнювати SQLJ як технологію простіше всього з аналогічною Java-орієнтованою технологією того ж призначення, а саме — з JDBC. Ситуація ускладнюється тим, що ці технології не паралельні й не повністю взаємозамінні, а перебувають одна над одною архітектурно.
- Запит однакового призначення, записаний у JDBC-викликах і в SQLJ-директиві, в більшості випадків буде більш компактно записано в тексті програми саме у другому випадку, що зменшує розмір лістинга й імовірність помилки, пов'язаної зі збиранням підсумкового рядка запиту з невеликих фрагментів;
- Будь-яка SQLJ-директива на етапі компіляції розбирається й перевіряється препроцесором, отже, всі помилки синтаксису виявляються ще на цьому етапі, на відміну від JDBC, де контролюється правильність конструкцій тільки з точки зору синтаксису Java — за розбір і правильність власне запиту відповідає вже СУБД, що, звичайно ж, призводить до того, що такі помилки будуть виявлені вже на етапі запуску;
- Власне сам препроцесор (що здебільшого має назву
sqlj) не входить до JDK; він і необхідні для його роботи бібліотеки звичайно надаються виробниками СУБД. Це закономірно — як показано вище, SQLJ значно ближчий до СУБД, ніж власне до мови Java; більше того, препроцесор має враховувати особливості SQL-синтаксису «своєї» СУБД; - В більшості випадків — особливо це стосується часто виконуваних складних запитів, що працюють з великими масивами даних, — SQLJ-директива буде виконуватись в вередньому швидше, ніж аналогічний набір JDBC-викликів. Це пов'язано з тим, що план для відповідного запиту в випадку SQLJ-директиви будет побудовано лише один раз, а потім використовуватиметься повторно, на відміну від JDBC, де побудова плану здійснюватиметься при кожному виклику;
- Створюваний при трансляції SQLJ-директиви план запиту за необхідності може піддаватись налаштуванню з боку користувача; в випадку JDBC така можливість зі зрозумілих причин відсутня;
- Якщо запит вимагає значних змін у кожному конкретному випадку (простий приклад: пошуковий запит за набором полів, значення в частині яких можуть бути відсутніми), то простіше використати JDBC, оскільки переваг у використанні SQLJ тут нема;
- Оскільки при використанні JDBC непотрібен додатковий етап обробки коду — трансляція, то процес компіляції в цьому випадку буде швидшим.
Приклади [ред.]
В наступних прикладах порівнюється синтаксис SQLJ з використанням JDBC.
| JDBC | SQLJ |
|---|---|
| Багаторядковий запит | |
PreparedStatement stmt = conn.prepareStatement(
"SELECT LASTNAME"
+ " , FIRSTNME"
+ " , SALARY"
+ " FROM DSN8710.EMP"
+ " WHERE SALARY BETWEEN ? AND ?");
stmt.setBigDecimal(1, min);
stmt.setBigDecimal(2, max);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
lastname = rs.getString(1);
firstname = rs.getString(2);
salary = rs.getBigDecimal(3);
// Print row...
}
rs.close();
stmt.close();
|
#sql private static iterator EmployeeIterator(
String, String, BigDecimal);
...
EmployeeIterator iter;
#sql [ctx] iter = {
SELECT LASTNAME
, FIRSTNME
, SALARY
FROM DSN8710.EMP
WHERE SALARY BETWEEN :min AND :max
};
do {
#sql {
FETCH :iter
INTO :lastname, :firstname, :salary
};
// Print row...
} while (!iter.endFetch());
iter.close();
|
| Однорядковий запит | |
PreparedStatement stmt = conn.prepareStatement(
"SELECT MAX(SALARY), AVG(SALARY)"
+ " FROM DSN8710.EMP");
rs = stmt.executeQuery();
if (!rs.next()) {
// Error—no rows found
}
maxSalary = rs.getBigDecimal(1);
avgSalary = rs.getBigDecimal(2);
if (rs.next()) {
// Error—more than one row found
}
rs.close();
stmt.close();
|
#sql [ctx] {
SELECT MAX(SALARY), AVG(SALARY)
INTO :maxSalary, :avgSalary
FROM DSN8710.EMP
};
|
| команда INSERT | |
stmt = conn.prepareStatement(
"INSERT INTO DSN8710.EMP (" +
"EMPNO, FIRSTNME, MIDINIT, LASTNAME, HIREDATE, SALARY"
+ ") VALUES (?, ?, ?, ?, CURRENT DATE, ?)");
stmt.setString(1, empno);
stmt.setString(2, firstname);
stmt.setString(3, midinit);
stmt.setString(4, lastname);
stmt.setBigDecimal(5, salary);
stmt.executeUpdate();
stmt.close();
|
#sql [ctx] {
INSERT INTO DSN8710.EMP
(EMPNO, FIRSTNME, MIDINIT, LASTNAME, HIREDATE, SALARY)
VALUES
(:empno, :firstname, :midinit,
:lastname, CURRENT DATE, :salary)
};
|
Підтримка програмними засобами [ред.]
Oracle [ред.]
DB/2 [ред.]
Informix [ред.]
http://www-01.ibm.com/software/data/informix/pubs/library/iif.html
Див. Embedded SQLJ User’s Guide
Посилання [ред.]
- Эндрю Эйзенберг, Джим Мелтон. «Связывания для объектных языков». Процитовано 12 листопада 2008.
- Эндрю Эйзенберг, Джим Мелтон. «SQLJ – Часть 1». Процитовано 12 листопада 2008.
- IBM Redbooks. «DB2 for z/OS and OS/390: Ready for Java». Процитовано 12 листопада 2008.
- Oracle Database 11g. «SQLJ Developer's Guide and Reference». Процитовано 12 листопада 2008.
