Java Persistence. Часть -1. До JDBC: как Java работала с базами данных

В предыдущих статьях я уже рассмотрел преимущества Hibernate/JPA и переход к ним с JDBC, а потом даже переход к Spring Data. Но теперь хотелось бы двинуться в обратную сторону и попасть в «доисторические» времена – до всех этих ORM-систем. В этой статье вспомню, что было в Java изначально с обменом данными, чтобы современные решения казались ещё более удобными.

До Java эры: CLI и ODBC

Стандарты «общения» с базами данных начали писать ещё в 1960-х: например, на CODASYL придумывали, как работать с IDS Чарльза Бахмана из COBOL. Но история развивалась так стремительно, и сама идея баз данных быстро эволюционировала, что все эти решения были весьма точечными.

К 1990-м реляционные базы данных стали стандартом (особенно благодаря работам Эдгара Кодда), а в лидеры вырвался язык запросов SQL от IBM (ранее SEQUEL), который одолел своего основного врага QUEL. Он использовался в нескольких базах данных и, соответственно, во многих программах.

Чтобы как-то стандартизировать API для работы с БД комитет SQL Access Group (SAG) начал работу над CLI (Call Level Interface, также SQL/CLI). Этот стандарт должен был описать единый способ обращения к базам данных. Изначально этот стандарт существовал лишь для языков C и Cobol.

Этот стандарт тесно связан с ODBC (Open Database Connectivity), который параллельно разрабатывали Microsoft и Simba Technologies. Логично предположить, что сначала появился общий стандарт, но нет, разработка ODBC началась раньше, и CLI построили на его основе. ODBC был ориентирован на экосистему C/C++.

Джавы в начале 90-х ещё не было, поэтому и ожидать совместимости с этим языком было нельзя. Но что случилось, когда этот язык всё же появился?

Работа с базами в Java до появления JDBC

За первые пару лет существования Java разработчикам пришлось немало помучиться с базами. Стандарт ODBC существовал, значит, были API для работы с базами данных, но все они были для другого языка. Поэтому существовало несколько решений этой проблемы – ни в одной из них код в Java даже не подозревал о существовании базы и обращался к сторонним библиотекам.

Апплеты и CGI-скрипты

Первым вариантом было использовать апплеты, которые обращались к скриптам на Perl или C. CGI скрипты уже формировали запрос к базе, на основе параметров в HTTP-запросе и отдавали данные из базы. По факту это прообраз сервлетов с разницей лишь, что каждый запрос создавал на сервере новый процесс, что со временем стало нагружать сервера – именно поэтому эта технология устарела и перестала использоваться.

Например, подключение к базе могло выглядеть так:

URL url = new URL("http://server/query.cgi?id=1");
InputStream in = url.openStream();

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

То есть для получения информации по книгам нужно было бы работать примерно так:

public class BookApplet extends Applet {
private TextArea output;
public void init() {
output = new TextArea();
add(output);
loadBooks();
}
private void loadBooks() {
Vector books = new Vector();
try {
URL url = new URL("http://library.com/books.cgi");
DataInputStream in = new DataInputStream(url.openStream());
String line;
while ((line = in.readLine()) != null) {
StringTokenizer tokenizer = new StringTokenizer(line, ";");
Book book = new Book();
book.setId((new Long(tokenizer.nextToken())).longValue());
book.setTitle(tokenizer.nextToken());
book.setAuthor(tokenizer.nextToken());
books.addElement(book);
}
in.close();
} catch (Exception e) {
output.appendText( "Error: " + e.toString()
);
}
}
}
}

Десять лет спустя тот подход выглядел бы немного более дружелюбно – больше знакомых слов (и даже уже появились бы generics), но всё равно сейчас кажется несколько дико:

URL url = new URL("http://library.com/book.cgi?id=1");
BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));
List<Book> books = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split(";");
Book book = new Book();
book.setId(Long.parseLong(parts[0]));
book.setTitle(parts[1]);
book.setAuthor(parts[2]);
books.add(book);
}

Сам скрипт при этом выглядел бы так:

#!/usr/bin/perl
use DBI;
my $dbh = DBI->connect(
"dbi:Oracle:LIBRARY",
"scott",
"tiger"
);
my $sth = $dbh->prepare(
"SELECT id, title, author FROM BOOK"
);
$sth->execute();
print "Content-Type: text/plain\n\n";
while (my @row = $sth->fetchrow_array()) {
print $row[0]
. ";"
. $row[1]
. ";"
. $row[2]
. "\n";
}
$sth->finish();
$dbh->disconnect();

JNI

Так как к появлению Java уже существовали библиотеки на C и C++, которые позволяли обращаться к базам, то можно было ещё работать с JNI. JNI (Java Native Interface) – механизм, позволяющий Java-коду вызывать нативные библиотеки, написанные на C или C++.

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

public class OracleClient {
static {
System.loadLibrary("myapp");
}
public native void connect(String server, String user, String password);
public native String getBookByAuthorAndTitle(String author, String title);
public native void disconnect();
}

