Книга: Дата-ориентированное программирование: Пер. с англ.
Назад: 7.3. Гибкость и строгость схемы
Дальше: 11.7. Доставка в срок

Персистентные структуры данных защищают данные от изменений.

Персистентные структуры данных обеспечивают эффективный способ создания

новых версий коллекций.

Персистентные структуры данных всегда сохраняют предыдущую версию самих

себя при их изменении.

Персистентные структуры данных представляют данные изнутри таким образом, что структурное совместное использование хорошо масштабируется с точки

зрения памяти и вычислений.

Когда данные неизменяемы, ими можно безопасно пользоваться совместно.

Изнутри персистентность использует коэффициент ветвления, равный 32.

На практике манипулирование персистентными структурами данных эффективно даже для коллекций с 10 миллиардами записей.

Из-за особенностей современной архитектуры процессоров производительность

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

Персистентными списками можно управлять практически за постоянное время.

В большинстве языков программирования реализацию персистентных структур

данных предоставляют сторонние библиотеки.

Коллекции Paguro реализуют доступные только для чтения части интерфейсов

коллекций Java.

Коллекции Paguro могут быть переданы любым методам, которые ожидают получения коллекции Java, без их изменения.

242

Часть 2. Масштабируемость

Операции с базой данных

Облако есть облако

В ЭТОЙ ГЛАВЕ РАССМАТРИВАЮТСЯ

Извлечение данных из базы данных.

Хранение данных в базе данных.

Манипулирование данными, извлеченными из базы данных.

Традиционно в ООП мы используем шаблоны проектирования и сложные слои

объектов для структурирования доступа к базе данных. В ДОП мы предпочитаем

представлять данные, извлеченные из базы данных, с помощью общих наборов

данных, а именно списков карт, где поля в картах соответствуют значениям столбцов базы данных. Как мы увидим на протяжении всей главы, тот факт, что поля

внутри карты доступны динамически через их имена, позволяет нам использовать

один и тот же общий код для разных объектов данных.

СОВЕТ. Лучший способ манипулировать данными — это представлять их как данные.

В этой главе мы проиллюстрируем применение дата-ориентированных принципов

при доступе к данным из реляционной базы данных. Предполагается, что читатель

в общем виде разбирается в реляционных базах данных и синтаксисе запросов SQL

(например, SELECT, AS, WHERE и INNER JOIN). Этот подход можно легко адаптировать

к базам данных NoSQL.

Приложения, работающие на сервере, обычно хранят данные в базе данных. В ДОП

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

коллекций данных. Это ведет к снижению сложности системы и повышению универсальности.

244

Часть 2. Масштабируемость

10.1. Извлечение данных из базы данных

Тео и Джо отправляются на прогулку в парк рядом с офисом. Они сидят на скамей-ке рядом с красивым озером и смотрят на облака. После пары минут медитативного

молчания Джо спрашивает Тео: «Что вы видите?». Тео говорит ему, что это облако

кажется ему лошадью, а то похоже на автомобиль. На обратном пути в офис Тео

просит Джо объяснить, что такое облака. Джо отвечает с загадочной улыбкой:

«Облако есть облако».

Тео: До сих пор вы показывали мне, как ДОП представляет данные, которые хранятся в памяти приложения. А как насчет данных, поступающих извне?

Джо: В каком смысле извне?

Тео: Данные, поступающие из базы данных.

Джо: Я еще вернусь к этому вопросу. Как, по-вашему, мы должны представлять

данные, поступающие из базы данных, в ДОП?

Тео: Как общие коллекции данных, полагаю.

Джо: Именно! В ДОП мы всегда представляем данные с помощью общих наборов

данных.

Тео: Означает ли это, что мы можем манипулировать данными из базы данных

с такой же гибкостью, с какой мы манипулируем данными в памяти?

Джо: Определенно.

СОВЕТ. В ДОП мы представляем данные из базы данных с помощью общих наборов

данных и манипулируем ими с помощью общих функций.

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

данные каталога хранятся в базе данных SQL?

Джо: Я покажу. Во-первых, расскажите мне, как бы вы спроектировали таблицы, в которых хранятся данные каталога.

Тео: Вы имеете в виду точные схемы таблиц с информацией о первичных ключах

и возможности обнуления каждого столбца?

Джо: Нет, мне нужен только приблизительный обзор таблиц, их столбцов и взаи-мосвязей между таблицами.

Тео подходит к доске. На рис. 10.1 показана диаграмма, которую он рисует, объяс-няя Джо свои мысли.

Тео: У меня есть таблица books с тремя столбцами: title, ISBN и publication_year.

У меня также есть таблица authors с двумя столбцами: для id и name. Позвольте мне

нарисовать эти таблицы на доске, чтобы дать вам наглядность (табл. 10.1 и 10.2).

Джо: Как насчет связи между книгами и авторами?

Тео: Давайте посмотрим, книга может быть написана несколькими авторами, и

один автор может написать несколько книг. Поэтому мне нужна таблица

Глава 10. Операции с базой данных

245

book_authors «многие ко многим», которая связывает авторов и книги с двумя

столбцами, book_isbn и author_id.

Таблица 10.1. Таблица books, заполненная двумя книгами

title ISBN

publication_year

The Power of Habit

978-0812981605

2012

7 Habits of Highly Effective People

978-1982137274

1989

Таблица 10.2. Таблица authors, заполненная тремя авторами

id name

sean-covey Sean

Covey

stephen-covey Stephen

Covey

charles-duhigg Charles

Duhigg

Рис. 10.1. Модель базы данных для книг и авторов

Тео снова поворачивается к доске. Он рисует табл. 10.3 book_authors, чтобы показать Джо.

Таблица 10.3. Таблица book_authors

book_isbn author_id

978-1982137274 sean-covey

978-1982137274 stephen-covey

978-0812981605 charles-duhigg

Джо: Отлично! Давайте начнем с самого простого случая. Мы собираемся написать

код, который ищет книги, соответствующие названию, и возвращает основную

информацию о книгах. Под основной информацией я подразумеваю название, ISBN и год публикации.

246

Часть 2. Масштабируемость

Тео: А как насчет авторов книг?

Джо: Мы разберемся с этим позже, т. к. это немного сложнее. Можете ли вы написать SQL-запрос для извлечения книг, содержащих слово habit в названии?

Тео: Конечно.

Это задание далось Тео довольно легко. Сначала он записывает SQL-запрос (листинг 10.1), затем отображает результаты (табл. 10.4).

Листинг 10.1. SQL-запрос для извлечения книг, название которых содержит habit SELECT

title,

isbn,

publication_year

FROM

books

WHERE title LIKE '%habit%';

Таблица 10.4. Результаты SQL-запроса для книг,

название которых содержит слово habit

title ISBN

publication_year

The Power of Habit

978-0812981605

2012

7 Habits of Highly Effective People

978-1982137274

1989

Джо: Как бы вы описали эти результаты как сбор данных?

Тео: Я бы сказал, что это список карт.

СОВЕТ. В ДОП доступ к данным из базы данных NoSQL аналогичен тому, как мы получаем доступ к данным из реляционной базы данных.

Джо: Правильно! Теперь можете ли вы записать результаты поиска в виде списка

карт?

Тео: Это звучит не слишком сложно. Как насчет этого?

Листинг 10.2. Результаты поиска в виде списка карт

[

{

"title": "7 Habits of Highly Effective People",

"isbn": "978-1982137274",

"publication_year": 1989

},

{

"title": "The Power of Habit",

Глава 10. Операции с базой данных

247

"isbn": "978-0812981605",

"publication_year": 2012

}

]

Джо: Как насчет схемы JSON для результатов поиска?

Тео: Это не должно быть слишком сложно, если вы позволите мне взглянуть на

шпаргалку схемы JSON, которую вы любезно предложили мне на днях.

Джо: Конечно. Цель подарка — быть использованным тем, кто его получает.

Тео просматривает шпаргалку схемы JSON, чтобы освежить в памяти синтаксис

схемы JSON. Через несколько минут Тео придумывает схему для результатов поиска. Он, безусловно, использует дар Джо с пользой.

Листинг 10.3. Шпаргалка JSON-схемы

{

"type": "array",

"items": {

"type": "object",

"properties": {

"myNumber": {"type": "number"},

"myString": {"type": "string"},

"myEnum": {"enum": ["myVal", "yourVal"]},

"myBool": {"type": "boolean"}

},

"required": ["myNumber", "myString"],

"additionalProperties": false

}

}

Листинг 10.4. Схема JSON для результатов поиска в базе данных

var dbSearchResultSchema = {

"type": "array",

"items": {

"type": "object",

"required": ["title", "isbn", "publication_year"],

"properties": {

"title": {"type": "string"},

"isbn": {"type": "string"},

"publication_year": {"type": "integer"}

}

}

};

Джо: Превосходно. Теперь я собираюсь показать вам, как реализовать searchBooks таким образом, чтобы он извлекал данные из базы данных и возвращал строку

248

Часть 2. Масштабируемость

JSON с результатами. Мы собираемся использовать только общие коллекции

данных от уровня базы данных до сериализации JSON, и это круто.

Тео: Будет ли это похоже на реализацию searchBooks, которую мы написали, когда

вы учили меня основам ДОП?

Джо: Абсолютно. С той лишь разницей, что тогда состояние системы хранилось

локально, и мы опрашивали его функцией типа _.filter. Теперь мы используем

SQL-запросы для получения состояния из базы данных. С точки зрения представления данных и манипулирования ими все точно так же.

Джо подходит к доске и набрасывает поток данных на рис. 10.2. Тео изучает схему.

Рис. 10.2. Поток данных

для обслуживания запроса,

который извлекает данные

из базы данных

Джо: Этап манипулирования данными на диаграмме реализуется с помощью универсальных функций, которые управляют наборами данных. По мере того как

наши примеры станут более сложными, я думаю, вы увидите преимущества

возможности манипулировать наборами данных с помощью универсальных

