Вложенные REST-URL и родительский идентификатор, какой дизайн лучше?

20

Хорошо, у нас есть два ресурса: Albumи Song. Вот API:

GET,POST /albums
GET,POST /albums/:albumId
GET,POST /albums/:albumId/songs
GET,POST /albums/:albumId/songs/:songId

Мы знаем, что мы ненавидим какую-то песню, это называется Susy, например. Куда мы должны положить searchдействие?

Другой вопрос. Хорошо, теперь это более реально. Открываем альбом 1 и загружаем все песни. Мы создаем объекты JS, каждый из которых содержит данные песни и имеет несколько методов, таких как: remove, update.

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

Итак, я вижу несколько решений, но я не совсем уверен.

  1. Сделать родительский идентификатор необязательным - в качестве get-параметра. Этот подход я использую в настоящее время, но я чувствую, что он уродлив.

    List,Create /songs?album=albumId
    Update,Delete /songs/:songId
    Get /songs/?name=susy # also, solution for first question
    
  2. Гибридный. Теперь это удобно, так как нам нужен идентификатор альбома для выполнения OPTIONSзапроса для получения метаданных.

    List,Create /album/:albumId/songs
    Update,Delete /songs/:songId
    POST /songs/search # also, solution for first question
    
  3. Вернуть полный URL с каждым экземпляром ресурса. API такой же, но мы получим такие песни:

    id: 5
    name: 'Elegy'
    url: /albums/2/songs/5
    

    Я слышал, что этот подход называется HATEOAS.

  4. Итак ... Чтобы предоставить родительский идентификатор

    id: 5
    name: 'Elegy'
    albumId: 2
    

Что лучше? А может я тупой? Киньте советы, ребята!

dt0xff
источник

Ответы:

31

Где мы должны поставить поисковое действие?

В GET /search/:text. Это вернет массив JSON, содержащий совпадения, каждое совпадение, содержащее альбом, которому он принадлежит. Это имеет смысл, потому что клиент может интересоваться не самим треком, а всем альбомом (представьте, что вы ищете песню, которая, как вы полагаете, была в том же альбоме, что и тот, который вы помните по имени).

будет не так хорошо возвращать родительские идентификаторы для каждого из них. Я ошибаюсь?

Отдельные треки могут содержать альбом. Это обеспечит равномерное представление трека, если вы можете получить трек либо через альбом, либо через поиск (здесь нет альбома).

Что лучше?

Как уже говорилось, в том числе альбом имеет смысл. Хотя третий пункт (с относительным URI) может быть интересен в некоторых случаях (вам не нужно думать о том, как должен формироваться URI), у него есть недостаток, заключающийся в том, что он не предоставляет явно альбом. Четвертый пункт исправляет это. Если вы видите преимущество наличия относительного URI в ответе, вы можете объединить пункты 3 и 4.

А может я тупой?

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

Аспект, который может быть проблематичным, заключается в том, как вы организуете данные внутренне, то есть используете иерархию. Из вашего комментария вам интересно, что должно содержать ответ GET /artist/1/album/10/song/3/comment/23, который показывает очень древовидное видение. Это может привести к нескольким проблемам при дальнейшем расширении системы. Например:

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

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

