- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF93:Java EE
Материал из Linuxformat.
Java EE |
---|
- Java Enterprise Edition Учимся писатьклиент-серверные приложения на Java
Содержание |
Все на базу!
- ЧАСТЬ 5 Ни одно серьезное приложение не обходится без большой базы данных. Александр Бабаев расскажет, что для этого требуется.
В предыдущих статьях цикла рассказывалось, как сделать интернет-приложение: правильно разделить создание интерфейса и бизнес-логику, а также реализовать некоторые технические детали. Но до сих пор мы использовали для хранения записей адресной книги простейшую сериализацию. Настало время сделать все «по-взрослому».
Хранение записей в базе данных имеет и достоинства, и недостатки. Из последних можно указать на необходимость поставки с приложением СУБД (системы управления базами данных), а также изучения специального языка запросов, который помогает сохранять данные в БД и получать их оттуда и достаточно сложен, если изучать его досконально.
Но есть и явные выгоды, иначе СУБД не использовались бы настолько широко, как они используются сейчас. Во-первых, язык, о котором я говорил (так называемый структурированный язык запросов, Structured Query Language, SQL) – более или менее стандартен. Это позволяет изучить его один раз, после чего использовать с разными СУБД. Эти системы также достаточно надежны и отлажены (и имеют приличную скорость работы), что позволяет использовать их в самых сложных ситуациях.
Хочется отметить еще одно свойство реляционных СУБД. Дело в том, что реляционными они называются не просто так. Есть специальная область математики, которая описывает такие объекты, как таблицы и поля, а также отношения (relations) между ними – реляционная алгебра и реляционное счисление. Знание основ этого раздела науки позволяет очень четко представить, что может и чего не может реляционная СУБД. А это, в свою очередь, дает понимание, где ее стоит, а где – не стоит использовать. В мире ПО это очень редкое и очень полезное свойство. Оно означает, что если сделать программу точно по математическим лекалам, то даже в условиях сверхдальнего космоса она будет работать так, как задумано. Значит, на нее можно положиться.
А теперь вернемся из космоса к нашим адресам…
JDBC
Рассмотрим, как можно обеспечить работу с СУБД, используя Java. Есть несколько подходов (например, применение специальных СУБД, которые имеют собственные API для Java), но в подавляющем боль- шинстве случаев вам придется использовать JDBC (Java DataBase Connectivity). Это библиотека, которая позволяет работать с любой СУБД, для которой создан так называемый JDBC-драйвер. Он преобразует запросы JDBC в родные команды СУБД и обратно.
Мы будем использовать самую обычную связку: MySQL + стандартный драйвер MySQL JDBC. Если вы занимаетесь созданием интернет-приложений, то эта СУБД, скорее всего, у вас уже установлена. А драйвер можно скачать с сайта MySQL по адресу: http://dev.mysql.com/downloads/connector/j/5.0.html или найти на нашем DVD. Из всего архива нам понадобится только файл mysql-connector-java-5.0.5-bin.jar.
Работа с JDBC
Для выполнения дальнейших действий СУБД (MySQLd) должна быть запущена.
В целом, работа с JDBC не сильно отличается от работы с любым другим API (в PHP, например, все идеологически очень похоже). Вопервых, нужно подключиться к СУБД, т.е. создать соединение. После работы соединение обязательно нужно закрыть. Пока соединение «открыто», можно опрашивать СУБД, сохранять или получать данные, используя язык SQL.
Вспомним основные и самые простые команды SQL. Научимся создавать записи в БД, изменять и удалять их. Но прежде нужно создать саму базу данных.
Если MySQL был только что установлен, то создать базу данных можно следующей командой:
mysqladmin -uroot create database addressbook
Теперь в этой БД нужно сделать таблицу, которую мы будем использовать для хранения записей. Сделаем маленькую программу на Java, которая выполнит эту работу за нас (см. листинг 1). Заодно посмотрим на общую структуру процесса работы с СУБД.
Листинг 1. Создание таблицы
import java.sql.*; public class CreateTable { public static void main(String[] args) throws Exception { // эта строчка загружает драйвер MySQL, чтобы его можно было использовать Class.forName(“org.gjt.mm.mysql.Driver”).newInstance(); // получим соединение Connection connection = DriverManager. getConnection(“jdbc:mysql://localhost:3306/addressbook”, “root”,“”); try { // создадим «выражение» Statement statement = connection.createStatement(); statement.executeUpdate( “CREATE TABLE `addresses` (\n” + “`name` varchar(200) default NULL,\n” + “`number` varchar(200) default NULL\n” + “`comment` varchar(200) default NULL,\n” + “) DEFAULT CHARSET=utf8”); statement.close(); } finally { // закроем соединение connection.close(); } } }
В этой программе присутствует одна «волшебная» строчка, которая загружает драйвер. Нужно отметить, что в Java 6 она необязательна. Остальное более или менее понятно. Мы открываем соединение (легко видеть, что при этом мы подключаемся к локальному компьютеру, порту 3306 и базе данных addressbook, с именем пользователя root и пустым паролем), потом генерируем выражение (statement) и выполняем SQL-код, который и создает таблицу. Затем выражение «закрывается», после чего в final-блоке закрывается и само соединение. Final- блок нужен для того, чтобы соединение было закрыто независимо от возникновения исключения в блоке try ... catch. Как правило, число одновременных соединений ограничено, а значит, это ценный ресурс, которым нужно пользоваться аккуратно.
Вместо кода, который создает таблицу, можно написать любой другой SQL-запрос, например, добавляющий запись или изменяющий ее. Для получения записи нужен несколько другой код, и его мы обсудим чуть позже.
Чтобы добавить запись в уже созданную таблицу, можно использовать следующий код (См. листинг 2):
Листинг 2. Создание записи в таблице
private void addEntry(String aName, String aNumber, String aComment) throws SQLException { Connection connection = DriverManager. getConnection(“jdbc:mysql://localhost:3306/addressbook”, “root”, “”); try { PreparedStatement statement = connection.prepareStatement( NSERT INTO `addresses` (`name`, `number`, `comment`) VALUES (?, ?, ?)”); statement.setString(1, aName); statement.setString(2, aNumber); statement.setString(3, aComment); statement.executeUpdate(); statement.close(); } finally { connection.close(); } }
Чтобы поменять номер, следует воспользоваться командой UPDATE:
PreparedStatement statement = connection.prepareStatement( “UPDATE `addresses` SET `number`=?, `comment`=? WHERE `name`=?;
Внимательный читатель может заметить, что здесь используется не Statement, а PreparedStatement (предварительно подготовленное выражение). Оба они служат одной цели – с их помощью формируются и выполняются запросы к СУБД. Но Statement формирует запросы и тут же выполняет их, в то время как PreparedStatement сначала подготавливает запрос, затем заполняет его параметры значениями (места вставки параметров – это вопросительные знаки), и только потом выполняет его. PreparedStatement имеет множество преимуществ перед обычными запросами. Во-первых, предварительно подготовленные запросы могут кэшироваться прямо в СУБД. Это ускоряет их выполнение. Кроме того, можно выполнить несколько запросов сразу, используя пакетную обработку. Например, если нужно добавить не одну, а несколько записей, можно поступить примерно так:
- Создать PreparedStatement.
- Заполнить его (statement.set…).
- Вызывать statement.addBatch().
- При необходимости повторить предыдущие два пункта.
- Вызывать statement.executeBatch().
Теперь давайте разберемся, как можно вытащить информацию из БД. Это не намного сложнее (см. листинг 3).
Листинг 3. Получение записи из таблицы
Contact result = null; PreparedStatement statement = connection.prepareStatement( “SELECT `name`, `number`, `comment` FROM `addresses` WHERE `name`=?”); statement.setString(1, aName); ResultSet resultSet = statement.executeQuery(); if (resultSet.next()) { result.setData(resultSet.getString(1), resultSet.getString(2), resultSet. getString(3)); }
Теперь мы вызываем метод statement.executeQuery(), который возвращает resultSet. Согласно документации, он никогда не равен null, поэтому проверку я не произвожу. Далее вызывается метод resultSet. next(), который возвращает true тогда и только тогда, когда в результате есть еще одна запись, и, одновременно с этим, переходит на эту запись. После чего можно получить доступ к полям записи. Я получаю доступ по номерам, мне так проще. Но можно написать и resultSet. getString(“name”), это тоже будет работать.
Но это будет работать плохо
Напомню, что для того, чтобы подключить jar-файл к проекту, нужно указать в параметрах javac и java «-cp [имя jar-файлов через разделитель]», где разделителем является точка с запятой в Windows и двоеточие в Unix.
Почему? Потому что никто в «нормальных» системах не работает с соединениями напрямую, а если работает, то никогда не открывает/закрывает соединение на каждый запрос. Иногда это может занять секунды и минуты. А сам запрос – несколько миллисекунд. Становится ясно, что создание соединения и его закрытие – узкое место, с которым нужно что-то делать. И решение называется «пул соединений». Пул (Pool) – это общий паттерн (см. LXF92), который состоит в следующем:
- У нас есть хранилище объектов. Представим себе оружейный ящик местной шпаны. В нем лежат несколько рогаток. Это и есть хранилище или пул.
- В каждый момент времени любой (кому позволено) может взять рогатку, попользоваться и положить обратно.
- Если рогатки нет, можно подождать, пока кто-нибудь другой не вернет ее в ящик.
Тогда, если время пользования рогаткой невелико (никто не держит ее долго), один пул может обслуживать очень большое количество хулиганов. Схематично пул изображен на рисунке. Видно, что при помощи небольшого количества соединений с ограниченным ресурсом обрабатывается большое количество «пользователей».
То же самое можно проделать и с соединениями. Создается пул, в котором держится открытыми, например, десять соединений. Если нужно произвести операцию с БД, соединение берется из пула, используется и возвращается на место. При этом оно остается открытым. И получается (кроме самого первого раза) – тоже уже открытым. Экономия зачастую просто огромна.
Рис. 1. Схема работы пула.
Полезные «Commons»
Если взглянуть на сайт http://jakarta.apache.org/, то можно увидеть очень много проектов, которые сделаны для Java. Среди них достаточно много хороших. Нам понадобится Commons (http://jakarta.apache.org/commons/). Это проект, в котором собраны маленькие «общие» библиотеки, которые приходится писать каждому программисту. Вместо того, чтобы терять время зря, можно просто скачать нужный пакет и использовать его. Сегодня нам будут интересны commons-dbcp и commons-pool. Их можно загрузить со страниц http://jakarta.apache.org/site/downloads/downloads_commons-dbcp.cgi и http://jakarta.apache.org/site/downloads/downloads_commons-pool.cgi соответственно или взять с нашего DVD. После скачивания нужно вытащить из архивов файлы commons-dbcp-1.2.2.jar и commons-pool-1.3.jar.
Основное предназначение второй библиотеки – создание пулов, первой – пулов соединений с СУБД. Давайте посмотрим, как все это используется. Для начала пул нужно инициализировать. Для этого мы напишем следующий метод:
String connectionString = “jdbc:mysql://localhost:3306/addressbook?user=root&passowrd=”; GenericObjectPool connectionPool = new GenericObjectPool(null); ConnectionFactory connectionFactory = new DriverManagerConnectionFactory(connectionString, null); new PoolableConnectionFactory(connectionFactory, connectionPool, null, null, false, true); PoolingDataSource _poolingDataSource = new PoolingDataSource(conne ctionPool);
Теперь вместо создания соединения «самим» можно получать его из PoolingDataSource. Сама работа с соединением при этом не изменится ни на грамм. Вот, например, добавление записи (листинг 4, сравните с листингом 2):
Листинг 4. Добавление записи (используем пул соединений)
private void addEntry(String aName, String aNumber, String aComment) throws SQLException { Connection connection = _poolingDataSource.getConnection(); try { PreparedStatement statement = connection.prepareStatement( INSERT INTO `addresses` (`name`, `number`, `comment`) VALUES (?, ?, ?)”); statement.setString(1, aName); statement.setString(2, aNumber); statement.setString(3, aComment); statement.executeUpdate(); statement.close(); } finally { connection.close(); } }
Повторяющийся текст оставлен специально. Чтобы было видно, что не поменялось ровно ничего. Даже close нужен. Но в действительности close перехватывается пулом соединений и вместо реального закрытия соединения возвращает его в пул.
Стоит упомянуть, что в данном случае используется шаблон Proxy. То есть создается класс, который унаследован от соединения, но в котором изменено поведение одного из методов (в данном случае, close). Благодаря этому код приложения не изменяется, но зато кардинальным образом изменяется поведение.
Другие пути
JDBC – великолепный способ работы с базами данных. Но не единственный. Главная особенность JDBC в том, что это самая низкоуровневая библиотека для работы с базами данных в Java. Все распространенные средства (EJB или Hibernate например), внутри так или иначе используют JDBC. Поэтому для понимания того, что происходит, JDBC знать просто обязательно. Иначе трудности отладки и недоумения поповоду «а что это тут произошло» будут преследовать вас постоянно и на каждом шагу.
Достоинства JDBC очевидны. Это сравнительная простота, мощность (мы не обсудили и десятой части того, что может эта библиотека: здесь есть и транзакции, и курсоры, и блокировки, и много чего еще), а также скорость работы. Но иногда требуется немного другое. В нашем случае в БД была только одна таблица, которая к тому же имела простую структуру. В сложных приложениях часто приходится работать с сотнями «бизнес-объектов», у которых бывает очень нетривиальная структура и взаимосвязи. При этом структура СУБД получается очень непростая. И процедура вытаскивания объектов (которые часто «раскиданы» по десяткам таблиц) – превращается в кошмар.
На этом этапе на передний план выходят другие библиотеки. В первую очередь это Hibernate. Во вторую – EJB. Во вторую, потому что EJB – это не только способ хранения объектов, но несколько более сложная «структура». А Hibernate – это типичный пример ORM (ObjectRelational Mapping, объектно-реляционное отображение), то есть биб- лиотеки, автоматически преобразующей объекты в строки таблиц и обратно. Работа с Hibernate сильно отличается от работы с JDBC, но внутри – это примерно то же, что мы и рассмотрели.
Также нельзя забывать, что несмотря на то, что JDBC – это унифицированная библиотека, которая работает с десятками различных СУБД (был бы драйвер), код, написанный для одной СУБД, далеко не всегда будет работать с другой. Разница обычно заключается в двух вещах:
- Возможности драйвера. Иногда что-то не реализовано. Например, вам нужны транзакции, а в драйвере их просто нет.
- Разница в диалекте SQL. Ни одна СУБД не повторяет никакую другу в языке запросов. Всегда есть различия. Поэтому, несмотря на стандар ты, одно приложение не обязательно будет работать со всеми СУБД.
Ну вот, собственно, и все. Адресная книга успешно «покладена» в базу данных. Это отличное начало, но пора двигаться дальше. Интересно, а что же такое AJAX? LXF
А дайте нам ваше соединение!
Существует еще один способ работы с соединениями, который используется в крупных приложениях. Точнее, не в крупных, а в тех, где за соединениями следит контейнер приложений или контейнер сервлетов.
Для этого в дескрипторе нужно описать способ подключения к СУБД и параметры подключения. И при запуске контейнера можно будет получить DataSource, который будет проинициализирован самим контейнером. Как это делается – лучше смотреть в документации к контейнеру/СУБД. Например, для используемого нами Tomcat нужная информация находится тут: http://tomcat.apache.org/tomcat-5.5-doc/jndi-datasource-examples-howto.html.
Что за штука... встраиваемые СУБД?
В Java 6 появилось новшество – некая Java DB. Что это? Это — встраиваемая СУБД (embeddable DB engine).
Обычно сервер СУБД работает отдельно (часто даже на своем собственном компьютере), но на этапе отладки приложения это зачастую не нужно. Да и в простых приложениях хочется, с одной стороны, использовать мощь баз данных, а с другой, распространять с программой сервер, учить пользователей им пользовать- ся – это кошмар.
Поэтому существует специальный класс СУБД, которые запускаются вместе с приложением (например, как обычный поток внутри вашей программы), и заканчивают работу вместе с ним. Их использование не отличается (за исключением общих разнящихся правил SQL-запросов) от работы с обычной СУБД: здесь тоже нужен драйвер (всё в поставке), те же выражения… Они сродни Jetty, которую мы использовали в самом начале, чтобы сделать наш первый сервлет.
Из известных встраиваемых СУБД можно назвать Java DB (это порт Apache Derby) и HSQLDB, используемую в OpenOffice.org, а из известных за пределами мира Java — SQLite (в принципе тоже подключаемую к Java).