функций.

Тео: Звучит интересно...

Джо: Для связи с базой данных мы используем драйвер, который возвращает список карт. В JavaScript вы могли бы использовать драйвер SQL, такой как node-postgres.

ПРИМЕЧАНИЕ. См. https://node-postgres.com для получения дополнительной информации об этой коллекции модулей node.js для взаимодействия с базами данных PostgreSQL.

Тео: А в Java?

Джо: В Java вы можете использовать JDBC (Java database connectivity, соединение

с базой данных Java) в дополнение к небольшой служебной функции, которая

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

Джо извлекает фрагмент кода из одного из своих личных репозиториев GitHub.

Затем он показывает код преобразования JDBC Тео, который кажется немного

удивленным.

Глава 10. Операции с базой данных

249

Листинг 10.5. Преобразование набора результатов JDBC в список хеш-карт

List<Map<String, Object>> convertJDBCResultSetToListOfMaps(ResultSet rs) {

List<Map<String, Object>> listOfMaps =

new ArrayList<Map<String, Object>>();

ResultSetMetaData meta = rs.getMetaData();

while (rs.next()) {

Map map = new HashMap();

for (int i = 1; i <= meta.getColumnCount(); i++) {

String key = meta.getColumnLabel(i);

Object value = rs.getObject(i);

map.put(key, value);

}

listOfMaps.add(map);

}

return listOfMaps;

}

СОВЕТ. Преобразование набора результатов JDBC в список хеш-карт выполняется довольно просто.

Тео: Я ожидал, что преобразовать набор результатов JDBC в список хеш-карт будет намного сложнее.

Джо: Это просто, потому что в некотором смысле JDBC ориентирован на данные.

Тео: А как насчет типов полей?

Джо: Когда мы преобразуем набор результатов JDBC в список карт, каждое значение рассматривается как Object.

Тео: Это раздражает, потому что это означает, что для доступа к значению нам

нужно привести его к его типу.

Джо: И да и нет. Посмотрите на наш пример использования поиска книг. Мы передаем все значения, не глядя на их тип. Конкретный тип значения имеет значение

только тогда, когда мы сериализуем результат в JSON, и это обрабатывается

библиотекой сериализации JSON. Это называется поздним связыванием.

ПРИМЕЧАНИЕ. При позднем связывании мы откладываем работу с типами данных как

можно дольше.

Тео: Означает ли это, что в моем приложении мне разрешено манипулировать данными, не имея дела с конкретными типами?

СОВЕТ. В ДОП повышается гибкость, поскольку многие части системы могут свободно

манипулировать данными, не имея дело с конкретными типами.

Джо: Именно так. Через мгновение вы увидите позднее связывание в действии. Это

одно из самых больших преимуществ ДОП.

Тео: Как интересно, я не могу дождаться, чтобы увидеть это!

250

Часть 2. Масштабируемость

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

JavaScript так, как если бы JavaScript имел дело с вводом-выводом синхронным

образом.

Тео: Что вы имеете в виду?

Джо: В JavaScript операция ввода-вывода, например отправка запроса в базу данных, выполняется асинхронно. В реальной жизни это означает использование

либо функций обратного вызова, либо ключевых слов async и await.

Тео: Ах да, это потому, что JavaScript однопоточный.

ПРИМЕЧАНИЕ. Для простоты фрагменты JavaScript в этой главе написаны так, как если

бы JavaScript обрабатывал ввод-вывод синхронным образом. В реальном JavaScript нам

нужно использовать async и await вызовов ввода-вывода.

Джо: Действительно, поэтому я буду писать код, который взаимодействует с базой

данных так, как если бы JavaScript выполнял операции ввода-вывода синхронно.

Вот пример.

Листинг 10.6. Поиск книг в базе данных, возврат результатов в формате JSON

var dbClient; ❶

var ajv = new Ajv({allErrors: true});❷

var title = "habit";

var matchingBooksQuery = `SELECT title, isbn

FROM books

WHERE title LIKE '%$1%'`;❸

var books = dbClient.query(matchingBooksQuery,

[title]); ❹

if(!ajv.validate(dbSearchResultSchema, books)) {

var errors = ajv.errorsText(ajv.errors);

throw "Internal error: Unexpected result from the database: " + errors;

}

JSON.stringify(books);

❶ dbClient удерживает соединение с базой данных.

❷ Инициализирует Ajv (библиотеку проверки схемы JSON) с помощью allErrors: true, чтобы перехватить все ошибки проверки данных.

❸ Использует параметризованный SQL-запрос в качестве наилучшей практики обеспечения

безопасности.

❹ Передает параметры в SQL-запрос в виде списка значений (в нашем случае списка с одним значением).

Тео: Я понимаю, что в языке с динамической типизацией, таком как JavaScript, ти-пы значений в списке карт, возвращаемых dbClient.query, определяются во вре-

Глава 10. Операции с базой данных

251

мя выполнения. Как это работает на языке со статической типизацией, таком как

Java, и какие бывают типы полей данных в книгах?

Джо: Функция convertJDBCResultSetToListOfMaps, которую мы создали ранее (см. листинг 10.5), возвращает список Map<String, Object>. Но библиотеки сериализации

JSON, такие как Gson, умеют определять во время выполнения конкретный тип

значений на карте и сериализовать значения в соответствии с их типом.

ПРИМЕЧАНИЕ. См. https://github.com/google/gson для получения информации о библиотеке Gson для сериализации/десериализации Java.

Тео: Что вы подразумеваете под сериализацией значения в соответствии с его типом?

Джо: Например, значением поля publish_year является число; поэтому оно не заключено в кавычки. Однако значением поля title является строка; поэтому оно

заключено в кавычки.

Тео: Замечательно! Теперь я понимаю, что вы имеете в виду под поздним связыванием.

Джо: Супер! Теперь позвольте мне показать вам, как мы храним данные в базе

данных.

10.2. Хранение данных в базе данных

В предыдущем разделе мы увидели, как извлекать данные из базы данных в виде

списка карт. Далее мы увидим, как хранить данные в базе данных, когда данные

представлены картой.

Тео: Полагаю, что хранение данных в базе данных очень похоже на выборку данных из базы данных.

Джо: Это похоже в том смысле, что мы имеем дело только с общими коллекциями

данных. Можете ли вы написать параметризованный SQL-запрос, который

вставляет строку с информацией о пользователе, используя только email и

encrypted_password, пожалуйста?

Тео: Да, сейчас.

Тео размышляет над кодом и пишет несколько строк SQL, как просил Джо. Он показывает это Джо.

Листинг 10.7. SQL-инструкция для добавления пользователя библиотеки

INSERT

INTO members

(email, encrypted_password)

VALUES ($1, $2)

Джо: Замечательно! А вот как интегрировать ваш SQL-запрос в код нашего приложения.

252

Часть 2. Масштабируемость

Листинг 10.8. Добавление пользователя библиотекой из приложения

var addMemberQuery =

"INSERT INTO members (email, password) VALUES ($1, $2)"; dbClient.query(addMemberQuery,

[_.get(member, "email"),❶

_.get(member, "encryptedPassword")]);

❶ Передает два параметра SQL-запросу в виде массива.

Тео: Ваш код очень понятен, но что-то меня все же беспокоит.

Джо: Что же?

Тео: Я нахожу громоздким использование _.get(user, "email") вместо user.email, как если бы данные были представлены классом.

Джо: В JavaScript вам разрешено использовать запись через точку user.email вместо _.get(user, "email").

Тео: Тогда почему бы вам не использовать запись через точку?

Джо: Потому что я хотел показать вам, как вы можете применять принципы ДОП

даже в таких языках, как Java, где запись через точку недоступна для хеш-карт.

ПРИМЕЧАНИЕ. В этой книге мы избегаем использования точечной записи JavaScript для

доступа к полю в хеш-карте, чтобы проиллюстрировать, как применять ДОП в языках, которые не поддерживают точечную запись в хеш-картах.

Тео: Это точно то, что я имею в виду. Я считаю громоздким в таком языке, как

Java, использовать _.get(user, "email") вместо user.email, как если бы данные

были представлены классом.

Джо: С одной стороны, это громоздко. С другой стороны, представление данных

с помощью хеш-карты вместо статического класса позволяет гибко обращаться

к полям.

Тео: Я знаю, вы столько раз говорили мне это! Но не могу к этому привыкнуть.

Джо: Позвольте мне привести вам еще один пример преимуществ гибкого доступа

к полям данных в контексте добавления члена в базу данных. Вы сказали, что

писать [_.get(member, "email"), _.get(member, "encryptedPassword")] менее удобно, чем писать [member.email, member.encryptedPassword]. Верно?

Тео: Все так!

Джо: Позвольте мне показать вам, как написать тот же код более лаконично, используя функцию Lodash под названием _.at.

Тео: Что делает _.at функция?

Джо: Она получает карту m, список keyList и возвращает список, состоящий из значений m, связанных с ключами в keyList.

Тео: Как насчет примера?

Глава 10. Операции с базой данных

253

Джо: Конечно. Мы создаем список из полей email и encryptedPassword пользователя

библиотекой.

Джо печатает. Затем он показывает этот код Тео.

Листинг 10.9. Создание списка из некоторых значений на карте с помощью _.at var member = {

"email": "[email protected]",

"encryptedPassword": "c2VjcmV0",

"isBlocked": false

};

_.at(member,

["email", "encryptedPassword"]);

// ? ["[email protected]", "c2VjcmV0"]

Тео: Значения в результатах появляются в том же порядке, что и ключи в keyList?

Джо: Да.

Тео: Классно!

СОВЕТ. Доступ к полю в хеш-карте более гибкий, чем доступ к члену в объекте, созданном из класса.

Джо: А вот код для добавления пользователя библиотекой с помощью _.at.

Листинг 10.10. Использование _.at для возврата нескольких значений из карты