Здесь система ищет библиотеку myapp.dll и загружает её в память процесса. Если тела метода нет, то он ищется в библиотеке. В myapp.dll используется стандартная библиотека oracle (например) с методами соединения с базой, но разбор конкретных таблиц нужно расписывать самим. Получится что-то типа этого

#include <jni.h>
#include <oci.h>
#include <string.h>
extern OCIEnv *g_env;
extern OCISvcCtx *g_svc;
extern OCIError *g_err;
JNIEXPORT jstring JNICALL
Java_OracleClient_getBookByAuthorAndTitle(JNIEnv* env, jobject obj, jstring author, jstring title)
{
OCIStmt *stmt = NULL;
OCIBind *authorBind = NULL;
OCIBind *titleBind = NULL;
OCIDefine *titleDefine = NULL;
const char* authorName = env->GetStringUTFChars(author, NULL);
const char* bookTitle = env->GetStringUTFChars(title, NULL);
char resultTitle[256];
memset(resultTitle, 0, sizeof(resultTitle));
sb2 resultTitleInd = 0;
const char* sql =
"SELECT title "
"FROM BOOK "
"WHERE author = :author "
"AND title = :title";
OCIHandleAlloc(g_env, (void**) &stmt, OCI_HTYPE_STMT, 0, NULL);
OCIStmtPrepare(stmt, g_err, (text*) sql, (ub4) strlen(sql), OCI_NTV_SYNTAX, OCI_DEFAULT);
OCIBindByName(stmt, &authorBind, g_err, (text*) ":author", -1, (void*) authorName,
(sb4) strlen(authorName) + 1, SQLT_STR, NULL, NULL, NULL, 0, NULL, OCI_DEFAULT);
OCIBindByName(stmt, &titleBind, g_err, (text*) ":title", -1, (void*) bookTitle,
(sb4) strlen(bookTitle) + 1, SQLT_STR, NULL, NULL, NULL, 0, NULL, OCI_DEFAULT);
OCIDefineByPos(stmt, &titleDefine, g_err, 1, resultTitle, sizeof(resultTitle),
SQLT_STR, &resultTitleInd, NULL, NULL, OCI_DEFAULT);
OCIStmtExecute(g_svc, stmt, g_err, 0, 0, NULL, NULL, OCI_DEFAULT);
sword fetchStatus = OCIStmtFetch2(stmt, g_err, 1, OCI_FETCH_NEXT, 0, OCI_DEFAULT);
jstring result;
if (fetchStatus == OCI_SUCCESS && resultTitleInd != -1) {
result = env->NewStringUTF(resultTitle);
} else {
result = env->NewStringUTF("");
}
env->ReleaseStringUTFChars(author, authorName);
env->ReleaseStringUTFChars(title, bookTitle);
OCIHandleFree(stmt, OCI_HTYPE_STMT);
return result;
}

За точность не ручаюсь, ведь я на C не пишу, и в 95-м не жил, но общее представление, что и сколько нужно было написать, теперь есть. Но в общем даже немного похоже на привычный нам JDBC (по крайней мере, логика и сам SQL-синтаксис с аргументами), просто вызов методов содержит очень много параметров (в Java они теперь далеко не все обязательны).

После этого работа в Java с объектами происходит так:

OracleClient client = new OracleClient();
client.connect("oracle.company.com", "scott", "tiger");
String book = client.getBookByAuthorAndTitle("Jane Austen", "Pride and Prejudice");
client.disconnect();
System.out.println(book);

Проблемой этого подхода было не только то, что нужно было владеть несколькими языками (зачем тогда нужна Java, если так много пишем на C?), а ещё то, что у каждой базы были свои библиотеки, с которыми работа могла отличаться. То есть клиенты были не универсальными.

Можно было ещё пользоваться специальными клиентами, написанными для Java, но их было ещё не так много, и работа с каждым отличалась – так что инструментов хватало, но все они были настолько разными, что напрашивался какой-то стандарт. Именно поэтому в 1997-м появился JDBC.

JDBC и JDBC-ODBC Bridge

В 1997 году в составе JDK 1.1 появился JDBC (Java Database Connectivity). Фактически он стал Java-аналогом идеи ODBC. Теперь разработчик получал единый API: Connection, Statement, PreparedStatement, ResultSet.

ResultSet стал настоящим прорывом. Независимо от содержимого таблицы, результат выглядел одинаково. И хоть парсить данные всё ещё приходилось вручную, делать это можно было намного легче, без токенов, потоков и строк:

Connection connection =
DriverManager.getConnection(
"jdbc:oracle:thin:@localhost:1521:xe",
"user",
"password"
);
PreparedStatement ps =
connection.prepareStatement(
"SELECT id, title FROM book WHERE id = ?"
);
ps.setLong(1, 1L);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
long id = rs.getLong("id");
String title = rs.getString("title");
Book book = new Book();
book.setId(id);
book.setTitle(title);
}

Таким образом, JDBC предложил инструменты для единого формата установки подключения, создания запросов к базе, представления результатов запроса и работы с ними.