Что произойдет, если вы разрушите иерархию? Посмотрим.

  1. GET /albums/:albumIdвозвращает JSON-файл, содержащий метаинформацию об альбоме (например, год, когда он был опубликован, или URI в формате JPEG с обложкой альбома) и массив дорожек. Например:

    GET /albums/151
    {
        "id": 151,
        "gid": "dbd3cec7-b927-423f-894b-742c4c7b54ce",
        "name": "Yellow Submarine",
        "year": 1969,
        "genre": "Psychedelic rock",
        "artists": ["John Lennon", "Paul McCartney", ...],
        "tracks": [
            {
                "id": 90224,
                "title": "Yellow Submarine",
                "length": "2:40"
            },
            {
                "id": 83192,
                "title": "Only a Northern Song",
                "length": "3:24"
            }
            ...
        ]
    }

    Почему я включаю, например, длину каждого трека? Потому что я представляю, что клиент, показывающий альбом, может быть заинтересован, перечисляя треки по названию, но также показывая длину каждого трека - большинство клиентов это делают. С другой стороны, я не могу показывать композитора (ов) или исполнителя (исполнителей) для каждого трека, потому что я решаю, что эта информация не нужна на этом уровне. Очевидно, ваш выбор может быть другим.

  2. GET /tracks/:trackIdвозвращает информацию о конкретной дорожке. Поскольку иерархии больше нет, вам не нужно угадывать альбом или исполнителя: единственное, что вам действительно нужно знать, - это идентификатор самого трека.

    А может даже нет? Что делать, если вы можете указать его по имени с GET /tracks/:trackName?

    GET /tracks/Only%20a%20Northern%20Song
    {
        "id": 83192,
        "gid": "8d9c4311-9d7b-40a4-8aeb-4fe96247fe2b",
        "title": "Only a Northern Song",
        "writers": ["George Harrison"],
        "artists": ["John Lennon", "Paul McCartney", "Ringo Starr"],
        "length": "3:24",
        "record-date": 1967,
        "albums": [151, 164],
        "soundtrack": {
            "uri": "http://audio.example.com/tracks/static/83192.mp3",
            "alias": "Beatles - Only a Northern Song.mp3",
            "length-bytes": 3524667,
            "allow-streaming": true,
            "allow-download": false
        }
    }

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

  3. GET /comments/:objectGid, Возможно, вы обнаружили уродливые GUID в ответах. Эти GUID позволяют идентифицировать сущность в базе данных для выполнения задач, которые могут быть применены к альбомам, исполнителям или дорожкам. Такие как комментирование.

    GET /comments/8d9c4311-9d7b-40a4-8aeb-4fe96247fe2b
    [
        {
            "author": {
                "id": 509931,
                "display-name": "Arseni Mourzenko"
            },
            "text": "What a great song! (And I'm proud of the usefulness of my comment)",
            "concerned-object": "/tracks/83192"
        }
    ]

    Комментарий ссылается на соответствующий объект, делая возможным переход к нему при доступе к комментарию вне его контекста (например, при модерации последних комментариев через GET /comments/latest).

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

  • Если ресурс не имеет смысла вне контекста его родительского ресурса, используйте иерархию.

  • Если ресурс может жить (1) один или (2) в контексте родительских ресурсов разных типов или (3) иметь несколько родителей, иерархия не должна использоваться.

Например, строки файла не имеют смысла вне контекста файла, поэтому:

GET /file/:fileId

и:

GET /file/:fileId/line/:lineIndex

в порядке.

Арсений Мурзенко
источник
Вы, из поиска, я могу также вернуть полную информацию об альбоме, это сделает другой ресурс - SongSearchResultэто нормально, я полагаю. Но как насчет URL? Должен ли я предоставить parentIDкаждый объект и использовать его в качестве GET-параметра или нормальной части URL? Что если у меня глубина> 2? /artist/1/album/10/song/3/comment/23- безумно указывать каждый идентификатор исполнителя, альбома и песни на commentобъекте, но я слышал, что это путь, но разве это не ужасно ?!
dt0xff
@ dt0xff: я отредактировал свой ответ. Я думаю, что теперь это должно дать вам четкое представление о том, как можно избежать глубины.
Арсений Мурзенко
Да, теперь ясно, что гораздо проще реализовать точку входа для каждого ресурса (кроме некоторого типа линии или другой функциональной вещи) без добавления его в parent по URL. Спасибо, вы убедили меня в том, что мой выбор был верным, и «общий подход» (на самом деле, многие вложенные вещи… restangularоснованы на нем) не так уж хорош.
dt0xff
Отличный ответ. У меня есть несколько возражений, хотя. «Что, если в альбоме есть несколько исполнителей?» Различные URI могут идентифицировать один и тот же ресурс, так как двоичное отношение URI -> resource уникально по праву (многие-к-одному). Так что URI /artists/foo/albums/quxи /artists/bar/albums/quxмогут прекрасно идентифицировать тот же ресурс альбома. Другими словами, компонент пути в URI представляет иерархию графа , не обязательно древовидную иерархию, что делает его подходящим для представления не только категорий, но и тегов.
Маджеро
1
… «Это, по сути, проблема, которую я объяснил в своем блоге : представление дерева имеет слишком много ограничений, чтобы эффективно использоваться во многих случаях». Так что нет, это не проблема. «Что если вы хотите добавить функцию, которая позволяет комментировать альбомы?» Это тоже не проблема /artists/foo/albums/qux/comments/7. «Что делать, если должны быть комментарии комментариев?» Точно так же: /artists/foo/albums/qux/song/5/comments/2/comments/8.
Маджеро