class CatalogDB {

static addMember(member) {

var addMemberQuery = ÌNSERT

INTO members (email, encrypted_password)

VALUES ($1, $2)`;

dbClient.query(addMemberQuery,

_.at(member, ["email",

"encryptedPassword"]));

}

}

Тео: Я вижу, как функция _.at становится действительно полезной, когда нам нужно передать большее количество значений полей.

Джо: Я покажу вам больше примеров, использующих гибкий доступ к данным, который у нас есть в ДОП.

254

Часть 2. Масштабируемость

10.3. Простая манипуляция данными

Довольно часто в производственном приложении нам необходимо изменить данные, извлеченные из базы данных. Самый простой случай — это когда нам нужно

переименовать столбцы из базы данных в те, которые больше подходят для нашего

приложения.

Джо: Вы заметили, что имена столбцов в нашей базе данных соответствуют стилю

написания snake?

Тео: Я так привык к стилю, что нет. Я даже не думал об этом.

Джо: Ну, например, столбец для года публикации книги называется publication_

year.

Тео: Погодите-ка...

Джо: Внутри JSON мне нравится использовать стиль Pascal, например publicationYear.

Тео: И я бы предпочел иметь bookTitle вместо title.

Джо: Итак, мы оба недовольны строкой JSON, которую возвращает searchBooks, если мы передаем данные, извлеченные из базы данных, как есть.

Тео: Именно!

Джо: Как бы мы починили ее?

Тео: Я бы изменил SQL-запрос так, чтобы он переименовывал столбцы в результатах. Позвольте мне показать.

Листинг 10.11. Переименование столбцов внутри SQL-запроса

SELECT

title AS bookTitle,

isbn,

publication_year AS publicationYear

FROM

books

WHERE title LIKE '%habit%';

Джо: Это сработало бы, но кажется немного странным изменять SQL-запрос так, чтобы он соответствовал соглашению об именовании приложения.

Тео: Да, я согласен. Я полагаю, что такая база данных, как MongoDB, не позволяет

легко переименовывать поля внутри запроса.

Джо: Ага. Иногда имеет смысл иметь дело с именами полей в коде приложения.

Как бы вы с этим справились?

Тео: Что ж, в таком случае для каждой карты, возвращаемой запросом к базе данных, я бы использовал функцию для изменения имен полей.

Джо: Не могли бы вы показать мне, как будет выглядеть код?

Тео: Конечно. Как насчет такого?

Глава 10. Операции с базой данных

255

Листинг 10.12. Переименование определенных ключей в списке карт

function renameBookInfoKeys(bookInfo) {

return {

"bookTitle": _.get(bookInfo, "title"),

"isbn": _.get(bookInfo, "isbn"),

"publicationYear": _.get(bookInfo, "publication_year")

};

}

var bookResults = [

{

"title": "7 Habits of Highly Effective People",

"isbn": "978-1982137274",

"publication_year": 1989

},

{

"title": "The Power of Habit",

"isbn": "978-0812981605",

"publication_year": 2012

}

];

_.map(bookResults, renameBookInfoKeys);

Джо: Не могли бы вы написать аналогичный фрагмент кода для каждого запроса, который вы извлекли из базы данных?

Тео: Что вы имеете в виду?

Джо: Предположим, вы хотите переименовать поля, возвращаемые запросом, который извлекает книги, взятые пользователем.

Тео: А-а. Я бы написал аналогичный фрагмент кода для каждого случая.

Джо: В ДОП мы можем использовать тот факт, что имя поля — это просто строка, и написать универсальную функцию под названием renameResultKeys, которая

работает с каждым списком карт.

Тео: Ого! Но как renameResultKeys узнает, какие поля переименовывать?

Джо: Вы передаете сопоставление между старым и новым именами в виде карты.

СОВЕТ. В ДОП имена полей — это просто строки. Это позволяет нам писать общие

функции для управления списком карт, которые представляют данные, извлеченные из базы

данных.

Тео: Не могли бы вы показать мне пример?

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

renameResultKeys следующим образом.

256

Часть 2. Масштабируемость

Листинг 10.13. Переименование полей в результатах SQL

renameResultKeys(bookResults, {

"title": "bookTitle",

"publication_year": "publicationYear"

});

Тео: Что случилось с полем, в котором хранится isbn?

Джо: Когда поле не упоминается, renameResultKeys оставляет его как есть.

Тео: Классно! Можете ли вы показать мне реализацию renameResultKeys?

Джо: Конечно, речь идет только о map и reduce, поэтому я бы сделал что-то вроде

этого.

Листинг 10.14. Переименование ключей в результатах SQL

function renameKeys(map, keyMap) {

return _.reduce(keyMap,

function(res, newKey, oldKey) {

var value = _.get(map, oldKey);

var resWithNewKey = _.set(res, newKey, value);

var resWithoutOldKey = _.omit(resWithNewKey, oldKey);

return resWithoutOldKey;

},

map);

}

function renameResultKeys(results, keyMap) {

return _.map(results, function(result) {

return renameKeys(result, keyMap);

});

}

Тео: Этот код не так уж и прост для понимания!

Джо: Не волнуйтесь. Чем больше вы пишете функции обработки данных с map, filter и reduce, тем больше вы к этому привыкаете.

Тео: Я надеюсь!

Джо: Что действительно важно на данный момент, так это то, что вы понимаете, что делает возможным в ДОП написание такой функции, как renameResultKeys.

Тео: Я бы сказал, это потому, что поля доступны динамически со строками.

Джо: Точно. Можно сказать, что поля — это первоклассные граждане.

СОВЕТ. В ДОП поля — это первоклассные граждане.

Тео: Как бы вы написали модульные тесты для функции манипулирования данными, такой как renameResultKeys?

Глава 10. Операции с базой данных

257

Джо: Это похоже на модульные тесты, которые мы писали ранее. Вы генерируете

входные данные и ожидаемые результаты и следите за тем, чтобы фактические

результаты соответствовали ожидаемым результатам. Я попрошу вас подождать, т. к. это может занять некоторое время.

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

приготовить два эспрессо. Какая удача! На столе стоит коробка швейцарского

шоколада. Он хватает пару штук и возвращается в свой офис как раз в тот момент, когда Джо заканчивает модульный тест.

Листинг 10.15. Модульный тест для renameResultKeys

var listOfMaps = [

{

"title": "7 Habits of Highly Effective People",

"isbn": "978-1982137274",

"publication_year": 1989

},

{

"title": "The Power of Habit",

"isbn": "978-0812981605",

"publication_year": 2012

}

];

var expectedResults = [

{

"bookTitle": "7 Habits of Highly Effective People",

"isbn": "978-1982137274",

"publicationYear": 1989

},

{

"bookTitle": "The Power of Habit",

"isbn": "978-0812981605",

"publicationYear": 2012

}

];

var results = renameResultKeys(listOfMaps,

{"title": "bookTitle",

"publication_year": "publicationYear"});

_.isEqual(expectedResults, results);

Тео: Супер!

Джо: Вы понимаете, почему можете свободно использовать renameResultKeys с результатами любого SQL-запроса?

258

Часть 2. Масштабируемость

Тео: Потому что код renameResultKeys отделен от внутренних деталей представления данных, с которыми он работает.

Джо: Совершенно верно! Теперь предположим, что SQL-запрос возвращает информацию о пользователе в таблице. Как бы вы использовали renameResultKeys для переименования email в userEmail? Предположим, что таблица выглядит следующим образом (табл. 10.5).

И снова в игру вступает доска. Когда Джо заканчивает, он показывает Тео таблицу.

Таблица 10.5. Результаты SQL-запроса,

который возвращает email и encrypted_password некоторых пользователей

email encrypted_password

[email protected] secret-pass

[email protected] my-secret

Тео: Это просто!

На своем ноутбуке Тео пишет код для переименования email. Удовлетворенный, он

поворачивает ноутбук к Джо.

Листинг 10.16. Переименование email в userEmail

var listOfMaps = [

{

"email": "[email protected]",

"encryptedPassword": "secret-pass"

},

{

"email": "[email protected]",

"encryptedPassword": "my-secret"

}

];

renameResultKeys(listOfMaps,

{"email": "userEmail"});

Джо: Превосходно! Я думаю, вы готовы перейти к продвинутой обработке данных.

10.4. Продвинутая обработка данных

В некоторых случаях нам необходимо изменить структуру строк, возвращаемых

SQL-запросом (например, объединить поля из разных строк в единую карту). Это

можно было бы сделать на уровне SQL-запроса, используя расширенные функции, такие как объединение JSON в PostgreSQL. Однако иногда имеет смысл изменить

Глава 10. Операции с базой данных

259

данные внутри приложения, поскольку это упрощает выполнение SQL-запросов.

Как и в случае простого сценария манипулирования данными из предыдущего раздела, как только мы напишем код, который реализует некоторые манипуляции

с данными, мы можем свободно использовать один и тот же код для аналогичных

вариантов использования, даже если они связаны с объектами данных разных

типов.

Тео: Какой вид продвинутой обработки данных вы имели в виду?

Джо: Вы узнаете об этом через минуту, но сначала SQL-задание для вас. Напишите

SQL-запрос, который возвращает книги, включая имена авторов, содержащие

слово habit в названии.

Тео: Я попробую это сделать.

После некоторых проб и ошибок Тео удается это сделать. Он записывает SQL-запрос, который объединяет три таблицы: books, book_authors и authors.

Листинг 10.17. SQL-запрос для получения книг, содержащих слово habit SELECT

title,

isbn,

authors.name AS author_name

FROM

books

INNER JOIN

book_authors

ON books.isbn = book_authors.book_isbn

INNER JOIN

authors

ON book_authors.author_id = authors.id

WHERE books.title LIKE '%habit%';

Джо: Сколько строк в результатах?

Тео подходит к доске. Он быстро набрасывает таблицу с результатами, затем отвечает на вопрос Джо. Поскольку у «7 Habits of Highly Effective People» два автора, Тео дважды перечисляет книгу в табл. 10.6.

Тео: Три ряда.

Таблица 10.6. Результаты SQL-запроса, который извлекает книги,

название которых содержит слово habit, включая имена авторов

title isbn

author_name

7 Habits of Highly Effective People

978-1982137274

Sean Covey

7 Habits of Highly Effective People

978-1982137274

Stephen Covey

The Power of Habit

978-0812981605

Charles Duhigg

260

Часть 2. Масштабируемость

Джо: И сколько книг?

Тео: Три.

Джо: Можете ли вы показать мне результаты SQL-запроса в виде списка карт?

Тео: Конечно.

Листинг 10.18. Список карт с результатами для листинга 10.17

[

{

"title": "7 Habits of Highly Effective People",

"isbn": "978-1982137274",

"publication_year": "Sean Covey"

},

{

"title": "7 Habits of Highly Effective People",

"isbn": "978-1982137274",

"author_name": "Stephen Covey"

},

{

"title": "The Power of Habit",

"isbn": "978-0812981605",

"author_name": "Charles Duhigg"

}

]

Джо: И как выглядит список карт, которые нам нужно вернуть?

Тео: Это список с двумя картами, где имена авторов объединены в список. Позвольте-ка мне написать код для этого.

Листинг 10.19. Объединение имен авторов в список

[

{

"isbn": "978-1982137274",

"title": "7 Habits of Highly Effective People",

"authorNames": [

"Sean Covey",

"Stephen Covey"

]

},

{

"isbn": "978-0812981605",

"title": "The Power of Habit",

"authorNames": ["Charles Duhigg"]

}

]

Глава 10. Операции с базой данных

261

Джо: Идеально! Теперь давайте рассмотрим пример продвинутой задачи манипулирования данными, в которой мы преобразуем список карт, возвращенных из

базы данных, в список карт, в которых объединены имена авторов.

Тео: Хм... Звучит не так уж и просто.

Джо: Позвольте мне разбить задачу на два этапа. Сначала мы группируем строки, принадлежащие одной и той же книге (с одинаковым ISBN). Затем в каждой

группе мы объединяем имена авторов в список. Подождите, я нарисую это в ви-де конвейера обработки данных.

Джо подходит к доске. Он рисует диаграмму (рис. 10.3).

Джо: Имеет ли сейчас это смысл?

Рис. 10.3. Конвейер данных для объединения имен авторов

262

Часть 2. Масштабируемость

Тео: Да, конвейер данных имеет смысл, но я понятия не имею, как написать код, который его реализует!

Джо: Позвольте мне показать шаг за шагом. Давайте начнем с группировки книг

с одинаковым ISBN с помощью _.groupBy.

Листинг 10.20. Группировка строк по ISBN

var sqlRows = [

{

"title": "7 Habits of Highly Effective People",

"isbn": "978-1982137274",

"author_name": "Sean Covey"

},

{

"title": "7 Habits of Highly Effective People",

"isbn": "978-1982137274",

"author_name": "Stephen Covey"

},

{

"title": "The Power of Habit",

"isbn": "978-0812981605",

"author_name": "Charles Duhigg"

}

];

_.groupBy(sqlRows, "isbn");

Листинг 10.21. Строки, сгруппированные по ISBN

{

"978-0812981605": [

{

"author_name": "Charles Duhigg",

"isbn": "978-0812981605",

"title": "The Power of Habit"

}

],

"978-1982137274": [

{

"author_name": "Sean Covey",

"isbn": "978-1982137274",

"title": "7 Habits of Highly Effective People"

},

{

"author_name": "Stephen Covey",

"isbn": "978-1982137274",

Глава 10. Операции с базой данных

263

"title": "7 Habits of Highly Effective People"

}

]

}

Тео: Каков следующий шаг?

Джо: Теперь нам нужно взять каждый список строк в rowsByIsbn и объединить имена авторов.

Тео: И как мы это сделаем?

Джо: Давайте сделаем это в списке из двух строк для «7 Habits of Highly Effective People». Код выглядит следующим образом.

Листинг 10.22. Объединение имен авторов

var rows7Habits = [

{

"author_name": "Sean Covey",

"isbn": "978-1982137274",

"title": "7 Habits of Highly Effective People"

},

{

"author_name": "Stephen Covey",

"isbn": "978-1982137274",

"title": "7 Habits of Highly Effective People"

}

];

var authorNames = _.map(rows7Habits, "author_name");❶

var firstRow = _.nth(rows7Habits, 0);

var bookInfoWithAuthorNames = _.set(firstRow, "authorNames", authorNames); _.omit(bookInfoWithAuthorNames, "author_name");❷

❶ Извлекает имена авторов из всех строк.

❷ Удаляет поле author_name.

Джо: Сначала мы берем имена авторов из всех строк. Затем берем первую строку

за основу для информации о книге, добавляем поле authorNames и удаляем поле

author_name.

Тео: Можем ли мы сделать из этого функцию?

Джо: Именно это я и хотел предложить!

Тео: Я вызову функцию aggregateField. Она получит три аргумента: строки, имя

поля для агрегирования и имя поля, которое содержит объединение.

Тео поворачивается к своему ноутбуку. Через пару минут на его экране отображается реализация для aggregateField.

264

Часть 2. Масштабируемость

Листинг 10.23. Объединение произвольного поля

function aggregateField(rows, fieldName, aggregateFieldName) {

var aggregatedValues = _.map(rows, fieldName);

var firstRow = _.nth(rows, 0);

var firstRowWithAggregatedValues = _.set(firstRow,

aggregateFieldName,

aggregatedValues);

return _.omit(firstRowWithAggregatedValues, fieldName);

}

Джо: Вы не могли бы написать тест-кейс, чтобы убедиться, что ваша функция

работает должным образом?

Тео: С удовольствием! Смотрите!

Листинг 10.24. Тест-кейс для aggregateField

var expectedResults = {

"isbn": "978-1982137274",

"title": "7 Habits of Highly Effective People",

"authorNames": [

"Sean Covey",

"Stephen Covey"

]

};

_.isEqual(expectedResults,

aggregateField(rows7Habits,

"author_name",

"authorNames"));

Джо: Превосходно! Теперь, когда у нас есть функция, которая объединяет поле из

списка строк, нам нужно только сопоставить функцию со значениями нашего

rowsByIsbn. Позвольте мне закодить это.

Листинг 10.25. Объединение имен авторов в rowsByIsbn

var rowsByIsbn = _.groupBy(sqlRows, "isbn");

var groupedRows = _.values(rowsByIsbn);

_.map(rowsByIsbn, function(groupedRows) {

return aggregateField(groupedRows, "author_name",

"authorNames");

})

Глава 10. Операции с базой данных

265

Тео: Почему вы взяли значения rowsByIsbn?

Джо: Потому что на самом деле нас не волнуют ключи в rowsByIsbn. Мы заботимся

только о группировке строк в значениях хеш-карты.

Тео: Позвольте мне попытаться объединить все, что мы сделали, и написать функцию, которая получает список строк и возвращает список информации о книге

с именами авторов, объединенными в список.

Джо: Удачи, мой друг!

Для Тео это не так сложно, как кажется. После пары проб и ошибок он приходит

к коду и тест-кейсу.

Листинг 10.26. Объединение поля в списке строк

function aggregateFields(rows, idFieldName,

fieldName, aggregateFieldName) {

var groupedRows = _.values(_.groupBy(rows, idFieldName));

return _.map(groupedRows, function(groupedRows) {

return aggregateField(groupedRows, fieldName,

aggregateFieldName);

});

}

var sqlRows = [

{

"title": "7 Habits of Highly Effective People",

"isbn": "978-1982137274",

"author_name": "Sean Covey"

},

{

"title": "7 Habits of Highly Effective People",

"isbn": "978-1982137274",

"author_name": "Stephen Covey"

},

{

"title": "The Power of Habit",

"isbn": "978-0812981605",

"author_name": "Charles Duhigg"

}

];

var expectedResults =

[

{

"authorNames": [

"Sean Covey",

"Stephen Covey"

],

266

Часть 2. Масштабируемость

"isbn": "978-1982137274",

"title": "7 Habits of Highly Effective People"

},

{

"authorNames": ["Charles Duhigg"],

"isbn": "978-0812981605",

"title": "The Power of Habit",

}

];

_.isEqual(aggregateFields(sqlRows,

"isbn",

"author_name",

"authorNames"),

expectedResults);

Тео: Я думаю, что у меня получилось.

Джо: Поздравляю! Я горжусь вами, Тео.

Теперь Тео понимает, что имел в виду Джо, когда сказал ему «облако есть облако», когда они возвращались из парка в офис. Вместо того чтобы ограничивать данные

пределами наших объектов, ДОП направляет нас к представлению данных как данных.

Итоги

Внутри наших приложений мы представляем данные, извлеченные из базы данных, независимо от того, являются ли они реляционными или нереляционными, в виде общей структуры данных.

В случае реляционной базы данных данные представляются в виде списка карт.

Представление данных из базы данных в виде данных снижает сложность системы, поскольку для этого нам не нужны шаблоны проектирования или сложные

иерархии классов.

Мы можем свободно манипулировать данными из базы данных с помощью общих функций, таких как возврат списка, составленного из значений некоторых

полей данных, создание версии карты без поля данных или группировка карт

в списке в соответствии со значением поля данных.

Мы используем универсальные функции для манипулирования данными

с большой гибкостью, используя тот факт, что внутри хеш-карты мы получаем

доступ к значению поля через его имя, представленное в виде строки.

Когда мы упаковываем нашу логику манипулирования данными в пользовательские функции, которые получают имена полей в качестве аргументов, мы можем

использовать эти функции для различных объектов данных.

Глава 10. Операции с базой данных

267

Лучший способ манипулировать данными — это представлять данные как данные.

Мы представляем данные из базы данных с помощью общих наборов данных и

манипулируем ими с помощью общих функций.

Доступ к данным из базы данных NoSQL осуществляется аналогично подходу, представленному в этой главе для доступа к данным из реляционной базы данных.

При позднем связывании мы заботимся о типах данных как можно позже.

Гибкость повышается, поскольку многие части системы могут свободно манипулировать данными, не имея дела с конкретными типами.

Доступ к полю в хеш-карте является более гибким, чем доступ к члену в объекте, созданном из класса.

В ДОП имена полей — это просто строки. Это позволяет нам писать универсальные функции для управления списком карт, представляющих данные, извлеченные из базы данных.

В ДОП поля — первоклассные граждане.

Мы можем реализовать переименование ключей в списке карт или агрегирование строк, возвращаемых запросом к базе данных, с помощью универсальных

функций.

JDBC расшифровывается как подключение к базе данных Java.

Преобразование результирующего набора JDBC в список карт довольно просто.

Функции Lodash, представленные в этой главе

Функция Описание

at(map, [paths])

Создает массив значений, соответствующих path из map

omit(map, [paths])

Создает карту, состоящую из полей map, не входящих в paths

nth(arr, n)

Получает элемент с индексом n в arr

groupBy(coll, f)

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

выполнения каждого элемента coll через f. Соответствующее

значение для каждого ключа представляет собой массив элементов,

отвечающих за генерацию ключа

268

Часть 2. Масштабируемость

Веб-сервисы

Верный посланник

В ЭТОЙ ГЛАВЕ РАССМАТРИВАЮТСЯ

Представление клиентского запроса в виде карты.

Представление ответа сервера в виде карты.

Дальнейшая передача данных.

Объединение данных из разных источников.

Архитектура современных информационных систем состоит из программных компонентов, написанных на различных языках программирования, таких как JSON, которые взаимодействуют по проводам, отправляя и получая данные, представленные в независимом от языка формате обмена данными. ДОП применяет тот же

принцип связи между внутренними частями программы.

СОВЕТ. Когда веб-браузер отправляет запрос веб-сервису, довольно часто сам веб-сервис отправляет запросы другим веб-сервисам, чтобы выполнить запрос веб-браузера. Одним

из популярных форматов обмена данными является JSON.

Внутри программы компоненты взаимодействуют, отправляя и получая данные, представленные в независимом от компонентов формате, а именно неизменяемые

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

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

преимуществам:

использованию общих функций манипулирования данными для манипулирования данными из нескольких источников;

передача данных осуществляется свободно, без каких-либо дополнительных

сложностей.

270

Часть 2. Масштабируемость

11.1. Другой запрос функции

После своевременного предоставления базы данных milestone Тео звонит Нэнси, чтобы поделиться хорошими новостями. Вместо того чтобы праздновать успех Тео, Нэнси спрашивает его об ETA для следующего этапа — расширенной информации

о книгах с помощью Open Library Books API. Тео говорит ей, что перезвонит ей насчет ETA к концу дня. Когда Джо приходит в офис, Тео рассказывает ему о разговоре с Нэнси.

Тео: Мне только что позвонила Нэнси, и она переживает из-за следующего этапа.

Джо: Что ждет нас на следующем этапе?

Тео: Вы помните Open Library Books API, о котором я рассказывал вам несколько

недель назад?

ПРИМЕЧАНИЕ. Вы можете найти Open Library Books API по адресу

https://openlibrary.org/dev/docs/api/books.

Джо: Нет.

Тео: Это веб-сервис, который предоставляет подробную информацию о книгах.

Джо: Круто!

Тео: Нэнси хочет расширить результаты поиска книг. Вместо извлечения информации о книге из базы данных нам нужно получить расширенную информацию

о книге из Open Library Books API.

Джо: Какого рода информация?

Тео: Все! Количество страниц, вес, физический формат, темы и т. д.

Джо: А как насчет информации из базы данных?

Тео: Кроме информации о наличии книг, она нам больше не нужна.

Джо: Вы уже ознакомились с Open Library Books API?

Тео: Это кошмар! Для некоторых книг информация содержит дюжину полей, а для

других книг она содержит только два или три поля.

Джо: Тогда в чем проблема?

Тео: Я понятия не имею, как представлять данные, которые настолько разрежены

и непредсказуемы.

Джо: Когда мы представляем данные как данные, это не проблема. Давай выпьем

кофе, и я тебе покажу.

11.2. Создание внутренностей,

подобных внешностям

Пока Тео пьет свой макиато, Джо рисует схему на доске. (На рис. 11.1 показана

схема Джо.)

Глава 11. Веб-сервисы

271

Рис. 11.1. Высокоуровневая

архитектура современной

информационной системы

Джо: Прежде чем мы углубимся в детали реализации функции расширения результатов поиска книг, позвольте мне дать вам краткое введение.

Тео: Конечно.

Джо делает глоток своего эспрессо. Затем он указывает на диаграмму на доске

(рис. 11.1).

Джо: Вам это кажется знакомым?

Тео: Конечно!

Джо: Можете ли вы показать мне примерные этапы в потоке данных веб-сервиса?

Тео: Ага.

Тео придвигается ближе к доске. Он пишет список шагов (см. врезку) рядом со

схемой архитектуры.

ЭТАПЫ ПОТОКА ДАННЫХ ВНУТРИ ВЕБ-СЕРВИСА

1. Получить запрос от клиента.

2. Применить бизнес-логику к запросу.

3. Извлечь данные из внешних источников (например, базы данных и других веб-служб).

4. Применить бизнес-логику к ответам из внешних источников.

5. Отправить ответ клиенту.

Джо: Прекрасно! Теперь приходит важное понимание о ДОП.

Тео: Я весь внимание.

Джо: Мы должны строить внутреннюю часть наших систем так же, как мы строим

внешнюю.

Тео: Что вы имеете в виду?

Джо: Как компоненты системы взаимодействуют по проводам?

Тео: Путем отправки данных.

Джо: Зависит ли формат данных от языка программирования компонентов?

Тео: Нет, довольно часто это JSON, для которого у нас есть парсеры на всех языках

программирования.

272

Часть 2. Масштабируемость

Джо: Идиома гласит, что внутри нашей программы внутренние компоненты программы должны взаимодействовать таким образом, чтобы это не зависело от

компонентов.

Тео: Я этого не понимаю.

Джо: Позвольте мне объяснить, почему традиционное ООП нарушает эту идиому.

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

Тео: Что вы имеете в виду?

Джо: Чтобы иметь возможность получить доступ к члену класса, компоненту необходимо импортировать определение класса.

Тео: Каким образом это могло быть иначе?

Джо: В ДОП, как мы уже видели, внутренние компоненты программы взаимодействуют с помощью общих наборов данных. Это похоже на то, как компоненты

системы взаимодействуют по проводам.

СОВЕТ. Мы должны строить внутреннюю часть наших систем так же, как мы строим

внешнюю.

Тео: Почему это так важно?

Джо: С точки зрения дизайна это важно, потому что это означает, что внутренние

компоненты программы слабо связаны.

Тео: Что вы подразумеваете под слабо связанными?

Джо: Я имею в виду, что компонентам не нужны знания о внутренностях других

компонентов. Единственное, что требуется знать, — это названия полей.

СОВЕТ. В ДОП внутренние компоненты программы слабо связаны.

Тео: А с точки зрения реализации?

Джо: Как вы сейчас увидите, реализовать этапы потока данных, которые вы только

что записали на доске, несложно. Все сводится к выражению бизнес-логики

в терминах общих функций манипулирования данными. Позвольте мне показать

вам диаграмму.

Джо подходит к доске и набрасывает рисунок (рис. 11.2). Когда Джо заканчивает, звонит его мобильный телефон. Он извиняется и выходит на улицу, чтобы ответить

на звонок.

Тео несколько минут стоит в одиночестве перед доской, размышляя, «как мы строим внутренние части наших систем так же, как мы строим внешние». Сам того не

замечая, он берет маркер и начинает рисовать новую диаграмму (рис. 11.3), которая

обобщает идеи, которыми Джо только что поделился с ним.

Глава 11. Веб-сервисы

273

Рис. 11.2. Внутренние

Рис. 11.3. Построение внутренних частей наших систем аналогично

компоненты

построению внешних. Внутренние компоненты веб-сервиса

дата-ориентированного

взаимодействуют с данными. В качестве примера типичный поток

веб-сервиса

веб-сервиса, обрабатывающий клиентский запрос: 1 — парсинг

клиентского запроса JSON в данные; 2 — манипулирование данными

в соответствии с бизнес-логикой; 3 — сериализация данных

в JSON-запрос к базе данных и другой веб-службе; 4 — парсинг

ответов JSON в данные; 5 — манипулирование данными

в соответствии с бизнес-логикой;

6 — сериализация данных в ответ JSON клиенту

11.3. Представление запроса клиента

в виде карты

Через несколько минут Джо возвращается. Когда он смотрит на новый рисунок Тео

(см. рис. 11.3), он выглядит довольным.

Джо: Извините, что прервался. Давайте начнем с самого начала — разберем запрос

клиента. Как вы обычно получаете параметры клиентского запроса?

Тео: Это зависит от того, что параметры могут быть отправлены как параметры

запроса URL в запросе GET или как полезная нагрузка JSON в теле запроса POST.

274

Часть 2. Масштабируемость

Джо: Давайте предположим, что мы получаем полезные данные JSON внутри веб-запроса. Можете ли вы привести мне пример полезных данных JSON для запроса расширенного поиска?

Тео: Он будет содержать текст, которому должно соответствовать название книги.

Джо: И каковы подробности полей для извлечения из Open Library Books API?

Тео: Они не будут переданы как часть полезных данных JSON, потому что они

одинаковы для всех поисковых запросов.

Джо: Я могу представить себе сценарий, в котором вы хотите, чтобы клиент сам

решал, какие поля извлекать. Например, мобильный клиент предпочел бы извлекать только самые важные поля и экономить пропускную способность сети.

Тео: Что ж, в таком случае у меня были бы две разные конечные точки поиска: одна для мобильных устройств и одна для компьютеров.

Джо: Как насчет ситуаций, когда клиент хочет отображать разные фрагменты информации в зависимости от экрана приложения? Например, на расширенном

экране результатов поиска мы отображаем все поля. На основном экране результатов поиска мы отображаем только самые важные поля. Теперь у вас есть четыре различных варианта использования: desktop extended, desktop basic, mobile extended и mobile basic. Могли бы вы создать четыре разные конечные точки?

Тео: Ладно, вы убедили меня. Давайте создадим единую конечную точку поиска и

позволим клиенту решать, какие поля извлекать.

Джо: Можете ли вы показать мне пример полезной нагрузки JSON для поискового

запроса?

Тео: Конечно.

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

Листинг 11.1. Пример полезных данных поискового запроса

{

"title": "habit",

"fields": ["title", "weight", "number_of_pages"]

}

Джо: Превосходно! Теперь первым шагом является парсинг строки JSON в структуру данных.

Тео: Позвольте мне угадать: это будет общая структура данных.

Джо: Конечно! В таком случае у нас будет карта. Обычно парсинг JSON обрабатывается фреймворком веб-сервера, но я собираюсь показать вам, как это сделать

вручную.

Тео: Подождите! Что вы подразумеваете под фреймворком веб-сервера?

Глава 11. Веб-сервисы

275

Джо: Такие вещи, как Express в Node.js, Spring в Java, Django в Python, Ruby в Rails, ASP.net в C# и т. д.

Тео: О, понятно. Итак, как вы вручную парсите строку JSON на карту?

Джо: В JavaScript мы используем JSON.parse. В Java мы используем стороннюю

библиотеку, такую как Gson (https://github.com/google/gson), поддерживаемый

Google.

Джо открывает свой ноутбук и записывает два фрагмента кода, один на JavaScript, а другой на Java с помощью Gson. Когда он заканчивает, он показывает код Тео.

Листинг 11.2. Парсинг строки JSON в JavaScript

var jsonString =

'{"title":"habit","fields":["title","weight",

"number_of_pages"]}';

JSON.parse(jsonString);

Листинг 11.3. Парсинг строки JSON в Java с помощью Gson

var jsonString =

'{"title":"habit","fields":["title","weight",

"number_of_pages"]}';

gson.fromJson(jsonString, Map.class);

Джо: Можете ли вы написать схему JSON для полезных данных поискового запроса?

Тео: Конечно. Это выглядело бы примерно так.

Листинг 11.4. Схема JSON для поискового запроса

var searchBooksRequestSchema = {

"type": "object",

"properties": {

"title": {"type": "string"},

"fields": {

"type": "array",

"items": {

"enum": [

"title",

"full_title",

"subtitle",

"publisher",

"publish_date",

"weight",

"physical_dimensions",

"number_of_pages",

"subjects",

276

Часть 2. Масштабируемость

"publishers",

"genre"

]

}

}

},

"required": ["title", "fields"]

};

Джо: Мило! Вы пометили элементы в массиве fields как перечисляемые, а не как

строки. Откуда у вас список разрешенных значений?

Тео: Нэнси дала мне список полей, которые она хочет предоставить пользователям.

Позвольте мне показать вам ее список.

Листинг 11.5. Важные поля из Open Library Books API

- title

- full_title

- subtitle

- publisher

- publish_date

- weight

- physical_dimensions

- number_of_pages

- subjects

- publishers

- genre

11.4. Представление ответа сервера

в виде карты

Когда Тео выполняет фрагмент кода, он отображает строку JSON с расширенной

информацией о «7 Habits of Highly Effective People».

Листинг 11.6. Извлечение данных из API Open Library Books

fetchAndLog(

"https:/ /openlibrary.org/isbn/978-1982137274.json"

);❶

//{

// "authors": [

//

{

//

"key": "/authors/OL383159A",

//

},

Глава 11. Веб-сервисы

277

//

{

//

"key": "/authors/OL30179A",

//

},

//

{

//

"key": "/authors/OL1802361A",

//

},

// ],

// "created": {

//

"type": "/type/datetime",

//

"value": "2020-08-17T14:26:27.274890",

// },

// "full_title": "7 Habits of Highly Effective

// People : Revised and Updated Powerful

// Lessons in Personal Change",,

// "isbn_13": [

//

"9781982137274",

// ],

// "key": "/books/OL28896586M",

// "languages": [

//

{

//

"key": "/languages/eng",

//

},

// ],

// "last_modified": {

//

"type": "/type/datetime",

//

"value": "2021-09-08T19:07:57.049009",

// },

// "latest_revision": 3,

// "lc_classifications": [

//

"",

// ],

// "number_of_pages": 432,

// "publish_date": "2020",

// "publishers": [

//

"Simon & Schuster, Incorporated",

// ],

// "revision": 3,

// "source_records": [

//

"bwb:9781982137274",

// ],

// "subtitle": "Powerful Lessons in Personal Change",

// "title": "7 Habits of Highly Effective

// People : Revised and Updated",

// "type": {

//

"key": "/type/edition",

// },

278

Часть 2. Масштабируемость

// "works": [

//

{

/3/

"key": "/works/OL2629977W",

//

},

// ],

//}

❶ Служебная функция, которая извлекает JSON и выводит его в консоль.

Джо: Просила ли Нэнси о какой-либо специальной обработке полей, возвращаемых

API?

Тео: Ничего особенного, кроме сохранения только тех полей, которые я вам показал.

Джо: И это все?

Тео: Да. Например, вот строка JSON, возвращаемая Open Library Books API для

«7 Habits of Highly Effective People» после сохранения только необходимых полей.

Листинг 11.7. Ответ Open Library для «7 Habits of Highly Effective People»

{

"title":"7 Habits of Highly Effective People : Revised and Updated",

"subtitle":"Powerful Lessons in Personal Change",

"number_of_pages":432,

"full_title":"7 Habits of Highly Effective People : Revised and Updated Powerful Lessons in Personal Change",

"publish_date":"2020",

"publishers":["Simon & Schuster, Incorporated"]

}

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

Джо: Знаете ли вы, как реализовать фильтрацию двойного поля?

Тео: Да, я проанализирую строку JSON из API в хеш-карту, как мы проанализиро-вали запрос клиента, а затем я дважды использую _.pick, чтобы сохранить только обязательные поля.

Джо: По-моему, это отличный план. Можете ли вы закодить его, включая проверку

данных, возвращаемых Open Library API?

Тео: Конечно! Позвольте мне сначала написать схему JSON для ответа Open Library API.

Тео нужно освежить в памяти материалы о составе схемы, чтобы выразить тот

факт, что либо isbn_10, либо isbn_13 являются обязательными. Через несколько

мгновений он показывает схему JSON Джо.

Глава 11. Веб-сервисы

279

Листинг 11.8. Схема JSON для ответа Open Library Books API

var basicBookInfoSchema = {

"type": "object",

"required": ["title"],

"properties": {

"title": {"type": "string"},

"publishers": {

"type": "array",

"items": {"type": "string"}

},

"number_of_pages": {"type": "integer"},

"weight": {"type": "string"},

"physical_format": {"type": "string"},

"subjects": {

"type": "array",

"items": {"type": "string"}

},

"isbn_13": {

"type": "array",

"items": {"type": "string"}

},

"isbn_10": {

"type": "array",

"items": {"type": "string"}

},

"publish_date": {"type": "string"},

"physical_dimensions": {"type": "string"}

}

};

var mandatoryIsbn13 = {

"type": "object",

"required": ["isbn_13"]

};

var mandatoryIsbn10 = {

"type": "object",

"required": ["isbn_10"]

};

var bookInfoSchema = {

"allOf": [

basicBookInfoSchema,

{

"anyOf": [mandatoryIsbn13, mandatoryIsbn10]

}

]

};

280

Часть 2. Масштабируемость

Тео: Теперь, предполагая, что у меня есть функция fetchResponseBody, которая отправляет запрос и извлекает тело ответа в виде строки, позвольте мне закодить, как выполнить извлечение. Дайте мне секунду.

Тео печатает в своей IDE в течение нескольких минут. Затем он показывает результат Джо.

Листинг 11.9. Извлечение информации о книге из API Open Library Books var ajv = new Ajv({allErrors: true});

class OpenLibraryDataSource {

static rawBookInfo(isbn) {

var url = `https:/ /openlibrary.org/isbn/${isbn}.json`;

var jsonString = fetchResponseBody(url);❶

return JSON.parse(jsonString);

}

static bookInfo(isbn, requestedFields) {

var relevantFields = ["title", "full_title",

"subtitle", "publisher",

"publish_date", "weight",

"physical_dimensions", "genre",

"subjects", "number_of_pages"];

var rawInfo = rawBookInfo(isbn);

if(!ajv.validate(bookInfoSchema, rawInfo)) {

var errors = ajv.errorsText(ajv.errors);

throw "Internal error: Unexpected result from Open Books API: " + errors;

}

var relevantInfo =

_.pick(_.pick(rawInfo, relevantFields),

requestedFields);

return _.set(relevantInfo, "isbn", isbn);

}

}

❶ Извлекает JSON в теле ответа.

ПРИМЕЧАНИЕ. Фрагменты JavaScript в этой главе написаны так, как если бы JavaScript обрабатывал ввод-вывод синхронным способом. В реальной жизни нам нужно использовать

async и await для вызовов ввода-вывода.

Джо: Выглядит хорошо! Но почему вы добавили поле isbn к карте, возвращаемой

bookInfo?

Тео: Это позволит мне объединить информацию из двух источников об одной и той

же книге.

Джо: Мне это нравится!

Глава 11. Веб-сервисы

281

11.5. Дальнейшая передача информации

Джо: Если я правильно понимаю, программе необходимо объединить два вида

данных: базовую информацию о книге из базы данных и расширенную информацию о книге из Open Library API. Как вы собираетесь объединить их в единый

фрагмент данных в ответе клиенту?

Тео: В традиционном ООП я бы создал определенный класс для каждого типа информации о книге.

Джо: Что вы имеете в виду?

Тео: Вы знаете, у меня были бы такие классы, как DBBook, OpenLibraryBook и

CombinedBook.

Джо: Хм...

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

Джо: Верно, и классы не приносят никакой дополнительной ценности, потому что

нам нужно передавать данные дальше. Знаете ли вы историю о парне, который

попросил своего друга принести цветы его невесте?

Тео: Нет.

Джо принимает торжественную позу, как будто собирается выступить перед собра-нием собратьев. Глубоко вздохнув, он рассказывает Тео следующую историю. За-чарованный, Тео внимательно слушает.

ИСТОРИЯ О ПАРНЕ, КОТОРЫЙ ПОПРОСИЛ СВОЕГО ДРУГА ПРИНЕСТИ ЦВЕТЫ ЕГО НЕВЕСТЕ

За несколько недель до их свадьбы Хьюго захотел послать цветы Айрис, своей невесте, которая отдыхала с семьей в соседнем городе. Не имея возможности поехать самому, потому что он был нужен на работе, чтобы исправить критическую ошибку в приложении

безопасности, Хьюго попросил своего друга Вилли совершить поездку и отвезти букет

цветов своей возлюбленной вместе с конвертом, содержащим любовное письмо, которое

Хьюго написал для своей невесты. Вилли, которому все равно пришлось бы ехать, любезно соглашается.

Прежде чем вручить цветы Айрис, Вилли позвонил своему другу Хьюго, чтобы сообщить

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

Вилли не сказал Хьюго, как сильно он восхищается качеством его стиля письма.

Хьюго был разочарован. «Что?! Ты читал письмо, которое я написал своей невесте?»

«Конечно!» — ответил Вилли. — «Это было необходимо сделать, чтобы добросовестно

выполнить свой долг».

Тео: В этом нет никакого смысла! Почему Вилли должен был прочитать письмо, чтобы выполнить свой долг?

Джо: В этом-то и суть истории! В некотором смысле традиционный ООП похож на

друга Хьюго, Вилли. Чтобы передавать информацию вперед, разработчики ООП

считают, что им нужно «открыть письмо» и представить информацию с помощью определенных классов.

282

Часть 2. Масштабируемость

Тео: О, я понимаю. И разработчики ДОП подражают духу того, что Хьюго ожидал

от курьера; они просто передают информацию в виде общих структур данных.

Джо: Именно.

Тео: Это тонкая, но забавная аналогия.

Джо: Давайте вернемся к вопросу объединения данных из базы данных с данными

из Books API. Есть два способа сделать это — вложение и слияние.

Джо подходит к доске. Он находит область, чтобы нарисовать таблицу для Тео

(табл. 11.1).

Таблица 11.1. Два способа объединения хеш-карт

Способ Преимущества

Недостатки

Вложение

Нет необходимости улаживать

Результат не является ровным

конфликты

Слияние

Результат — ровный

Необходимость улаживать конфликты

Тео: Как работает вложенность?

Джо: При вложении мы добавляем поле с именем extendedInfo к информации, по-лученной из Open Library API.

Тео: Понимаю. А как насчет слияния?

Джо: При слиянии мы объединяем поля обеих карт в единую карту.

Тео: Если на обеих картах есть поля с одинаковым названием, что тогда?

Джо: Тогда у вас возникает конфликт слияния, и вам нужно решить, как справиться

с этим конфликтом. В этом недостаток слияния.

ПРИМЕЧАНИЕ. При объединении карт нужно подумать о появлении конфликтующих полей.

Тео: Будем надеяться, что в контексте расширенных результатов поиска карты не

имеют никаких общих полей.

Джо: Тогда давайте объединим их!

Тео: Нужно ли мне писать собственный код для объединения двух карт?

Джо: Нет! Как вы, возможно, помните из одного из наших предыдущих сеансов, Lodash предоставляет удобную функцию _.merge.

ПРИМЕЧАНИЕ. _.merge было введено в главе 5.

Тео: Не могли бы вы освежить мою память?

Джо: Конечно. Покажите мне пример карт с данными из базы данных и данными

из Open Library Books API, и я покажу вам, как их объединить.

Тео: Из базы данных мы получаем только два поля: isbn и available (листинг 11.10).

Из Open Library API мы получаем шесть полей (листинг 11.11).

Глава 11. Веб-сервисы

283

Листинг 11.10. Карта с информацией о книге из базы данных

var dataFromDb = {

"available": true,

"isbn": "978-1982137274"

};

Листинг 11.11. Карта с информацией о книгах из Open Library Books API var dataFromOpenLib = {

"title":"7 Habits of Highly Effective People : Revised and Updated",

"subtitle":"Powerful Lessons in Personal Change",

"number_of_pages":432,

"full_title":"7 Habits of Highly Effective People : \ Revised and Updated Powerful Lessons in Personal Change",

"publish_date":"2020",

"publishers":["Simon & Schuster, Incorporated"]

};

Джо: После вызова _.merge результатом будет карта с полями из обеих карт.

Листинг 11.12. Слияние двух карт

_.merge(dataFromDb, dataFromOpenLib);

//{

// "available": true,

// "full_title": "7 Habits of Highly Effective People :\

// Revised and Updated Powerful Lessons in Personal Change",

// "isbn": "978-1982137274",

// "number_of_pages": 432,

// "publish_date": "2020",

// "publishers": [ "Simon & Schuster, Incorporated"],

// "subtitle": "Powerful Lessons in Personal Change",

// "title": "7 Habits of Highly Effective People : Revised and Updated"

//}

Тео: Позвольте мне закодить схему JSON для ответа поиска книг. Вот как это могло бы выглядеть.

Листинг 11.13. Схема JSON для ответа на поиск книг

var searchBooksResponseSchema = {

"type": "object",

"required": ["title", "isbn", "available"],

"properties": {

"title": {"type": "string"},

284

Часть 2. Масштабируемость

"available": {"type": "boolean"},

"publishers": {

"type": "array",

"items": {"type": "string"}

},

"number_of_pages": {"type": "integer"},

"weight": {"type": "string"},

"physical_format": {"type": "string"},

"subjects": {

"type": "array",

"items": {"type": "string"}

},

"isbn": {"type": "string"},

"publish_date": {"type": "string"},

"physical_dimensions": {"type": "string"}

}

};

Тео: Да! Я думаю, что теперь у нас есть все необходимое для расширения результатов нашего поиска.

11.6. Расширение результатов поиска

в действии

Джо: Можете ли вы описать шаги потока расширения данных?

Тео: Конечно.

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

ШАГИ ДЛЯ ПОТОКА РАСШИРЕНИЯ ДАННЫХ РЕЗУЛЬТАТОВ ПОИСКА

1. Получение запроса от клиента.

2. Извлечение из запроса клиента запроса и поля для извлечения из Open Library.

3. Извлечение из базы данных книги, соответствующей запросу.

4. Извлечение информации из Open Library для каждого ISBN, соответствующего запросу.

5. Извлечение из Open Library ответов для обязательных полей.

6. Объединение информации о книге из базы данных с информацией из Open Library.

7. Отправка ответа клиенту.

Джо: Идеально! Хотите попробовать реализовать это?

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

на то, что мы делали в прошлом месяце.

ПРИМЕЧАНИЕ. Смотрите урок прошлого месяца в главе 10.

Глава 11. Веб-сервисы

285

Джо: На самом деле это даже проще, потому что вам не нужно объединять таблицы.

Тео: Правильно, мне нужны значения только для столбцов isbn и available.

Тео немного поработал в своей IDE. Он начинает с поиска книги из базы данных.

Листинг 11.14. Извлечение книг, название которых соответствует запросу

var dbSearchResultSchema = {

"type": "array",

"items": {

"type": "object",

"required": ["isbn", "available"],

"properties": {

"isbn": {"type": "string"},

"available": {"type": "boolean"}

}

}

};

class CatalogDB {

static matchingBooks(title) {

var matchingBooksQuery = `

SELECT isbn, available

FROM books

WHERE title = like '%$1%';

`;

var books = dbClient.query(catalogDB, matchingBooksQuery,

[title]);

if(!ajv.validate(dbSearchResultSchema, books)) {

var errors = ajv.errorsText(ajv.errors);

throw "Internal error: Unexpected result from the

database: " +

errors;

}

return books;

}

}

Джо: Пока что все хорошо...

Тео: Далее я перейду к реализации извлечения информации о книге из Open Library для нескольких книг. К сожалению, Open Library Books API не поддерживает

одновременный запрос нескольких книг. Мне нужно будет отправить по одному

запросу на каждую книгу.

Джо: Это немного раздражает. Давайте упростим нашу жизнь и представим, что

_.map работает с асинхронными функциями. В реальной жизни вам понадобится

286

Часть 2. Масштабируемость

что-то вроде Promise.all, чтобы отправлять запросы параллельно и объединять

ответы.

Тео: Хорошо, тогда все довольно просто. Я возьму код поиска книги и добавлю

функцию multipleBookInfo, которая отображает поверх bookInfo.

Тео просматривает код поиска книги в листинге 11.9, а затем концентрируется, вводя текст в IDE. Когда он закончит, он показывает результат в листинге 11.15

Джо.

Листинг 11.15. Извлечение информации о книге из Open Library для нескольких книг

class OpenLibraryDataSource {

static rawBookInfo(isbn) {

var url = `https:/ /openlibrary.org/isbn/${isbn}.json`;

var jsonString = fetchResponseBody(url);

return JSON.parse(jsonString);

}

static bookInfo(isbn, requestedFields) {

var relevantFields = ["title", "full_title",

"subtitle", "publisher",

"publish_date", "weight",

"physical_dimensions", "genre",

"subjects", "number_of_pages"];

var rawInfo = rawBookInfo(isbn);

if(!ajv.validate(dbSearchResultSchema, bookInfoSchema)) {

var errors = ajv.errorsText(ajv.errors);

throw "Internal error: Unexpected result from Open Books API: " + errors;

}

var relevantInfo =

_.pick(_.pick(rawInfo, relevantFields),

requestedFields);

return _.set(relevantInfo, "isbn", isbn);

}

static multipleBookInfo(isbns, fields) {

return _.map(function(isbn) {

return bookInfo(isbn, fields);

}, isbns);

}

}

Джо: Хорошо! Теперь начинается самое интересное: объединение информации из

нескольких источников данных.

Тео: Ага. У меня в руках два массива: один с информацией о книге из базы данных

и один с информацией о книге из Open Library. Мне как-то нужно объединить

массивы, но я не уверен, что могу предположить, что позиции информации

о книге одинаковы в обоих массивах.

Глава 11. Веб-сервисы

287

Джо: Что бы вы хотели иметь в своих руках?

Тео: Жаль, что у меня нет двух хеш-карт.

Джо: И какими будут ключи в хеш-картах?

Тео: Книжные номера ISBN.

Джо: Что ж, у меня для вас хорошие новости: ваше желание исполнено!

Тео: Как?

Джо: Lodash предоставляет функцию с именем _.keyBy, которая преобразует массив

в карту.

Тео: Я не могу в это поверить. Можете ли вы показать мне пример?

Джо: Конечно. Давайте вызовем _.keyBy для массива с двумя книгами.

Листинг 11.16. Преобразование массива в карту с помощью _.keyBy

var books = [

{

"title": "7 Habits of Highly Effective People",

"isbn": "978-1982137274",

"available": true

},

{

"title": "The Power of Habit",

"isbn": "978-0812981605",

"available": false

}

];

_.keyBy(books, "isbn");

Джо: А вот и результат.

Листинг 11.17. Результат keyBy

{

"978-0812981605": {

"available": false,

"isbn": "978-0812981605",

"title": "The Power of Habit"

},

"978-1982137274": {

"available": true,

"isbn": "978-1982137274",

"title": "7 Habits of Highly Effective People"

}

}

288

Часть 2. Масштабируемость

Тео: keyBy потрясающая!

Джо: Не преувеличивай, мой друг; _.keyBy очень похожа на _.groupBy. Единственное отличие заключается в том, что _.keyBy предполагает, что в каждой группе

есть только один элемент.

Тео: Я думаю, что с помощью _.keyBy я смогу написать общую функцию joinArrays.

Джо: Я рад видеть, что вы мыслите в терминах реализации бизнес-логики с помощью общих функций манипулирования данными.

СОВЕТ. Многие части бизнес-логики могут быть реализованы с помощью общих функций манипулирования данными.

Тео: Функция joinArrays должна получать массивы и имя поля, для которых мы

определяем два элемента, которые необходимо объединить, например isbn.

Джо: Помните, что, как правило, это не обязательно одно и то же имя поля для

обоих массивов.

Тео: Правильно! Итак, joinArrays должен получить четыре аргумента: два массива

и два имени полей.

Джо: Действуйте! И, пожалуйста, напишите модульный тест для joinArrays.

Тео: Конечно...

Листинг 11.18. Универсальная функция для объединения массивов

function joinArrays(a, b, keyA, keyB) {

var mapA = _.keyBy(a, keyA);

var mapB = _.keyBy(b, keyB);

var mapsMerged = _.merge(mapA, mapB);

return _.values(mapsMerged);

}

Листинг 11.19. Модульный тест для joinArrays

var dbBookInfos = [

{

"isbn": "978-1982137274",

"title": "7 Habits of Highly Effective People",

"available": true

},

{

"isbn": "978-0812981605",

"title": "The Power of Habit",

"available": false

}

];

Глава 11. Веб-сервисы

289

var openLibBookInfos = [

{

"isbn": "978-0812981605",

"title": "7 Habits of Highly Effective People",

"subtitle": "Powerful Lessons in Personal Change",

"number_of_pages": 432,

},

{

"isbn": "978-1982137274",

"title": "The Power of Habit",

"subtitle": "Why We Do What We Do in Life and Business",

"subjects": [

"Social aspects",

"Habit",

"Change (Psychology)"

],

}

];

var joinedArrays = [

{

"available": true,

"isbn": "978-1982137274",

"subjects": [

"Social aspects",

"Habit",

"Change (Psychology)",

],

"subtitle": "Why We Do What We Do in Life and Business",

"title": "The Power of Habit",

},

{

"available": false,

"isbn": "978-0812981605",

"number_of_pages": 432,

"subtitle": "Powerful Lessons in Personal Change",

"title": "7 Habits of Highly Effective People",

},

]

_.isEqual(joinedArrays,

joinArrays(dbBookInfos, openLibBookInfos, "isbn", "isbn")); Джо: Превосходно! Теперь вы готовы настроить последнюю часть конечной точки

расширенного результата поиска.

Тео: Это довольно просто. Мы извлекаем данные из базы данных и из Open Library и объединяем их.

290

Часть 2. Масштабируемость

Тео работает довольно быстро. Затем он показывает Джо код.

Листинг 11.20. Поиск книг и расширенная информация о книгах

class Catalog {

static enrichedSearchBooksByTitle(searchPayload) {

if(!ajv.validate(searchBooksRequestSchema, searchPayload)) {

var errors = ajv.errorsText(ajv.errors);

throw "Invalid request:" + errors;

}

var title = _.get(searchPayload, "title");

var fields = _.get(searchPayload, "fields");

var dbBookInfos = CatalogDataSource.matchingBooks(title);

var isbns = _.map(dbBookInfos, "isbn");

var openLibBookInfos =

OpenLibraryDataSource.multipleBookInfo(isbns, fields);

var res = joinArrays(dbBookInfos, openLibBookInfos);

if(!ajv.validate(searchBooksResponseSchema, request)) {

var errors = ajv.errorsText(ajv.errors);

throw "Invalid response:" + errors;

}

return res;

}

}

Теперь начинается самая сложная часть. Тео тратит несколько минут на размышления о простоте кода, реализующего конечную точку расширенного поиска. Он

думает о том, что классы намного менее сложны, когда мы используем их только

для агрегирования функций без состояния, которые работают с аналогичными

объектами домена, а затем приступает к построению кода.

Листинг 11.21. Схема для конечной точки расширенного поиска

(часть Open Books API)

var basicBookInfoSchema = {

"type": "object",

"required": ["title"],

"properties": {

"title": {"type": "string"},

"publishers": {

"type": "array",

"items": {"type": "string"}

},

Глава 11. Веб-сервисы

291

"number_of_pages": {"type": "integer"},

"weight": {"type": "string"},

"physical_format": {"type": "string"},

"subjects": {

"type": "array",

"items": {"type": "string"}

},

"isbn_13": {

"type": "array",

"items": {"type": "string"}

},

"isbn_10": {

"type": "array",

"items": {"type": "string"}

},

"publish_date": {"type": "string"},

"physical_dimensions": {"type": "string"}

}

};

var mandatoryIsbn13 = {

"type": "object",

"required": ["isbn_13"]

};

var mandatoryIsbn10 = {

"type": "object",

"required": ["isbn_10"]

};

var bookInfoSchema = {

"allOf": [

basicBookInfoSchema,

{

"anyOf": [mandatoryIsbn13, mandatoryIsbn10]

}

]

};

Листинг 11.22. Конечная точка расширенного поиска (часть Open Books API) var ajv = new Ajv({allErrors: true});

class OpenLibraryDataSource {

static rawBookInfo(isbn) {

var url = `https:/ /openlibrary.org/isbn/${isbn}.json`;

var jsonString = fetchResponseBody(url);

292

Часть 2. Масштабируемость

return JSON.parse(jsonString);

}

static bookInfo(isbn, requestedFields) {

var relevantFields = ["title", "full_title",

"subtitle", "publisher",

"publish_date", "weight",

"physical_dimensions", "genre",

"subjects", "number_of_pages"];

var rawInfo = rawBookInfo(isbn);

if(!ajv.validate(bookInfoSchema, rawInfo)) {

var errors = ajv.errorsText(ajv.errors);

throw "Internal error: Unexpected result from Open Books API: " +

errors;

}

var relevantInfo = _.pick(

_.pick(rawInfo, relevantFields),

requestedFields);

return _.set(relevantInfo, "isbn", isbn);

}

static multipleBookInfo(isbns, fields) {

return _.map(function(isbn) {

return bookInfo(isbn, fields);

}, isbns);

}

}

Листинг 11.23. Конечная точка расширенного поиска

(часть базы данных)

var dbClient;

var dbSearchResultSchema = {

"type": "array",

"items": {

"type": "object",

"required": ["isbn", "available"],

"properties": {

"isbn": {"type": "string"},

"available": {"type": "boolean"}

}

}

};

class CatalogDB {

static matchingBooks(title) {

var matchingBooksQuery = `

Глава 11. Веб-сервисы

293

SELECT isbn, available

FROM books

WHERE title = like '%$1%';

`;

var books = dbClient.query(catalogDB, matchingBooksQuery, [title]); if(!ajv.validate(dbSearchResultSchema, books)) {

var errors = ajv.errorsText(ajv.errors);

throw "Internal error: Unexpected result from the database: " + errors;

}

return books;

}

}

Листинг 11.24. Схема для реализации конечной точки расширенного поиска

var searchBooksRequestSchema = {

"type": "object",

"properties": {

"title": {"type": "string"},

"fields": {

"type": "array",

"items": {

"type": [

"title",

"full_title",

"subtitle",

"publisher",

"publish_date",

"weight",

"physical_dimensions",

"number_of_pages",

"subjects",

"publishers",

"genre"

]

}

}

},

"required": ["title", "fields"]

};

var searchBooksResponseSchema = {

"type": "object",

Назад: 7.3. Гибкость и строгость схемы
Дальше: 11.7. Доставка в срок