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

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

  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 [Архівовано 12 лютого 2009 у Wayback Machine.]

Див. Embedded SQLJ User’s Guide

Посилання[ред. | ред. код]

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

Зовнішні посилання[ред. | ред. код]