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);

Виходячи з досвіду програмування, можна зробити припущення, що

  1. Значення змінної i в процесі розбору SQL-виразу не змінюватиметься;
  2. Сформований запит буде мати вигляд
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

Отже:

  1. Після виконання SQLJ-директиви матиме місце i = 4;
  2. Виконуватись буде запит
SELECT result
    FROM    table1
    WHERE   field1 = :(x[1]) AND field2 = :(y[2]) AND field3 = :(z[3])

КонтекстиРедагувати

В термінології SQLJ і JDBC контекстом підключення називається сукупність із трьох параметрів, що однозначно ними визначається:

  1. назва бази даних;
  2. ідентифікатор сесії;
  3. ідентифікатор активної транзакції.

Для будь-якої 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. Ситуація ускладнюється тим, що ці технології не паралельні й не повністю взаємозамінні, а перебувають одна над одною архітектурно.

  1. Запит однакового призначення, записаний у JDBC-викликах і в SQLJ-директиві, в більшості випадків буде більш компактно записано в тексті програми саме у другому випадку, що зменшує розмір лістингу й імовірність помилки, пов'язаної зі збиранням підсумкового рядка запиту з невеликих фрагментів;
  2. Будь-яка SQLJ-директива на етапі компіляції розбирається й перевіряється препроцесором, отже, всі помилки синтаксису виявляються ще на цьому етапі, на відміну від JDBC, де контролюється правильність конструкцій тільки з точки зору синтаксису Java — за розбір і правильність власне запиту відповідає вже СУБД, що, звичайно ж, призводить до того, що такі помилки будуть виявлені вже на етапі запуску;
  3. Власне сам препроцесор (що здебільшого має назву sqlj) не входить до JDK; він і необхідні для його роботи бібліотеки звичайно надаються виробниками СУБД. Це закономірно — як показано вище, SQLJ значно ближчий до СУБД, ніж власне до мови Java; більше того, препроцесор має враховувати особливості SQL-синтаксису «своєї» СУБД;
  4. В більшості випадків — особливо це стосується часто виконуваних складних запитів, що працюють з великими масивами даних, — SQLJ-директива буде виконуватись в середньому швидше, ніж аналогічний набір JDBC-викликів. Це пов'язано з тим, що план для відповідного запиту в випадку SQLJ-директиви буде побудовано лише один раз, а потім використовуватиметься повторно, на відміну від JDBC, де побудова плану здійснюватиметься при кожному виклику;
  5. Створюваний при трансляції SQLJ-директиви план запиту за необхідності може піддаватись налаштуванню з боку користувача; в випадку JDBC така можливість зі зрозумілих причин відсутня;
  6. Якщо запит вимагає значних змін у кожному конкретному випадку (простий приклад: пошуковий запит за набором полів, значення в частині яких можуть бути відсутніми), то простіше використати JDBC, оскільки переваг у використанні SQLJ тут нема;
  7. Оскільки при використанні 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

ПосиланняРедагувати

  1. Эндрю Эйзенберг, Джим Мелтон. Связывания для объектных языков. Архів оригіналу за 2011-08-25. Процитовано 2008-11-12. 
  2. Эндрю Эйзенберг, Джим Мелтон. SQLJ – Часть 1. Архів оригіналу за 2011-08-25. Процитовано 2008-11-12. 
  3. IBM Redbooks. DB2 for z/OS and OS/390: Ready for Java. Архів оригіналу за 2011-08-25. Процитовано 2008-11-12. 
  4. Oracle Database 11g. SQLJ Developer's Guide and Reference. Архів оригіналу за 2011-08-25. Процитовано 2008-11-12. 

Зовнішні посиланняРедагувати