В предыдущих статьях я уже рассмотрел преимущества 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/perluse 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 JNICALLJava_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 JNICALLJava_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());}