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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Привет, Хабр!

У нас выходит долгожданное второе издание книги "Веб-разработка с применением Node и Express".



В рамках исследования этой темы нами была найдена концептуальная статья о проектировании веб-API по модели, где вместо ключей и значений базы данных применяются ссылки на ресурсы. Оригинал — из блога Google Cloud, добро пожаловать под кат.


Когда мы занимаемся моделированием информации, ключевой вопрос – в том, как обозначить отношение и взаимоотношение между двумя сущностями. Описывать закономерности, наблюдаемые в реальном мире, в терминах сущностей и их взаимоотношений — это фундаментальная идея, восходящая, как минимум, к Древней Греции. Она играет основополагающую роль и в современной IT-отрасли.

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

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

Наиболее распространенный способ, применяемый разработчиками API для выражения взаимоотношений — это предоставление ключей базы данных или прокси для них в полях тех сущностей, которые ассоциированы с этими ключами. Однако, как минимум в одном классе API (веб-ориентированных) такому подходу есть более предпочтительная альтернатива: использование веб-ссылок.

Согласно стандарту Internet Engineering Task Force (IETF), веб-ссылку можно представить как инструмент для описания отношений между страницами в вебе. Наиболее известные веб-ссылки – те, что фигурируют на HTML-страницах и заключаются в элементы link или anchor, либо в заголовки HTTP. Но ссылки также могут фигурировать и в ресурсах API, а при использовании их вместо внешних ключей существенно сокращается объем информации, которую поставщику API приходится дополнительно документировать, а пользователю – изучать.

Ссылка – это элемент в одном веб-ресурсе, содержащий указание на другой веб-ресурс, а также название взаимоотношения между двумя ресурсами. Ссылка на другую сущность записывается в особом формате, именуемом «уникальный идентификатор ресурса» (URI), для которого существует стандарт IETF. В этом стандарте словом «ресурс» называется любая сущность, на которую указывает URI. Название типа отношения в ссылке можно считать аналогичным имени того столбца базы данных, в котором содержатся внешние ключи, а URI в ссылке аналогичен значению внешнего ключа. Наиболее полезны из всех URI – те, по которым можно получить информацию о ресурсе, на который проставлена ссылка, при помощи стандартного веб-протокола. Такие URI называются «унифицированный указатель ресурса» (URL) — и наиболее важной разновидностью URL для API являются HTTP URL.

В то время как ссылки не находят широкого применения в API, некоторые очень известные веб-API все-таки основаны на HTTP URL, используемых в качестве средства представления взаимоотношений. Таковы, например, Google Drive API и GitHub API. Почему так складывается? В этой статье я покажу, как на практике строится использование внешних ключей API, объясню их недостатки по сравнению с использованием ссылок, и расскажу, как преобразовать дизайн, использующий внешние ключи, в такой, где применяются ссылки.

Представление взаимоотношений при помощи внешних ключей


Рассмотрим популярное учебное приложение «зоомагазин». В этом приложении хранятся записи для отслеживания информации о питомцах и их владельцах. У питомцев есть атрибуты, в частности, кличка, вид и порода. У владельцев есть имена и адреса. Каждый питомец взаимосвязан со своим владельцем, а обратное взаимоотношение позволяет найти всех питомцев конкретного владельца.

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



Взаимоотношение между Лесси и Джо выражается так: в представлении Лесси Джо обозначен как обладатель имени и значения, соответствующих «владельцу». Обратное взаимоотношение не выражено. Значение «владельца» равно «98765», это внешний ключ. Вероятно, это самый настоящий внешний ключ базы данных — то есть, перед нами значение первичного ключа из какой-то строки какой-то таблицы базы данных. Но, даже если реализация API немного изменит значения ключей, она все равно сближается по основным характеристикам с внешним ключом.
Значение «98765» не слишком годится для непосредственного использования клиентом. В наиболее распространенных случаях клиенту, воспользовавшись этим значением, нужно составить URL, а в документации к API необходимо описать формулу для выполнения такого преобразования. Как правило, это делается путем определения шаблона URI, вот так:

/people/{person_id}

Обратное взаимоотношение – любимцы принадлежат владельцу – также можно предоставить в API, реализовав и документировав один из следующих шаблонов URI (различия между ними – только стилистические, а не по существу):

