Come progettare un endpoint HTTP che ricerca una singola istanza di risorsa e la restituisce con un'altra istanza connessa

3

Ho un endpoint che restituisce una raccolta di elementi singoli (non ho restituito solo l'istanza di oggetto per mantenere la coerenza con la convenzione resource-as-collection, quindi solo get-by-id restituisce una singola istanza)

GET /devices?serialNumber=12345
[
    {
        "id": 1, 
        "serialNumber": "12345"
    }
]

E il nuovo requisito è apparso: alcuni dispositivi sono collegati a coppie e mentre cercano il dispositivo 12345 che è accoppiato con il dispositivo 78901 Ho bisogno di recuperarli entrambi (preferibilmente in una singola chiamata HTTP). Quali sono le mie migliori opzioni? Ho provato con:

GET /devices?serialNumber=12345
[
    {
        "id": 1, 
        "serialNumber": "12345"
    },
     {
        "id": 2, 
        "serialNumber": "78901"
    }
]

Ma ciò interrompe la semantica (I filter devices rresoure "list" per un S / N e improvvisamente un altro dispositivo con diversi S / N si apre)

Poi ho provato questo:

GET /devices?serialNumber=12345
[
    {
        "id": 1, 
        "serialNumber": "12345"
        "connected": 
        {
            "id": 2, 
            "serialNumber": "78901"
        }
    }
]

Tuttavia, la risorsa annidata ricorsivamente non si sente corretta poiché è completamente diversa da come viene espresso il modello di dominio. C'è un modo migliore per progettare questo endpoint senza esporre i dettagli di come sono collegati? C'è una logica aziendale che è piuttosto complicata e irrilevante per il consumatore endpoint, lei ha solo bisogno di sapere se c'è un dispositivo associato o meno.

    
posta AndrzejD 23.01.2017 - 14:31
fonte

4 risposte

4

But still recursively-nested resource doesn't feel right since this is totally different from how the domain model is expressed.

Non c'è una buona ragione per accoppiare strongmente le rappresentazioni delle risorse API al modello di dominio. Servono clienti diversi con esigenze diverse e i loro paradigmi di design sono completamente diversi.

Suggerirei di aggiungere il supporto per un parametro di query ?includeConnectedDevices=true . Quindi puoi fare

GET /devices?serialNumber=12345
[
    {
        "id": 1, 
        "serialNumber": "12345"
    }
]

GET /devices?serialNumber=12345&includeConnectedDevices=true
[
    {
        "id": 1, 
        "serialNumber": "12345",
        "connected": 
        {
            "id": 2, 
            "serialNumber": "78901"
        }
    }
]

I clienti possono scegliere se preoccuparsi dei dispositivi collegati. Come bonus, sei in grado di un numero arbitrario di connessioni.

Qualunque cosa tu faccia, dovresti considerare di includere un link (o link) da un dispositivo al suo dispositivo (i) connesso (i). Potrebbe trovarsi in un'intestazione o in un attributo dell'entità o magari avvolgere le tue risposte in una busta. I seguenti collegamenti sono molto più belli degli URL di costruzione. Potrebbe sembrare /devices?connectedTo=12345 .

    
risposta data 23.01.2017 - 14:58
fonte
0

Puoi utilizzare HATEOAS per collegare le tue risorse, quindi invece di restituire questo:

GET /devices?serialNumber=12345
[
    {
        "id": 1, 
        "serialNumber": "12345"
    },
    {
        "id": 2, 
        "serialNumber": "78901"
    }
]

puoi restituire qualcosa del genere:

GET /devices?serialNumber=12345
    {
        "id": 1, 
        "serialNumber": "12345",
        "links":[
            {
               rel:"connected",
               "id":2,
               "serialNumber": "78901"
            }
        ]
    }

Aggiorna

@Ewan ha perfettamente ragione, dovrebbe essere qualcosa del tipo:

 GET /devices?serialNumber=12345
    {
        "id": 1, 
        "serialNumber": "12345",
        "links":[
            {
               "rel":"connected",
               "href":"http://yourserver.com/yourapp/devices?serialNumber=78901"
            }
        ]
    }

** SECOND UPDATE ** (dopo la mia edizione del caffè del mattino)

Se dai un'occhiata ai commenti vedrai che mentre la risposta originale è conforme a tutti i tuoi requisiti (io sono con @ Eric-Stein sul "questo non dovrebbe richiedere una singola richiesta HTTP") il primo aggiornamento è l'approccio "standard" (almeno uno di questi). IMHO si dovrebbe andare con HATEOAS o HAL, o qualche altra alternativa ben nota invece di fare la cosa nidificata delle risorse.

    
risposta data 23.01.2017 - 15:00
fonte
0

new requirement showed up: some devices are connected into pairs and while searching for device 12345 which is paired with device 78901 I need to retrieve both of those (preferably in single HTTP call). What are my best options?

Questo dipenderà dai tuoi vincoli.

Un approccio è molto semplice: riconosce che il nuovo requisito descrive una nuova vista del dispositivo 12345. Ciò significa che descrive un nuovo documento, che è un altro modo di dire che descrive una nuova risorsa.

Quindi cerca un nome per questo nuovo rapporto, quindi usa quel nome con gli standard di ortografia dell'URI per capire un approccio ragionevole.

GET /devicePairs?eitherDevice=12345 ...

Nota: Il suggerimento di Eric di aggiungere un altro parametro alla stringa di query è un esempio di questo approccio - inventare un nuovo URI per coprire il nuovo caso d'uso.

D'altra parte, se il nuovo requisito è che estendi la rappresentazione esistente per includere nuove informazioni quando disponibili, allora non hai bisogno di coniare un nuovo URI, ma dovresti estendere la rappresentazione esistente con i dati aggiuntivi.

Questo, a sua volta, dipende dall'avere un tipo di supporto che supporti le estensioni. La parte facile è mettere i dati; la parte più difficile è garantire che i client esistenti non si interrompano.

Il contratto base di cui hai bisogno è la nozione di campi opzionali nella tua rappresentazione, combinata con l'idea che i consumatori devono ignorare i campi che non riconoscono. La specifica di syndication di Atom è una buona implementazione di riferimento di questa idea.

Nel semplice vecchio JSON, questo potrebbe apparire come

{
    "id": 1, 
    "serialNumber": "12345",
    "connectedTo" : "78901"
}

Oppure potresti preferibilmente rendere esplicita la nozione di estensioni nella tua rappresentazione

{
    "id": 1, 
    "serialNumber": "12345",
     extensions : {
        "connectedTo" : "78901"
     }
}

Ci sono molti modi diversi per compitare questo, ma ciò che realmente equivale a garantire che le tue rappresentazioni siano retrocompatibili.

Se la tua soluzione utilizzava hypermedia, potresti fornire le estensioni con i link.

{
    "id": 1, 
    "serialNumber": "12345",
     connectedTo : {
        "serialNumber" : "78901",
        "href" : "/devices?serialNumber=78901"
     }
}   

I link sono adatti per i punti di estensione, quindi potresti invertirlo

{
    "id": 1, 
    "serialNumber": "12345",
     links : [
         {
             rel : "connectedTo",
             href : "/devices/78901"
             serialNumber : "78901"
         }
     ]
} 

Continua a dimenare la rappresentazione e alla fine finisci con qualcosa come HAL o API JSON

    
risposta data 24.01.2017 - 04:54
fonte
0

Molti approcci sono già stati descritti, ma credo che ci siano solo 2 opzioni ragionevoli (estremi) con tutto il resto come una mezza misura tra i due.

  1. Utilizzare un parametro di query che descrive un attributo comune "univoco" per entrambi i dispositivi e restituirli in un array. eitherSerialNumber sembra strano:

    GET /devices?eitherSerialNumber=12345
    [
        {
            "id": 1, 
            "serialNumber": "12345"
        },
         {
            "id": 2, 
            "serialNumber": "78901"
        }
    ]

  1. Utilizzare una busta completa con la risorsa principale e collegata e i collegamenti (opzionalmente in entrambi) appiattiti. Questo è ciò che JSON: API e alcune altre proposte standard fanno:

     GET /devices?serialNumber=12345
     {
         "data": {
             "id": 1, 
             "serialNumber": "12345",
             "related": {
                  "connectedDevice": {
                      "type": "device",
                      "id": 2
                  }
             }
         },
         included: [{
             "type": "device",
             "id": 2, 
             "serialNumber": "78901"
         }]
     }

La prima opzione è semplice e ancora semanticamente corretta. Il secondo infine risolve il problema della serializzazione dei grafici delle risorse introducendo alcuni overhead.

Mi sento di raccomandare di non scegliere una via di mezzo, perché potrebbe funzionare oggi, ma domani non sarà sufficiente e finirai per chiedere nuovamente una domanda di follow-up.

    
risposta data 24.01.2017 - 11:26
fonte

Leggi altre domande sui tag