Но всё ещё оставалась маленькая проблема – драйверы всё ещё были написаны на C, и использовать их из Java приходилось с JNI. Стандарт существовал, но библиотеки ещё нет. Поэтому появился JDBC-ODBC bridge – мост между двумя (и без того совместимыми) стандартами. Это позволяло использовать JDBC, но под капотом всё ещё обращаться к существующим драйверам, а не переписывать их.

Сначала нужно было создать источник данных. Пароль, логин и адрес не прописывались в соединении в Java-коде, а описывались именно в этом источнике. Я даже помню времена, когда NetBeans предлагал функцию для создания этих источников.

Данные о базе хранились в системе. Например, нужно было создать LibraryDB и прописать все данные для подключения. Тогда уже в Java можно было работать с этим источником:

Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Connection connection = DriverManager.getConnection("jdbc:odbc:LibraryDB");
Statement statement = connection.createStatement();
ResultSet rs = statement.executeQuery(
"SELECT id, title, author "
+ "FROM BOOK"
);

Для разработчика здесь не было больших проблем, но мы получали дополнительные слои между Java и базой данных. Между ними появлялся мост и ODBC, потом ODBC драйвер. Всё это способствовало потере производительности.

Ещё стоит понимать, что ODBC в первую очередь был заточен под Windows. А Java пропагандировала кросс-платформенность: запускай везде и всё такое.

Также ODBC старался быть стандартом, поэтому игнорировал некоторые уникальные возможности каждой базы. А JDBC-драйверы конкретных СУБД позволяют использовать специфические возможности.

По мере появления драйверов этот «адаптер» больше был не нужен, и его окончательно удалили из Java 8 (поэтому мой код на лабах в универе резко перестал работать, и я удивился отсутствию обратной совместимости, о которой так много говорили).


Напоследок предлагаю сравнить подход образца 95-го с JNI и более современный – JDBC. Насколько легче стала жизнь!

public class OracleClient {
static {
System.loadLibrary("library");
}
public native String[] getBooksByAuthor(String author);
}
#include <jni.h>
#include <oci.h>
#include <string.h>
extern OCIEnv *g_env;
extern OCISvcCtx *g_svc;
extern OCIError *g_err;
JNIEXPORT jobjectArray JNICALL
Java_OracleClient_getBooksByAuthor(JNIEnv* env, jobject obj, jstring author)
{
OCIStmt *stmt = NULL;
OCIBind *authorBind = NULL;
OCIDefine *titleDefine = NULL;
const char* authorName = env->GetStringUTFChars(author, NULL);
char resultTitle[256];
sb2 resultTitleInd = 0;
const char* sql =
"SELECT title "
"FROM BOOK "
"WHERE author = :author";
OCIHandleAlloc(g_env, (void**) &stmt, OCI_HTYPE_STMT, 0, NULL);
OCIStmtPrepare(stmt, g_err, (text*) sql, (ub4) strlen(sql),
OCI_NTV_SYNTAX, OCI_DEFAULT);
OCIBindByName(stmt, &authorBind, g_err, (text*) ":author", -1,
(void*) authorName, (sb4) strlen(authorName) + 1,
SQLT_STR, NULL, NULL, NULL, 0, NULL, OCI_DEFAULT);
OCIDefineByPos(stmt, &titleDefine, g_err, 1,
resultTitle, sizeof(resultTitle),
SQLT_STR, &resultTitleInd,
NULL, NULL, OCI_DEFAULT);
OCIStmtExecute(g_svc, stmt, g_err, 0, 0, NULL, NULL, OCI_DEFAULT);
jobjectArray result =
env->NewObjectArray(
10,
env->FindClass("java/lang/String"),
NULL
);
int index = 0;
while (index < 10 && OCIStmtFetch2(stmt, g_err, 1, OCI_FETCH_NEXT, 0, OCI_DEFAULT) == OCI_SUCCESS) {
if (resultTitleInd != -1) {
env->SetObjectArrayElement(result, index,env->NewStringUTF(resultTitle));
}
index++;
memset(resultTitle, 0, sizeof(resultTitle));
}
env->ReleaseStringUTFChars(author, authorName);
OCIHandleFree(stmt, OCI_HTYPE_STMT);
return result;
}
OracleClient client = new OracleClient();
String[] books = client.getBooksByAuthor("Jane Austen");
for (int i = 0; i < books.length;i++) {
System.out.println(books[i]);
}

Всё это стало десять лет спустя JDBC-ODBC:

Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Connection connection = DriverManager.getConnection("jdbc:odbc:LibraryDB");
PreparedStatement ps =
connection.prepareStatement(
"SELECT title "
+ "FROM BOOK "
+ "WHERE author = ?"
);
ps.setString(1, "Jane Austen");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("title")
);
}

Но даже с тех пор мы очень много упростили. Так стало ещё десять лет спустя с Spring Data JPA:

public interface BookRepository extends JpaRepository<Book, Long> {
List<Book> findByAuthor(String author);
}
List<Book> books = bookRepository.findByAuthor("Jane Austen");
for (Book book : books) {
System.out.println(book.getTitle());
}

Оставить комментарий