/pets?owner={person_id}
/people/{person_id}/pets


В API, спроектированных по такому принципу, обычно требуется определять и документировать много шаблонов URI. Наиболее популярным языком для определения таких шаблонов является не тот, что задан в спецификации IETF, а язык OpenAPI (ранее известный под названием Swagger). До версии 3.0 в OpenAPI не существовало способа указать, какие значения полей могут быть вставлены в какие шаблоны, поэтому часть документации требовалось составлять на естественном языке, а что-то приходилось угадывать клиенту. В версии 3.0 OpenAPI появился новый синтаксис под названием «links», призванный решить эту проблему, но для того, чтобы пользоваться этой возможностью последовательно, надо потрудиться.

Итак, при всей распространенности этого стиля, он требует от поставщика – документировать, а от клиента – выучить и использовать значительное количество шаблонов URI, не вполне хорошо описанных в нынешних спецификациях API. К счастью, есть вариант получше.

Представление взаимоотношений при помощи ссылок


Что, если бы ресурсы, показанные выше, были видоизменены следующим образом:



Основное отличие заключается в том, что в данном случае значения выражены при помощи ссылок, а не при помощи значений внешнего ключа. Здесь ссылки записаны обычным JSON, в формате пар имя/значение (ниже есть раздел, в котором обсуждаются и другие подходы к написанию ссылок в JSON).

Обратите внимание: обратное взаимоотношение, то есть, от питомца к владельцу, теперь тоже реализовано явно, поскольку к представлению Joel добавлено поле "pets".

Изменение "id" на "self", в сущности, не является необходимым или важным, но существует соглашение, что при помощи "self" идентифицируется ресурс, чьи атрибуты и взаимоотношения указаны другими парами имя/значение в том же объекте JSON. "self" – это имя, зарегистрированное в IANA для этой цели.

С точки зрения реализации, заменить все ключи базы данных ссылками должно быть довольно просто – сервер преобразует все внешние ключи базы данных в URL, так, что на клиенте ничего делать уже не требуется – но сам API в таком случае существенно упрощается, а связанность между клиентом и сервером снижается. Многие шаблоны URI, которые были важны при первом дизайне, больше не требуются, и могут быть удалены из спецификации и документации API.

Теперь серверу ничто не мешает в любой момент изменить формат новых URL, не затронув клиентов (разумеется, сервер должен продолжать соблюдать все URL, сформулированные ранее). URL, передаваемый клиенту сервером, должен будет включать первичный ключ сущности, указанный в базе данных, плюс некоторую информацию о маршрутизации. Но, поскольку клиент просто повторяет URL, отзываясь серверу, и клиенту никогда не приходится выполнять синтаксический разбор URL, клиентам не обязательно знать, как форматировать URL. В результате снижается степень связанности между клиентом и сервером. Сервер даже может прибегнуть к обфускации собственных URL при помощи base64 или подобных кодировок, если хочет подчеркнуть клиентам, что те не должны «гадать», каков формат URL, либо делать выводы о значении URL по их формату.

В предыдущем примере я использовал в ссылках относительную форму записи URI, например, /people/98765. Возможно, клиенту было бы немного удобнее (хотя, автору при форматировании этого поста было не слишком сподручно), если бы я выразил URI в абсолютной форме, напр. pets.org/people/98765. Клиентам необходимо знать лишь стандартные правила URI, определенные в спецификациях IETF, чтобы преобразовывать такие URI из одной формы в другую, поэтому выбор конкретной формы URI не так важен, как могло бы показаться на первый взгляд. Сравните эту ситуацию с описанным выше преобразованием из внешнего ключа в URL, для чего требовались конкретные знания об API зоомагазина. Относительные URL несколько удобнее для тех, кто занимается реализацией сервера, о чем рассказано ниже, но абсолютные URL, пожалуй, удобнее для большинства клиентов. Возможно, именно поэтому в API Google Drive и GitHub используются абсолютные URL.

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

Подводные камни


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

