PL / SQL — одна из таких вещей.
Большинство людей стараются держаться подальше от этого. Мало кто действительно любит это. У меня просто случился Стокгольмский синдром, так как я много работаю с банками.
Даже если синтаксис PL / SQL и инструментарий иногда напоминают мне о старых добрых временах…
… Я все еще считаю, что процедурный язык (ну, любой язык) в сочетании с SQL может творить чудеса с точки зрения производительности, производительности и выразительности.
В этой статье мы увидим позже, как мы можем добиться того же с SQL (и PL / SQL) в Java, используя jOOQ .
Но сначала немного истории…
Доступ к PL / SQL из Java
Одна из главных причин, почему разработчики Java, в частности, воздерживаются от написания своего собственного кода PL / SQL, заключается в том, что интерфейс между PL / SQL и Java — ojdbc — является серьезной проблемой. В следующих примерах мы увидим, как это происходит.
Предположим, мы работаем над портом Oracle популярной базы данных Sakila ( изначально созданной для MySQL ). Этот конкретный порт Sakila / Oracle был реализован DB Software Laboratory и опубликован под лицензией BSD.
Вот частичное представление этой базы данных Sakila.
Теперь, давайте предположим, что у нас есть API в базе данных, который не предоставляет вышеуказанную схему, но вместо этого предоставляет API PL / SQL . API может выглядеть примерно так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
CREATE TYPE LANGUAGE_T AS OBJECT ( language_id SMALLINT , name CHAR (20), last_update DATE ); / CREATE TYPE LANGUAGES_T AS TABLE OF LANGUAGE_T; / CREATE TYPE FILM_T AS OBJECT ( film_id int , title VARCHAR (255), description CLOB, release_year VARCHAR (4), language LANGUAGE_T, original_language LANGUAGE_T, rental_duration SMALLINT , rental_rate DECIMAL (4,2), length SMALLINT , replacement_cost DECIMAL (5,2), rating VARCHAR (10), special_features VARCHAR (100), last_update DATE ); / CREATE TYPE FILMS_T AS TABLE OF FILM_T; / CREATE TYPE ACTOR_T AS OBJECT ( actor_id numeric , first_name VARCHAR (45), last_name VARCHAR (45), last_update DATE ); / CREATE TYPE ACTORS_T AS TABLE OF ACTOR_T; / CREATE TYPE CATEGORY_T AS OBJECT ( category_id SMALLINT , name VARCHAR (25), last_update DATE ); / CREATE TYPE CATEGORIES_T AS TABLE OF CATEGORY_T; / CREATE TYPE FILM_INFO_T AS OBJECT ( film FILM_T, actors ACTORS_T, categories CATEGORIES_T ); / |
Вы сразу заметите, что это, по сути, просто копия схемы 1: 1 в этом случае, смоделированная как типы Oracle SQL OBJECT
и TABLE
, за FILM_INFO_T
типа FILM_INFO_T
, который действует как агрегат.
Теперь наш администратор БД (или наш разработчик базы данных) реализовал следующий API для доступа к вышеуказанной информации:
1
2
3
4
5
6
7
8
9
|
CREATE OR REPLACE PACKAGE RENTALS AS FUNCTION GET_ACTOR(p_actor_id INT ) RETURN ACTOR_T; FUNCTION GET_ACTORS RETURN ACTORS_T; FUNCTION GET_FILM(p_film_id INT ) RETURN FILM_T; FUNCTION GET_FILMS RETURN FILMS_T; FUNCTION GET_FILM_INFO(p_film_id INT ) RETURN FILM_INFO_T; FUNCTION GET_FILM_INFO(p_film FILM_T) RETURN FILM_INFO_T; END RENTALS; / |
Дамы и господа, вот как вы можете …
… Утомительно получить доступ к PL / SQL API с помощью JDBC
Итак, чтобы избежать неуклюжего CallableStatement с его регистрацией параметров OUT
и escape-синтаксисом JDBC, мы собираемся FILM_INFO_T
запись FILM_INFO_T
помощью оператора SQL, подобного следующему:
1
2
3
4
5
6
|
try (PreparedStatement stmt = conn.prepareStatement( "SELECT rentals.get_film_info(1) FROM DUAL" ); ResultSet rs = stmt.executeQuery()) { // STRUCT unnesting here... } |
Все идет нормально. К счастью, есть Java-попытка с ресурсами, чтобы помочь нам очистить эти многочисленные объекты JDBC. Теперь, как действовать? Что мы получим от этого ResultSet
? java.sql.Struct
:
1
2
3
4
5
|
while (rs.next()) { Struct film_info_t = (Struct) rs.getObject( 1 ); // And so on... } |
Теперь самые смелые из вас продолжат java.sql.Struct
до еще более неясного и загадочного oracle.sql.STRUCT
, который почти не содержит Javadoc, но содержит множество устаревших дополнительных, специфичных для поставщика методов.
А пока давайте придерживаться «стандартного API».
Interlude:
Давайте на минутку оценим JDBC во времена Java 8.
Когда появилась Java 5, были и дженерики. Мы переписали наши большие базы кода, чтобы убрать все виды бессмысленных приведений типового образца, которые больше не нужны. За исключением JDBC. Когда дело доходит до JDBC, угадывать подходящие типы — дело удачи. Мы обращаемся к сложным вложенным структурам данных, предоставляемым внешними системами, путем разыменования элементов по индексу, а затем даем необдуманные предположения о результирующих типах данных.
Лямбды только что были представлены, но JDBC все еще общается с мэйнфреймами.
А потом…
Хорошо, достаточно этих рантов.
Давайте продолжим навигацию по нашей STRUCT
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
while (rs.next()) { Struct film_info_t = (Struct) rs.getObject( 1 ); Struct film_t = (Struct) film_info_t.getAttributes()[ 0 ]; String title = (String) film_t.getAttributes()[ 1 ]; Clob description_clob = (Clob) film_t.getAttributes()[ 2 ]; String description = description_clob.getSubString( 1 , ( int ) description_clob.length()); Struct language_t = (Struct) film_t.getAttributes()[ 4 ]; String language = (String) language_t.getAttributes()[ 1 ]; System.out.println( "Film : " + title); System.out.println( "Description: " + description); System.out.println( "Language : " + language); } |
Исходя из начального STRUCT
который мы получили в позиции 1 из ResultSet
, мы можем продолжить разыменование атрибутов по индексу. К сожалению, нам постоянно нужно искать тип SQL в Oracle (или в некоторой документации), чтобы запомнить порядок атрибутов:
1
2
3
4
5
6
|
CREATE TYPE FILM_INFO_T AS OBJECT ( film FILM_T, actors ACTORS_T, categories CATEGORIES_T ); / |
И это еще не все! Первый атрибут типа FILM_T
— это еще один вложенный STRUCT
. А потом, эти ужасные CLOB
s. Приведенный выше код не является строго полным. В некоторых случаях, которые могут понять только сопровождающие JDBC, java.sql.Clob.free()
должен быть вызван, чтобы убедиться, что ресурсы высвобождаются во времени. Помните, что CLOB
, в зависимости от вашей базы данных и конфигурации драйвера, может находиться за пределами вашей транзакции.
К сожалению, метод вызывается free()
вместо AutoCloseable.close()
, поэтому использовать try-with-resources невозможно. Итак, поехали:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
List<Clob> clobs = new ArrayList<>(); while (rs.next()) { try { Struct film_info_t = (Struct) rs.getObject( 1 ); Struct film_t = (Struct) film_info_t.getAttributes()[ 0 ]; String title = (String) film_t.getAttributes()[ 1 ]; Clob description_clob = (Clob) film_t.getAttributes()[ 2 ]; String description = description_clob.getSubString( 1 , ( int ) description_clob.length()); Struct language_t = (Struct) film_t.getAttributes()[ 4 ]; String language = (String) language_t.getAttributes()[ 1 ]; System.out.println( "Film : " + title); System.out.println( "Description: " + description); System.out.println( "Language : " + language); } finally { // And don't think you can call this early, either // The internal specifics are mysterious! for (Clob clob : clobs) clob.free(); } } |
Вот и все. Теперь мы нашли небольшой вывод на консоль:
1
2
3
4
5
|
Film : ACADEMY DINOSAUR Description: A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies Language : English |
Вот и все — вы можете подумать! Но…
Боль только началась
… потому что мы еще не закончили. Есть также два типа вложенных таблиц, которые нам необходимо десериализовать из STRUCT
. Если вы еще не java.sql.Array
меня, хорошие новости близки), вам понравится читать о том, как получить и раскрутить java.sql.Array
. Давайте продолжим сразу после печати фильма:
1
2
|
Array actors_t = (Array) film_info_t.getAttributes()[ 1 ]; Array categories_t = (Array) film_info_t.getAttributes()[ 2 ]; |
Опять же, мы обращаемся к атрибутам по индексам, которые мы должны помнить и которые могут легко сломаться. Массив ACTORS_T
— это не что иное, как еще один упакованный STRUCT
:
01
02
03
04
05
06
07
08
09
10
|
System.out.println( "Actors : " ); Object[] actors = (Object[]) actors_t.getArray(); for (Object actor : actors) { Struct actor_t = (Struct) actor; System.out.println( " " + actor_t.getAttributes()[ 1 ] + " " + actor_t.getAttributes()[ 2 ]); } |
Вы заметите несколько вещей:
- Метод
Array.getArray()
возвращает массив. Но он объявляет возвращениеObject
. Надо вручную кастовать. - Мы не можем привести к
Struct[]
даже если это разумный тип. Но тип, возвращаемый ojdbc — этоObject[]
(содержащий элементыStruct
) - Цикл foreach также не может разыменовать структуру с правой стороны. Там нет никакого способа заставить тип
actor
в то, что мы знаем, что это на самом деле - Мы могли бы использовать Java 8 и Streams и тому подобное, но, к сожалению, все лямбда-выражения, которые могут быть переданы в Streams API, не допускают выброс проверенных исключений . И JDBC бросает проверенные исключения. Это будет еще страшнее.
Так или иначе. Теперь, когда мы наконец достигли этого, мы можем увидеть вывод на печать:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
Film : ACADEMY DINOSAUR Description: A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies Language : English Actors : PENELOPE GUINESS CHRISTIAN GABLE LUCILLE TRACY SANDRA PECK JOHNNY CAGE MENA TEMPLE WARREN NOLTE OPRAH KILMER ROCK DUKAKIS MARY KEITEL |
Когда это безумие прекратится?
Это остановится прямо здесь!
До сих пор эта статья читалась как учебник (или, скорее, средневековая пытка) о том, как десериализовать вложенные пользовательские типы из Oracle SQL в Java (не заставляйте меня начинать их сериализацию снова!)
В следующем разделе мы увидим, как с помощью jOOQ и его генератора исходного кода можно без проблем реализовать ту же бизнес-логику (перечисляя Film с ID = 1 и ее участников). Проверь это:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// Simply call the packaged stored function from // Java, and get a deserialised, type safe record FilmInfoTRecord film_info_t = Rentals.getFilmInfo1( configuration, new BigInteger( "1" )); // The generated record has getters (and setters) // for type safe navigation of nested structures FilmTRecord film_t = film_info_t.getFilm(); // In fact, all these types have generated getters: System.out.println( "Film : " + film_t.getTitle()); System.out.println( "Description: " + film_t.getDescription()); System.out.println( "Language : " + film_t.getLanguage().getName()); // Simply loop nested type safe array structures System.out.println( "Actors : " ); for (ActorTRecord actor_t : film_info_t.getActors()) { System.out.println( " " + actor_t.getFirstName() + " " + actor_t.getLastName()); } System.out.println( "Categories : " ); for (CategoryTRecord category_t : film_info_t.getCategories()) { System.out.println(category_t.getName()); } |
Это оно?
Да!
Вау, я имею в виду, что все эти типы / процедуры / функции PL / SQL на самом деле были частью Java. Все предостережения, которые мы видели ранее, скрыты за этими сгенерированными типами и реализованы в jOOQ , так что вы можете сосредоточиться на том, что изначально хотели сделать. Получите доступ к объектам данных и сделайте с ними значимую работу. Не сериализуйте / десериализуйте их!
Давайте на минутку оценим эту потребительскую рекламу:
Еще не убежден?
Я сказал вам не начинать сериализацию типов в JDBC. И я не буду, но вот как сериализовать типы в JOOQ, потому что это кусок пирога!
Давайте рассмотрим этот другой тип агрегата, который возвращает историю аренды клиента:
1
2
3
4
5
|
CREATE TYPE CUSTOMER_RENTAL_HISTORY_T AS OBJECT ( customer CUSTOMER_T, films FILMS_T ); / |
И полные спецификации пакета PL / SQL:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
CREATE OR REPLACE PACKAGE RENTALS AS FUNCTION GET_ACTOR(p_actor_id INT ) RETURN ACTOR_T; FUNCTION GET_ACTORS RETURN ACTORS_T; FUNCTION GET_CUSTOMER(p_customer_id INT ) RETURN CUSTOMER_T; FUNCTION GET_CUSTOMERS RETURN CUSTOMERS_T; FUNCTION GET_FILM(p_film_id INT ) RETURN FILM_T; FUNCTION GET_FILMS RETURN FILMS_T; FUNCTION GET_CUSTOMER_RENTAL_HISTORY(p_customer_id INT ) RETURN CUSTOMER_RENTAL_HISTORY_T; FUNCTION GET_CUSTOMER_RENTAL_HISTORY(p_customer CUSTOMER_T) RETURN CUSTOMER_RENTAL_HISTORY_T; FUNCTION GET_FILM_INFO(p_film_id INT ) RETURN FILM_INFO_T; FUNCTION GET_FILM_INFO(p_film FILM_T) RETURN FILM_INFO_T; END RENTALS; / |
Таким образом, при вызове RENTALS.GET_CUSTOMER_RENTAL_HISTORY
мы можем найти все фильмы, которые когда-либо снимал клиент. Давайте сделаем это для всех клиентов, чье FIRST_NAME
— «JAMIE», и на этот раз мы используем Java 8:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
// We call the stored function directly inline in // a SQL statement dsl().select(Rentals.getCustomer( CUSTOMER.CUSTOMER_ID )) .from(CUSTOMER) .where(CUSTOMER.FIRST_NAME.eq( "JAMIE" )) // This returns Result<Record1<CustomerTRecord>> // We unwrap the CustomerTRecord and consume // the result with a lambda expression .fetch() .map(Record1::value1) .forEach(customer -> { System.out.println( "Customer : " ); System.out.println( "- Name : " + customer.getFirstName() + " " + customer.getLastName()); System.out.println( "- E-Mail : " + customer.getEmail()); System.out.println( "- Address : " + customer.getAddress().getAddress()); System.out.println( " " + customer.getAddress().getPostalCode() + " " + customer.getAddress().getCity().getCity()); System.out.println( " " + customer.getAddress().getCity().getCountry().getCountry()); // Now, lets send the customer over the wire again to // call that other stored procedure, fetching his // rental history: CustomerRentalHistoryTRecord history = Rentals.getCustomerRentalHistory2(dsl().configuration(), customer); System.out.println( " Customer Rental History : " ); System.out.println( " Films : " ); history.getFilms().forEach(film -> { System.out.println( " Film : " + film.getTitle()); System.out.println( " Language : " + film.getLanguage().getName()); System.out.println( " Description : " + film.getDescription()); // And then, let's call again the first procedure // in order to get a film's actors and categories FilmInfoTRecord info = Rentals.getFilmInfo2(dsl().configuration(), film); info.getActors().forEach(actor -> { System.out.println( " Actor : " + actor.getFirstName() + " " + actor.getLastName()); }); info.getCategories().forEach(category -> { System.out.println( " Category : " + category.getName()); }); }); }); |
… И краткая выдержка из результатов, полученных выше:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
Customer : - Name : JAMIE RICE - E-Mail : JAMIE.RICE @sakilacustomer .org - Address : 879 Newcastle Way 90732 Sterling Heights United States Customer Rental History : Films : Film : ALASKA PHANTOM Language : English Description : A Fanciful Saga of a Hunter And a Pastry Chef who must Vanquish a Boy in Australia Actor : VAL BOLGER Actor : BURT POSEY Actor : SIDNEY CROWE Actor : SYLVESTER DERN Actor : ALBERT JOHANSSON Actor : GENE MCKELLEN Actor : JEFF SILVERSTONE Category : Music Film : ALONE TRIP Language : English Description : A Fast-Paced Character Study of a Composer And a Dog who must Outgun a Boat in An Abandoned Fun House Actor : ED CHASE Actor : KARL BERRY Actor : UMA WOOD Actor : WOODY JOLIE Actor : SPENCER DEPP Actor : CHRIS DEPP Actor : LAURENCE BULLOCK Actor : RENEE BALL Category : Music |
Если вы используете Java и PL / SQL …
… Тогда вы должны нажать на баннер ниже и скачать бесплатную пробную версию прямо сейчас, чтобы экспериментировать с jOOQ и Oracle:
Порт Oracle базы данных Sakila доступен по этому URL-адресу бесплатно на условиях лицензии BSD:
https://github.com/jOOQ/jOOQ/tree/master/jOOQ-examples/Sakila/oracle-sakila-db
Наконец, пришло время снова наслаждаться написанием PL / SQL!
Ссылка: | Безболезненный доступ из Java к процедурам PL / SQL с jOOQ от нашего партнера по JCG Лукаса Эдера из блога JAVA, SQL и AND JOOQ . |