Привет, Хабр! Недавно, пришлось работать на проекте с внешним API. Работал, я, к слову, всегда либо с простым REST, либо с GET/POST only запросами, но в этом нужно было работать с API Timetta. Он использует OData и что же это такое?
Содержание
REST vs OData
В то время как REST — набор архитектурных правил создания хорошего API, OData — это уже веб-протокол, собравший в себя «лучшие архитектурные практики»: defines a set of best practices for building and consuming RESTful APIs (как написано на официальном сайте). Сам протокол очень большой, поэтому я затрону наиболее практически-значимые аспекты.
Схема
Каждая система использующая OData должна описать свою схему данных. По ней можно понять все: какие сущности есть в системе, какие операции над ними можно производить. Схема может описывается в формате XML или JSON. Для получения схемы нужно сделать запрос по адресу:
<root>/$metadata
Где <root> — корень сервиса OData. Примеры дальше будут предполагать, что мы делаем запросы из этого <root>. Для Timetta этот адрес такой:
https://api.timetta.com/odata/$metadata
Примеры дальше будут с использованием XML схем.
Типы данных
Примитивные
Протокол определяет ряд встроенных типов данных. Все имеют префикс «Edm». Например:
-
Edm.Boolean
-
Edm.String
-
Edm.Int32
-
Edm.Int16
-
Edm.Stream
-
Edm.Date
-
Edm.Byte
-
Edm.Decimal
-
Edm.Binary
EntityType
EntityType похож на сущность из DDD: у него есть как состояние, так и свой ID (в схеме указывается отдельно). В схеме состоит из элементов
-
Property — поля со скалярными данными. Например, строка или число. Имеет атрибуты:
-
Name — название поля (обязателен)
-
Type — тип поля (обязателен)
-
Nullable — может ли быть null
-
-
NavigationProperty — поле, которое ссылается на другую сущность
-
Name — название (обязателен)
-
Type — тип (обязателен)
-
ReferentialConstraint — «как» мы ссылаемся
-
Property — название поля в ССЫЛАЮЩЕМСЯ типе
-
ReferencedProperty — название поля в типе, на который ССЫЛАЕМСЯ
-
-
-
Key — элемент, определяющий первичный ключ сущности. Значения могут быть только примитивными типами или перечислением и не может равняться null.
-
Name — название поля, которое является ключом
-
Alias — псеводним для ключа. Например, если ключ — поле вложенного типа.
-
Пример, для сущности TimeSheet, представляющей рабочий график проекта (табель) за некоторый период времени.
<EntityType Name="TimeSheet" OpenType="true"> <Key> <PropertyRef Name="id"/> </Key> <Property Name="dueDate" Type="Edm.Date"/> <Property Name="dateFrom" Type="Edm.Date" Nullable="false"/> <Property Name="dateTo" Type="Edm.Date" Nullable="false"/> <Property Name="approvalStatusId" Type="Edm.Guid"/> <Property Name="submitted" Type="Edm.DateTimeOffset"/> <Property Name="approved" Type="Edm.DateTimeOffset"/> <Property Name="userId" Type="Edm.Guid"/> <Property Name="departmentId" Type="Edm.Guid"/> <Property Name="approvalInstanceId" Type="Edm.Guid"/> <Property Name="templateId" Type="Edm.Guid" Nullable="false"/> <Property Name="name" Type="Edm.String"/> <Property Name="rowVersion" Type="Edm.Binary"/> <Property Name="createdById" Type="Edm.Guid"/> <Property Name="modifiedById" Type="Edm.Guid"/> <Property Name="id" Type="Edm.Guid" Nullable="false"/> <Property Name="created" Type="Edm.DateTimeOffset"/> <Property Name="modified" Type="Edm.DateTimeOffset"/> <Property Name="isActive" Type="Edm.Boolean" Nullable="false"/> <NavigationProperty Name="approvalStatus" Type="WP.ApprovalStatus"> <ReferentialConstraint Property="approvalStatusId" ReferencedProperty="id"/> </NavigationProperty> <NavigationProperty Name="user" Type="WP.User"> <ReferentialConstraint Property="userId" ReferencedProperty="id"/> </NavigationProperty> <NavigationProperty Name="department" Type="WP.Department"> <ReferentialConstraint Property="departmentId" ReferencedProperty="id"/> </NavigationProperty> <NavigationProperty Name="approvalInstance" Type="WP.ApprovalInstance"> <ReferentialConstraint Property="approvalInstanceId" ReferencedProperty="id"/> </NavigationProperty> <NavigationProperty Name="timeSheetLines" Type="Collection(WP.TimeSheetLine)"/> <NavigationProperty Name="timeAllocations" Type="Collection(WP.TimeAllocation)"/> <NavigationProperty Name="approvalRecords" Type="Collection(WP.TimeSheetApprovalRecord)"/> <NavigationProperty Name="lineApprovals" Type="Collection(WP.TimeSheetLineApproval)"/> <NavigationProperty Name="template" Type="WP.TimeSheetTemplate"/> <NavigationProperty Name="total" Type="WP.TimeSheetTotal"/> <NavigationProperty Name="timeOffRequests" Type="Collection(WP.TimeOffRequest)"/> <NavigationProperty Name="createdBy" Type="WP.User"> <ReferentialConstraint Property="userId" ReferencedProperty="id"/> </NavigationProperty> <NavigationProperty Name="modifiedBy" Type="WP.User"> <ReferentialConstraint Property="userId" ReferencedProperty="id"/> </NavigationProperty> </EntityType>
Объяснение:
-
Поле «id» — первичый ключ
-
Имеет несколько полей с примитивными типами. Например:
-
dueDate — до какой даты нужно закончить
-
dateFrom — когда начинать проект
-
dateTo — когда оканчивать проект
-
approvalInstanceId — Id документа (ресурса), который сигнализирует о том, что работа согласована (если не согласован, то значение — null, нет атрибута Nullable, что по умолчанию означает поддержку null)
Некоторые из которых могут принимать значение null:
-
dueDate — мы еще не знаем дедлайн
-
approvalInstanceId — работу еще могли не согласовать
-
-
Имеет навигационные свойства. Например:
-
approvalInstance — документ согласования
-
timeSheetLines — потраченные часы
-
createdBy — кем создан план работ
Где:
-
approvalInstance можно найти по занчению поля approvalInstanceId родителя и сопоставление по полю id искомой сущности
-
timeSheetLines — «слабая» сущность. Для нее не нужен Id, время жизни привязано к самому родителю
-
ComplexType
ComplexType похож на Value type из DDD — не имеет первичного ключа, сравнение по значению полей. Может состоять из тех же элементов, что и EntityType за исключением Key.
Пример, для объекта с 2 полями, который используется для отметок времени
<ComplexType Name="DateHours"> <Property Name="date" Type="Edm.Date" Nullable="false"/> <Property Name="hours" Type="Edm.Decimal"/> </ComplexType>
Объяснение:
-
Тип DateHours является сложным (ComplexType)
-
Состоит из 2 полей:
-
date — дата (Type=»Edm.Date»), за которую идет отсчет.
-
hours — время, представляемое дробным числом (Type=»Edm.Decimal»)
-
-
Где:
-
date — не может быть null, т.к. мы должны знать дату
-
hours — может быть null, т.к. время может быть просто не проставлено/указано
-
EnumType
EnumType — обычный тип перечисления. Особенность в том, как передается значение в параметры — сначала пишется полное имя Enum, затем в кавычках его значение. Для данного примера, чтобы передать PlanningMethod.Manual, нужно написать PlanningMethod'Manual'. Атрибуты:
-
Name — название перечисления (обязательно)
-
UnderlyingType — тип, которое определяет используемое значение (по умолчанию, Edm.Int32)
-
IsFlags — является ли перечисление флагом. Чтобы исползовать в UnderlyingType должно быть проставлено «true»
Для определения занчений — элемент Member:
-
Name — название элемента
-
Value — значение элемента
<EnumType Name="PlanningMethod"> <Member Name="Manual" Value="0"/> <Member Name="FrontLoad" Value="1"/> <Member Name="RemainingCapacity" Value="2"/> <Member Name="Evenly" Value="3"/> </EnumType>
Объяснение:
-
Тип PlanningMethod является типом перечисления (EnumType)
-
Может принимать значения:
-
Manual, определяемое значением 0
-
FrontLoad, определяемое значением 1
-
RemainingCapacity, определяемое значением 2
-
Evenly, определяемое значением 3
-
P.S. В общем случае такой вид записи применяется ко всем типам и имеет вид: ПолныйТипСущности’Значение’. Например, для даты — date’2022-07-01′
Collection
Тип коллекции является отдельным. Определяется как Collection(ПолноеНазваниеТипа). Например, Collection(WP.TimeSheet).
<EntityType Name="TimeSheet" OpenType="true"> <Key> <PropertyRef Name="id"/> </Key> <Property Name="dueDate" Type="Edm.Date"/> <Property Name="dateFrom" Type="Edm.Date" Nullable="false"/> <Property Name="dateTo" Type="Edm.Date" Nullable="false"/> <Property Name="approvalStatusId" Type="Edm.Guid"/> <Property Name="submitted" Type="Edm.DateTimeOffset"/> <Property Name="approved" Type="Edm.DateTimeOffset"/> <Property Name="userId" Type="Edm.Guid"/> <Property Name="departmentId" Type="Edm.Guid"/> <Property Name="approvalInstanceId" Type="Edm.Guid"/> <Property Name="templateId" Type="Edm.Guid" Nullable="false"/> <Property Name="name" Type="Edm.String"/> <Property Name="rowVersion" Type="Edm.Binary"/> <Property Name="createdById" Type="Edm.Guid"/> <Property Name="modifiedById" Type="Edm.Guid"/> <Property Name="id" Type="Edm.Guid" Nullable="false"/> <Property Name="created" Type="Edm.DateTimeOffset"/> <Property Name="modified" Type="Edm.DateTimeOffset"/> <Property Name="isActive" Type="Edm.Boolean" Nullable="false"/> <NavigationProperty Name="approvalStatus" Type="WP.ApprovalStatus"> <ReferentialConstraint Property="approvalStatusId" ReferencedProperty="id"/> </NavigationProperty> <NavigationProperty Name="user" Type="WP.User"> <ReferentialConstraint Property="userId" ReferencedProperty="id"/> </NavigationProperty> <NavigationProperty Name="department" Type="WP.Department"> <ReferentialConstraint Property="departmentId" ReferencedProperty="id"/> </NavigationProperty> <NavigationProperty Name="approvalInstance" Type="WP.ApprovalInstance"> <ReferentialConstraint Property="approvalInstanceId" ReferencedProperty="id"/> </NavigationProperty> <NavigationProperty Name="timeSheetLines" Type="Collection(WP.TimeSheetLine)"/> <NavigationProperty Name="timeAllocations" Type="Collection(WP.TimeAllocation)"/> <NavigationProperty Name="approvalRecords" Type="Collection(WP.TimeSheetApprovalRecord)"/> <NavigationProperty Name="lineApprovals" Type="Collection(WP.TimeSheetLineApproval)"/> <NavigationProperty Name="template" Type="WP.TimeSheetTemplate"/> <NavigationProperty Name="total" Type="WP.TimeSheetTotal"/> <NavigationProperty Name="timeOffRequests" Type="Collection(WP.TimeOffRequest)"/> <NavigationProperty Name="createdBy" Type="WP.User"> <ReferentialConstraint Property="userId" ReferencedProperty="id"/> </NavigationProperty> <NavigationProperty Name="modifiedBy" Type="WP.User"> <ReferentialConstraint Property="userId" ReferencedProperty="id"/> </NavigationProperty> </EntityType>
Возвращаясь к типу TimeSheet. В нем присутствуют 5 коллекций*:
-
timeSheetLines
-
timeAllocations
-
lineApprovals
-
approvalRecors
-
timeOffRequests
*Тип коллекции может быть и у Property, не только NavigationProperty
EntitySet
У нас есть сущности, сложные типы, перечисления и т.д. Но где это хранить? Для этого нам нужна коллекция.EntitySet — это top-level коллекция доступная всем. Внутри нее хранятся какие-либо сущности. Соответственно, для нее обязательны имя и тип: Name=»SampleName» EntityType=»SampleType», соответственно. По умолчанию, все обычные поля (Property) коллекции включаются в вывод. Если у типа есть навигационные свойства, то их должны указать в EntitySet, иначе их существование не гарантируется.
<EntitySet Name="TimeSheets" EntityType="WP.TimeSheet"> <NavigationPropertyBinding Path="approvalStatus" Target="ApprovalStatuses"/> <NavigationPropertyBinding Path="createdBy" Target="Users"/> <NavigationPropertyBinding Path="department" Target="Departments"/> <NavigationPropertyBinding Path="modifiedBy" Target="Users"/> <NavigationPropertyBinding Path="template" Target="TimesheetTemplates"/> <NavigationPropertyBinding Path="timeAllocations" Target="TimeAllocations"/> <NavigationPropertyBinding Path="timeOffRequests" Target="TimeOffRequests"/> <NavigationPropertyBinding Path="timeSheetLines" Target="TimeSheetLines"/> <NavigationPropertyBinding Path="user" Target="Users"/> </EntitySet>
Здесь:
-
Name=»TimeSheets» — название коллекции. Доступ к ней через это слово:
https://app.timetta.com/TimeSheets -
EntityType=»WP.TimeSheet» — тип сущностей используемый в коллекции (был выше). WP — пространство имен.
-
У коллекции есть несколько навигационных свойств. Свойства — те же, что и у самой сущности. Например:
-
user
-
template
-
modifiedBy
Но некоторые (total, lineApprovements) отсутствуют.
-
Есть коллекция, значит есть и единственный элемент. Для получения нужно использовать ID сущности, который указан в схеме. Указывать в круглых скобках после названии коллекции. Например, для получения TimeSheet то его ID нужно сделать запрос:
/TimeSheets(00000000-0000-0000-0000-000000000000)
Запросы
На мой взгляд, что примечательного в OData — язык запросов. Можно делать запросы к ресурсам прямо в строке запроса. Для запросов есть свой специальный язык. По фукнционалу он очень похож на SQL.
Сам запрос передается в строке запросов (query string) URL. Например:
/TimeSheets?$select=id,dateFrom,dateTo&$filter=approval&$expand=createdBy($select=name)&$count=true
Метаданные
Вместе с каждым запросом возвращаются специальные поля с метаданными. Они могут быть полезны, например, для получения мета информации о запрашиваемой сущности. Дальше на них акцент делаться не будет.
Поэтому, многие запросы OData транслируются в SQL. У этого языка много ключевых слов, но рассмотрим основные. Для лучшего погружения представим ситуацию: нам нужно получить отчеты.
$select
$select позволяет нам выбрать только нужные поля.
Пример: нам нужно получить только отрезки времени отсчета таймшитов и имя того, кто его создал. Тогда следующий запрос:
/TimeSheets?$select=dateFrom,dateTo,id
{ "@odata.context": "https://api.timetta.com/odata/$metadata#TimeSheets(dateFrom,dateTo,id)", "value": [ { "dateFrom": "2021-10-04", "dateTo": "2021-10-10", "id": "00000000-0000-0000-0000-00000000" }, { "dateFrom": "2021-11-22", "dateTo": "2021-11-28", "id": "00000000-0000-0000-0000-00000000" }, { "dateFrom": "2022-06-27", "dateTo": "2022-07-03", "id": "00000000-0000-0000-0000-00000000" } }
$filter
Прекрасно. Мы получили данные. Но что если какие-то табеля не были согласованы? Нужно убрать такие:
/TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null
{ "@odata.context": "https://api.timetta.com/odata/$metadata#TimeSheets(dateFrom,dateTo,id)", "value": [ { "dateFrom": "2021-10-04", "dateTo": "2021-10-10", "id": "00000000-0000-0000-0000-00000000" }, { "dateFrom": "2021-11-22", "dateTo": "2021-11-28", "id": "00000000-0000-0000-0000-00000000" } }
Какой-то табель не был согласован! Хорошо, что мы обнаружили это.
Сам $filter принимает в себя логическое выражение. Доступные логические операции (bash-like):
-
eq (equal), ==:
name eq 'За ноябрь' -
ne (not equal), !=:
approvalInstanceId ne null -
gt (greater than), >:
dueDate gt '2022-03-15' -
ge (greater or equal), >=:
dateFrom ge '2021-01-01' -
lt (less than), <:
age lt 18 -
le (less or equal), <=:
ttl le 0
Эти выражения можно комбинировать с помощью скобок и:
-
and (логическое ‘и’):
(name eq 'За ноябрь') and (dateFrom ge '2021-01-01') -
or (логическое отрицание): (
dateFrom ge '2021-01-01') or (approvalInstanceId ne null) -
not (отрицание):
not isActive
Также есть и булевы литералы: true, false
$expand
Прекрасно. Мы отфильтровали данные и выбрали только те поля, которые нужны. Но теперь в отчете нужно получить и имя человека создавшего табель. Можно сделать несколько запросов: сначала массив табелей, затем для каждого — запрос на информацию о пользователе. Но это лишнее. Можно ведь сделать просто — добавить $expand:
/TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null&$expand=user($select=name)
{ "@odata.context": "https://api.timetta.com/odata/$metadata#TimeSheets(dateFrom,dateTo,id)", "value": [ { "dateFrom": "2021-10-04", "dateTo": "2021-10-10", "id": "00000000-0000-0000-0000-00000000", "user": { "name": "Владислав Иванов" } }, { "dateFrom": "2021-11-22", "dateTo": "2021-11-28", "id": "00000000-0000-0000-0000-00000000", "user": { "name": "Кирилл Ильин" } } }
Здесь продемонстрированы сразу 2 фичи:
-
Возможность вставить сущность из навигационного свойства (у типа TimeSheet есть навигационное свойство user типа WP.User)
-
Сделать подзапросы: здесь из всех возможных свойств нам нужно только имя. Для подзапросов есть ограничения — ключевые слова разделяются точкой с запятой, иначе парсинг строки запроса будет неверным. Например:
/TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null&$expand=user($select=name;$expand=schedules) — дает нам не только имя пользователя, но и список его рабочих графиков. Если бы вместо точки с запятой был амперсанд, то получилось бы 2 параметра: $expand=user($select=name и $expand=schedules)
$orderby
Вроде бы все прекрасно. Данные получаются, но мы хотим получать упорядоченные данные сразу. Нам поможет ключевое слово $orderby. Представим, что мы хотим сортировать табели сначала по дате начала по убыванию, а затем по дате конца по возрастанию (получим самые близкие к нам по дате, короткие по длительности):
/TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null&$expand=user($select=name)&$orderby=dateFrom desc, dateTo asc
{ "@odata.context": "https://api.timetta.com/odata/$metadata#TimeSheets(dateFrom,dateTo,id,user(name)) "value": [ { "dateFrom": "2021-11-22", "dateTo": "2021-11-28", "id": "00000000-0000-0000-0000-00000000", "user": { "name": "Кирилл Ильин" } }, { "dateFrom": "2021-10-04", "dateTo": "2021-10-10", "id": "00000000-0000-0000-0000-00000000", "user": { "name": "Владислав Иванов" } } }
По умолчанию используется сортировка по возрастанию — asc
$top, $skip
Вот проблема! У нас слишком много данных. Как бы нам ограничить их прием? Для этого можно указать какое количество мы хотим принять с помощью $top. Тогда нам вернется не больше заданного количества:
/TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null&$expand=user($select=name)&$orderby=dateFrom desc, dateTo asc$top=5
А что если нужно еще и пропускать некоторое количество (для пагинации, например)? Тогда используем $skip:
/TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null&$expand=user($select=name)&$orderby=dateFrom desc, dateTo asc&$top=5&$skip=10
$count
Мы можем получить наши данные — прекрасно. А если я хочу знать только количество удовлетворяющих критерию? Или для пагинации? Нужно посчитать общее количество элементов. Здесь нам поможет $count. Он принимает булево значение: true — вернуть общее количество, false — не возвращать (по умолчанию)
/TimeSheets?$select=dateFrom,dateTo,id&$filter=approvalInstanceId ne null&$expand=user($select=name)&$orderby=dateFrom desc, dateTo asc&$count=true
{ "@odata.context": "https://api.timetta.com/odata/$metadata#TimeSheets(dateFrom,dateTo,id,user(name)) "@odata.count": 2, "value": [ { "dateFrom": "2021-11-22", "dateTo": "2021-11-28", "id": "00000000-0000-0000-0000-00000000", "user": { "name": "Кирилл Ильин" } }, { "dateFrom": "2021-10-04", "dateTo": "2021-10-10", "id": "00000000-0000-0000-0000-00000000", "user": { "name": "Владислав Иванов" } } }
Функции, действия
Вот здесь начинается самое интересное — функции и действия (Functions, Actions). Теперь мы хотим получить табель за текущий период. Можно получить все табеля и отфильтровать их — сделать сложный запрос, а потом отфильтровать еще и результат. Это лишнее. Не проще ли использовать функцию:
/TimeSheets/Current
И все!
OData определяет функции (Function) и действия (Action)
Function — это операция над ресурсами, которая обязательно возвращает значение и не имеет сторонних эффектов.
Action — это операция, которая может изменить значение
PS. похоже на CQRS
Как же ими пользоваться? Для начала: операции могут быть привязанными и нет — требуется ли сущность для выполнения операции. Описание операции состоит из
Функция из примера имеет следующее пределение:
<Function Name="Current" IsBound="true"> <Parameter Name="bindingParameter" Type="Collection(WP.TimeSheet)"/> <ReturnType Type="WP.TimeSheet"/> </Function>
Что это все значит:
-
Name=»Current» — название функции, которую будем вызывать
-
IsBound=»true» — функция привязана к конкретному типу. Т.е. вызвать ее из произвольного места нельзя
-
«bindingParameter» — особый параметр означающий к какому типу функция применяется. Здесь применяется к типу
Collection(WP.TimeSheet)(чем и является/TimeSheets) -
Возвращает тип
TimeSheet
Если функция не принимает ничего, то скобок нет. Видели. А если функция принимает параметры? Тогда они указываются в элементе Parameter:
<Function Name="GetUserSchedule" IsBound="true"> <Parameter Name="bindingParameter" Type="Collection(WP.Schedule)"/> <Parameter Name="userId" Type="Edm.Guid" Nullable="false"/> <Parameter Name="from" Type="Edm.Date" Nullable="false"/> <Parameter Name="to" Type="Edm.Date" Nullable="false"/> <ReturnType Type="Collection(WP.DateHours)"/> </Function>
Эта функция принимает дополнительные параметры: userId, from, to (в данном случае они обязательные — Nullable=»false»). Передача параметров — как вызов функции, в скобках, причем все параметры именованные и вставляются через запятую (как в Python). Пример:
/Schedules/GetUserSchedule(userId=00000000-0000-0000-0000-00000000,from=2022-01-01,to=2022-02-01) — получение расписания для пользователя за весь январь
Что по Action? Разница в том, что действие может модифицировать данные и может не возвращать данные. Например:
<Action Name="SetAsDefault" IsBound="true"> <Parameter Name="bindingParameter" Type="WP.Role"/> </Action>
Это действие устанавливает тип роли пользователя применяемой по умолчанию. Логично, что оно может модифицировать, а может и нет (если это тип уже был по умолчанию). Также он ничего не возвращает (разве что статусный код по которму и будет понятен результат). Как вызывать:
/Roles(00000000-0000-0000-0000-00000000)/SetAsDefault
Это привязанное действие и привязано оно к типу WP.Role, а значит к единственному элементу, а не к целой коллекции как было в предыдущем примере.
Пример действия, который что-то возвращает:
<Action Name="UpdatePermissionSets" IsBound="true"> <Parameter Name="bindingParameter" Type="WP.User"/> <Parameter Name="permissionSets" Type="Collection(WP.UserPermissionSet)"/> <ReturnType Type="Collection(WP.UserPermissionSet)"/> </Action>
Модификация ресурсов
Для операций модификаций ресурсов используются HTTP методы: POST, PATCH, PUT, DELETE
Создание
Создание сущности = добавление в коллекцию. Для этого нужно сделать POST запрос с адресом коллекции и передать необходимые для создания параметры. Например, для создания нового департамента нужно сделать такой POST запрос:
POST /Departments
{ "code": "69", "resourcePoolId": null, "name": "Какой-то департамент", "leadDepartmentId": null }
И в ответ получим:
{ "@odata.context": "https://api.timetta.com/odata/$metadata#Departments/$entity", "code": "69", "resourceType": "Department", "resourcePoolId": null, "name": "Департамент", "rowVersion": "AAAAAAACG60=", "createdById": "00000000-0000-0000-0000-000000000000", "modifiedById": "00000000-0000-0000-0000-000000000000", "id": "11111111-1111-1111-1111-111111111111", "created": "2022-07-22T20:24:00.9318599+03:00", "modified": "2022-07-22T17:24:00.8776997Z", "isActive": true, "leadDepartmentId": null }
Обновление
Для обновления используются 2 HTTP метода: PUT, PATCH (последний предпочтительнее). Если бы мы хотели обновить название только что созданного департамента на «Лучший департамент», то сделали такой запрос:
PATCH /Departments(11111111-1111-1111-1111-111111111111)
{ "name": "Лучший департамент" }
И в ответ — 204 No Content
При повторном запросе:
{ "@odata.context": "https://api.timetta.com/odata/$metadata#Departments/$entity", "code": "69", "resourceType": "Department", "resourcePoolId": null, "name": "Лучший департамент", "rowVersion": "AAAAAAACG7A=", "createdById": "00000000-0000-0000-0000-000000000000", "modifiedById": "00000000-0000-0000-0000-000000000000", "id": "11111111-1111-1111-1111-111111111111", "created": "2022-07-22T20:24:00.9318599+03:00", "modified": "2022-07-24T05:21:09.764289Z", "isActive": true, "leadDepartmentId": null, "editAllowed": true, "deleteAllowed": true, "rolesEditAllowed": true }
Название действительно изменилось. Также изменилось и поле «rowVersion» — для предотвращение параллельного обновления.
Но говорилось еще и о PUT. При использовании PUT нам нужно передавать ВСЮ сущность. Даже те поля, которые не обновляются (за исключением тех над которыми не имеем власти, например, rowVersion или modified). Тоже самое обновление, но с помощью PUT:
PUT /Departments(11111111-1111-1111-1111-111111111111)
{ "code": "69", "resourceType": "Department", "resourcePoolId": null, "name": "Лучший департамент", "id": "11111111-1111-1111-1111-111111111111", "isActive": true, "leadDepartmentId": null, "editAllowed": true, "deleteAllowed": true, "rolesEditAllowed": true }
Удаление
И для удаления остался последний метод — DELETE. Удаление через ID сущности. Удалим же наш департамент:
DELETE /Departments(11111111-1111-1111-1111-111111111111)
В ответ получим — 204 No Content. И при обращении по этому же ID получаем Not Found.
Подытожим
OData — мощный веб-фреймворк, ядром которого является управление ресурсами.
Для функционирования использует возможности HTTP: HTTP методы, HTTP заголовки, строки запроса, URL.
Сервис, использующий OData, определяет свою схему. По ней можно понять какую функциональность данный сервис предоставляет:
-
Типы данных:
-
Перечисления, Enum
-
Сложные типы, ComplexType
-
Сущности, EntityType
-
-
Их атрибуты
-
Свойства (и свойства свойств (Nullability, MaxLength)
-
Навигационные свойства
-
Специфичные для данного типа атрибуты: ключ для сущности, значения для перечисления
-
-
Коллекции: их название, доступ, содержащийся в них тип данных
-
Предопределенные функции и действия (Function, Action)
Сам протокол определяет SQL подобный язык запросов (передается в строке запроса) и позволяет управлять получаемым контентом с помощью ключевых слов:
-
$select — в выводе только указанные поля. (~ SELECT)
-
$filter — вывести ресурсы, удовлетворяющие предикату (~ WHERE)
-
$expand — включить другие ресурсы, на который ссылается (~JOIN)
-
$orderby — сортировка по полю. (~ORDER BY)
-
$top — ограничить вывод максимальным количеством. (~TAKE)
-
$skip — пропустить какое-то количество. (~SKIP, OFFSET)
-
$count — дополнительно подсчитать общее число (или удовлетворяющих предикату) сущностей. (~COUNT)
Напоследок
OData очень большой фреймворк. Одна статья не может покрыть его полностью. Но целью этого поста стало простое ознакомление с наиболее используемой (по мнению автора) функциональностью. Многие темы не были покрыты, как например ключевое слово запроса $compute или лямбда операций any/all. Если вы хотите исследовать эту тему дальше, то вот некоторый список ссылок от куда можно стартовать:
-
https://www.odata.org — сайт по OData. Здесь можно найти спецификацию, туториалы для новичков и продвинутых, полезные инструменты и так далее.
-
https://docs.microsoft.com/en-us/odata — раздел об OData созданный Microsoft. Здесь много туториалов как по самому OData, так и по инструментам связанным с ним. Много уделяется фреймворку Microsoft.OData.
-
https://services.odata.org/ — сервис, для обучения/тестирования/просто попробовать OData. Как пользоваться для Read-Only Service:
-
Устанавливаю базовый URL
https://services.odata.org/V3/OData/OData.svc/ -
Мне удобнее получать результат в виде JSON. Поэтому выставляю заголовок
Accept: application/json -
Получаю схему OData:
https://services.odata.org/V3/OData/OData.svc/$metadata -
Изучаю полученную схему и делаю запросы.
-
Для получения списка всех продуктов по схеме нужно делаю такой запрос:
https://services.odata.org/V3/OData/OData.svc/ProductsRead-Only Northwind пользоваться по аналогии
-
Если нужно потренироваться и в модификации, то есть Read-Write. Для этого нужен ваш секретный ключ. Получить его можно, перейдя по ссылке Browse the Full Access (Read-Write) Service на указанном сайте. В браузере ваша строка заменится на строку с ключом. Либо, эту строка указывается в полученной схеме. Сам вид строки: https://services.odata.org/V3/(S(<секретный ключ>))/OData/OData.svc. А в запросах на создание (POST) нужно также указывать тип ресурса, который хотим создать в поле «odata.type» (там используется наследование, про которое не было рассказано). Например, для создания нового Product сделать вот такой запрос:
POST https://services.odata.org/V3/(S(j5lmqrfbgk1st4mmgrva1jtg))/OData/OData.svc/Products
{ "odata.type" :"ODataDemo.Product", "Name": "Cottage cheese", "ID": 11, "Description": "Best cottage cheese", "ReleaseDate": "2021-12-31T23:59:59", "DiscontinuedDate": "2022-01-01T00:00:00", "Rating": 5, "Price": 123 }
Полезные инструменты:
-
Расширение VS Code для работы с OData.
-
Веб-приложение для визуализации и исследования OData, по ее схеме. Использование: в верхней части в поисковике выберите вариант ‘Metadata URL’. Вбейте в поисковик URL и нажмите ‘Get Details’.

Триггером написания статьи было, то что автор не нашел «краш курсов» по OData и пришлось собирать знания по кусочкам. Если статья дала вам хороший старт, то значит проделанное было не зря.
ссылка на оригинал статьи https://habr.com/ru/post/678614/
Добавить комментарий