Перед многими реализациями API поставлены обратные прокси, обеспечивающие безопасность, балансировку нагрузки и пр. Некоторые прокси «любят» переписывать URL. Когда в API для представления взаимоотношений используются внешние ключи, единственный URL, который требуется переписать в прокси – это главный URL запроса. В HTTP этот URL разделяется между адресной строкой (первая строка заголовка) и заголовком хоста.

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

  1. Не переписывайте URL в прокси. Я стараюсь избегать переписывания URL, но в вашей среде может не быть такой возможности.
  2. В прокси аккуратно найдите и переназначьте им формат везде, где они фигурируют в запросе или в отклике. Я так никогда не делал, поскольку мне это кажется сложным, чреватым ошибками и неэффективным, но кто-то, возможно, так поступает.
  3. Записывайте все ссылки в относительном виде. Можно не только встроить во все прокси некоторые возможности по перезаписи URL; более того, относительные URL могут упростить использование одного и того же кода в тестировании и в продакшене, так как код не придется конфигурировать и знать для этого его хост-имя. Если писать ссылки с использованием относительных URL, то есть, с единственным ведущим слэшем, как я показал в примере выше, то возникают некоторые минусы как для сервера, так и для клиента. Но в таком случае в прокси появляется лишь возможность сменить хост-имя (точнее, те части URL, которые называются «схемой» и «источником»), но не путь. В зависимости от того, как построены ваши URL, вы можете реализовать в прокси некоторую возможность переписывать пути, если готовы писать ссылки с использованием относительных URL без ведущих слэшей, но я так никогда не делал, поскольку полагаю, что серверам будет сложно записывать такие URL как следует.


Относительными URL без ведущих слэшей также сложнее пользоваться клиентам, поскольку они должны работать со стандартизированной библиотекой, а не обходиться простым сцеплением строк для обработки этих URL, а также тщательно понимать и сохранять базовый URL.

Использование стандартизированной библиотеки для обращения с URL – в любом случае, хорошая практика для клиентов, но на многих клиентах так не делается.
При использовании ссылок вам также, возможно, придется перепроверить версионирование вашего API. Во многих API принято ставить номера версий в URL, вот так:

/v1/pets/12345
/v2/pets/12345
/v1/people/98765
/v2/people/98765


Это версионирование такого рода, где данные по конкретному ресурсу можно одновременно просматривать более чем в одном «формате» — здесь не идет речь о таких версиях, которые со временем заменяют друг друга по мере последовательного редактирования.

Эта ситуация весьма напоминает возможность просмотра одного и того же документа на нескольких естественных языках, для чего существует веб-стандарт; какая жалость, что нет подобного стандарта для версий. Присваивая каждой версии собственный URL, вы повышаете каждую версию по статусу до полноценного веб-ресурса. Нет ничего дурного с «версионными URL» такого рода, но они не подходят для выражения ссылок. Если клиент запрашивает Лесси в формате версии 2, это не означает, что он также хочет получить в формате 2 и информацию о Джо, хозяине Лесси, поэтому сервер не может выбрать, какой номер версии поставить в ссылку.

Возможно, формат 2 для описания владельцев даже не будет предусмотрен. Также нет концептуального смысла в том, чтобы использовать в ссылках конкретную версию URL – ведь Лесси принадлежит не конкретной версии Джо, а Джо как таковому. Поэтому, даже если вы предоставляете URL в формате /v1/people/98765 и идентифицируете таким образом конкретную версию Джо, то также должны предоставлять URL /people/98765 для идентификации самого Джо, и именно второй вариант использовать в ссылках. Другой вариант – определить только URL /people/98765 и позволить клиентам выбирать конкретную версию, включая для этого заголовок запроса. Для этого заголовка нет никакого стандарта, но, если называть его Accept-Version, то такой вариант хорошо сочетается с именованием стандартных заголовков. Лично я предпочитаю использовать для версионирования заголовок и избегаю ставить в URL номера версий. но URL с номерами версий популярны, и я часто реализую и заголовок. и «версионные URL», так как легче реализовать оба варианта, чем спорить, какой лучше. Подробнее о версионировании API можете почитать в этой статье.

Возможно, вам все равно потребуется документировать некоторые шаблоны URL


