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 [Архівовано 12 лютого 2009 у Wayback Machine.]
Див. Embedded SQLJ User’s Guide
Посилання[ред. | ред. код]
- Эндрю Эйзенберг, Джим Мелтон. Связывания для объектных языков. Архів оригіналу за 25 серпня 2011. Процитовано 12 листопада 2008.
{{cite web}}
: Cite має пусті невідомі параметри:|description=
та|datepublished=
(довідка) - Эндрю Эйзенберг, Джим Мелтон. SQLJ – Часть 1. Архів оригіналу за 25 серпня 2011. Процитовано 12 листопада 2008.
{{cite web}}
: Cite має пусті невідомі параметри:|description=
та|datepublished=
(довідка) - IBM Redbooks. DB2 for z/OS and OS/390: Ready for Java. Архів оригіналу за 25 серпня 2011. Процитовано 12 листопада 2008.
{{cite web}}
: Cite має пусті невідомі параметри:|description=
та|datepublished=
(довідка) - Oracle Database 11g. SQLJ Developer's Guide and Reference. Архів оригіналу за 25 серпня 2011. Процитовано 12 листопада 2008.
{{cite web}}
: Cite має пусті невідомі параметри:|description=
та|datepublished=
(довідка)
Зовнішні посилання[ред. | ред. код]
- http://sqlj.org/ [Архівовано 4 січня 2014 у Wayback Machine.]
- IBM Redbook: DB2 for z/OS and OS/390: Ready for Java [Архівовано 18 травня 2011 у Wayback Machine.]