В большинстве веб-API URL нового ресурса выделяется сервером, когда новый ресурс создается при помощи метода POST. Если вы пользуетесь этим методом для создания ресурсов и указываете взаимоотношения при помощи ссылок, то вам не требуется публиковать шаблон для URI этих ресурсов. Однако, некоторые API позволяют клиенту контролировать URL нового ресурса. Позволяя клиентам контролировать URL новых ресурсов, мы значительно упрощаем многие паттерны написания скриптов API разработчикам клиентской части, а также поддерживаем сценарии, в которых API используется для синхронизации информационной модели с внешним источником информации. В HTTP для этой цели предусмотрен специальный метод: PUT. PUT означает «создай ресурс по этому URL, если он еще не существует, а если он существует – обнови его». Если ваш API позволяет клиентам создавать новые сущности при помощи метода PUT, то вы должны документировать правила составления новых URL, возможно, включив для этого шаблон URI в спецификацию API. Также можно предоставить клиентам частичный контроль над URL, включив первичное ключ-подобное значение в тело или заголовки POST. В таком случае не требуется шаблон URI для POST как такового, но клиенту все равно придется выучить шаблон URI, чтобы полноценно пользоваться достигаемой в результате предсказуемостью URI.

Другой контекст, в котором целесообразно документировать шаблоны URL – это случай, когда API позволяет клиентам кодировать запросы в URL. Не каждый API позволяет вам запрашивать свои ресурсы, но такая возможность может быть очень полезна для клиентов, и естественно позволить клиентам кодировать запросы в URL и извлекать результаты методом GET. В следующем примере показано, почему.

В вышеприведенном примере мы включили следующую пару «имя/значение» в представление Джо:

"pets": "/pets?owner=/people/98765"

Клиенту, чтобы пользоваться этим URL, не требуется что-либо знать о его структуре кроме того, что он был записан в соответствии со стандартными спецификациями. Таким образом, клиент может получить по этой ссылке список питомцев Джо, не изучая для этого никакой язык запросов. Также отсутствует необходимость документировать в API форматы его URL — но только в случае, если клиент сначала сделает запрос GET к /people/98765. Если же, кроме того, в API зоомагазина документирована возможность выполнения запросов, то клиент может составить такой же или эквивалентный URL запроса, чтобы извлечь питомцев интересующего его владельца, не извлекая перед этим самого владельца – достаточно будет знать URI владельца. Возможно, даже важнее, что клиент может формировать и запросы, подобные следующим, что в ином случае было бы невозможно:

/pets?owner=/people/98765&species=Dog
/pets?species=Dog&breed=Collie


Спецификация URI описывает для этой цели часть HTTP URL, называемую "компонент запроса" — это участок URL после первого “?” и до первого “#”. Стиль запрашивания URI, который я предпочитаю использовать – всегда ставить клиент-специфичные запросы в компонент запроса URI. Но при этом допустимо выражать клиентские запросы и в той части URL, которая называется «путь». Так или иначе, необходимо описать клиентам, как составляются эти URL — вы фактически проектируете и документируете язык запросов, специфичный для вашего API. Разумеется, также можно разрешить клиентам ставить запросы в теле сообщения, а не в URL, и пользоваться методом POST, а не GET. Поскольку существует практический лимит по размеру URL — превышая 4k байт, вы всякий раз испытываете судьбу — рекомендуется поддерживать POST для запросов, даже если вы уже поддерживаете GET.

Поскольку запросы — такая полезная возможность в API, и поскольку проектировать и реализовывать языки запросов непросто, появились такие технологии, как GraphQL. Я никогда не пользовался GraphQL, поэтому не могу рекомендовать его, но вы можете рассмотреть его в качестве альтернативы для реализации возможности запросов в вашем API. Инструменты для реализации запросов в API, в том числе, GraphQL, лучше всего использовать в качестве дополнения к стандартному HTTP API для считывания и записи ресурсов, а не как альтернативу HTTP.

И кстати… Как лучше всего писать ссылки в JSON?


В JSON, в отличие от HTML, нет встроенного механизма для выражения ссылок. Многие по-своему понимают, как ссылки должны выражаться в JSON, и некоторые подобные мнения публиковались в более или менее официальных документах, но в настоящее время нет стандартов, ратифицированных авторитетными организациями, которые бы это регламентировали. В вышеприведенном примере я выражал ссылки при помощи обычных пар имя/значение, написанных на JSON — предпочитаю такой стиль и, кстати, этот же стиль используется в Google Drive и GitHub. Другой стиль, который вам, вероятно, встретится, таков:
  {"self": "/pets/12345",
 "name": "Lassie",
 "links": [
   {"rel": "owner" ,
    "href": "/people/98765"
   }
 ]
}

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

Есть и другой стиль написания ссылок на JSON, который мне нравится, и он выглядит так:
 {"self": "/pets/12345",
 "name": "Lassie",
 "owner": {"self": "/people/98765"}
}


Польза этого стиля в том, что он явно дает: "/people/98765" – это URL, а не просто строка. Я изучил этот паттерн по RDF/JSON. Одна из причин освоить этот паттерн – вам так или иначе придется им пользоваться, всякий раз, когда вы захотите отобразить информацию об одном ресурсе, вложенную в другом ресурсе, как показано в следующем примере. Если использовать этот паттерн повсюду, код приобретает красивое единообразие:

{"self": "/pets?owner=/people/98765",
 "type": "Collection",
  "contents": [
   {"self": "/pets/12345",
    "name": "Lassie",
    "owner": {"self": "/people/98765"}
   }
 ]
}


Более подробно о том, как лучше всего использовать JSON для представления данных, рассказано в статье Terrifically Simple JSON.

Наконец, в чем же разница между атрибутом и взаимоотношением?


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

{"self": "/people/98765",
 "shoeSize": 10
}

Принято считать, что shoeSize – это атрибут, а не взаимоотношение, а 10 – это значение, а не сущность. Правда, не менее логично утверждать, что строка '10” фактически является ссылкой, записанной специальной нотацией, предназначенной для ссылок на числа, до 11-го целого числа, которое само по себе является сущностью. Если 11-е целое число – совершенно полноценная сущность, а строка '10' лишь указывает на нее, то пара имя/значение '"shoeSize": 10' концептуально является ссылкой, хотя, здесь и не используются URI.

То же можно утверждать и относительно булевых значений, и строк, поэтому все пары имя/значение в JSON можно трактовать как ссылки. Если такое восприятие JSON кажется вам оправданным, то естественно использовать простые пары имя/значение в JSON как ссылки на те сущности, на которые также можно указывать при помощи URL.

В более общем виде этот аргумент формулируется как «нет фундаментальной разницы между атрибутами и взаимоотношениями». Атрибуты – это просто взаимоотношения между сущностью или другой абстрактной или конкретной сущностью, например числом или цветом. Но исторически сложилось, что к их обработке относились по-особенному. Признаться, это достаточно абстрактный вариант восприятия мира. Так, если вы покажете кому-либо черного кота и спросите, сколько тут объектов – большинство людей ответит вам, что всего один. Немногие скажут, что видят два объекта – кота и его черный цвет – а также взаимоотношение между ними.

Ссылки попросту лучше


Веб-API, передающие ключи базы данных, а не просто ссылки, сложнее в изучении, а также сложнее в использовании для клиентов. Также API первого рода теснее связывают клиент и сервер, требуя в качестве «общего знаменателя» более подробную информацию, и всю эту информацию нужно документировать и читать. Единственное преимущество API первого рода – в том, что они настолько распространены, программисты с ними освоились, знают, как их создавать и как потреблять. Если вы стремитесь предоставить клиентам высококачественные API, не требующие тонн документации и максимально обеспечивающие независимость клиента от сервера, то подумайте о том, чтобы предоставлять в ваших веб-API ссылки, а не ключи базы данных.
Источник: https://habr.com/ru/company/piter/blog/540640/


Интересные статьи

Интересные статьи

На CES 2021 стало ясно, что салонные системы ИИ становятся трендом в автомобильной индустрии. Такое положение дел серьезно повлияет на рынок систем мониторинга водителей – и Qualcomm ...
меня с детства не покидало подсознательное ощущение, что с миром что-то не так, что он не то, чем нам кажется на самом деле. когда я ребенком ложился спать где-то за городом, у меня в...
(Мы продолжаем цикл очерков из истории нашего университета под названием «Красный Хогвартс»). В НИТУ «МИСиС» сейчас очень активно отмечается 75-летие Великой Победы. Это будет первый большой ю...
Язык программирования Ада родился в середине 1970-х, когда министерство обороны США и министерство обороны Британии решили заменить сотни специализированных языков программирования для встрое...
Элизабет Холмс, подобно Бенджамину Франклину и Эдит Кларк, подвергла сомнению базовое допущение. Она задалась вопросом: действительно ли врачам и исследователям нужно брать для проведения анали...