In che modo CQS si integra con gli inserimenti del database?

4

Ho cercato per settimane una risposta a questa domanda, che sembra essere comune a quasi tutte le applicazioni e quindi un problema in prima linea in CQS.

CQS indica che una funzione dovrebbe:

  • Cambia stato e restituisci void (comando) o
  • Non modificare nulla e restituire un risultato (query)

Quindi, supponiamo di voler creare una nuova entità di dati, mantenerla nel database, quindi emettere una notifica dell'ID auto-incrementato.

Per prima cosa, creiamo l'oggetto - a questo punto non ha un identificatore univoco. Quindi lo salviamo nel database, che genera un identificatore univoco tramite il campo ID auto-incrementato. Il risultato dell'esecuzione della query viene restituito all'applicazione, restituendo la riga del database risultante, che può quindi essere interrogata per determinare l'ID. (qv PostgreSQL INSERT ... RETURNING )

Dato che stiamo inserendo l'insert come comando (è è che modifica l'ID sul nostro oggetto da null a "qualcosa"), e i comandi devono restituire void, come determiniamo l'ID generato?

    
posta e_i_pi 14.11.2017 - 05:50
fonte

4 risposte

2

Given that we're issuing the insert as a command (it is changing the ID on our object from null to "something"), and commands must return void, how do we determine the generated ID?

Eseguendo una query, proprio come dice sul barattolo:

object.save();
id = object.getAutoIncrementedId();

La parte difficile è capire cosa sta facendo save () in questo caso.

Idea chiave: stiamo modificando l'archivio dati (inserendo una nuova riga) e stiamo modificando la copia locale dell'oggetto (passando l'ID auto incrementato ad esso). Quindi abbiamo davvero a che fare con l'orchestrazione di due comandi

void DB::addRow(state)
void Object::setAutoIncrementedId(id);

Ma queste firme non sono del tutto corrette; non hanno le affordance di cui abbiamo bisogno per scambiare i dati tra loro. Possiamo farlo con un callback

void DB::addRow(state, callback)
void Object::setAutoIncrementedId(id);

E quindi il comando save() sarebbe simile a:

void Object::save() {
    db.addRow(this.state(), this::setAutoIncrementedId);
}

Potrebbe sembrare più familiare se cambiamo leggermente l'ortografia

void DB::addRow(state, onSuccess, onError)

In altre parole, inviamo un messaggio al DB e descriviamo nelle caselle di posta dei messaggi che il database dovrebbe utilizzare per inoltrare messaggi aggiuntivi.

Come notato nei commenti, Seemann è una buona lettura

Lo fa, penso, palpare una carta, però ... da dove viene il GUID ? Guid.NewGuid() è semanticamente sicuro , ma non è davvero privo di effetti collaterali. Quindi, se dovessimo davvero andare a CQS tutte le cose, la firma dovrebbe davvero assomigliare a

void Guid::NewGuid(callback)

Vedi Applicazioni e i loro effetti collaterali ; come nel caso dell'esempio di Stack , può avere senso scambiare "purezza" per "chiarezza" e "familiarità".

    
risposta data 14.11.2017 - 14:49
fonte
2

In senso stretto, se restituisci l'ID generato automaticamente, interrompi CQS principio. Ci sono casi in cui semplicemente non puoi interromperlo. Lo stack è un altro esempio. L'operazione pop è sempre un comando e una query. Ciò non significa che gli stack siano sbagliati, sono utili quando veramente hanno bisogno di loro.

Se non vuoi infrangere questo principio, allora puoi usare GUID o UUID come ID entità e lasciare che l'ID generato automaticamente sia usato solo come surrogate ID, utile solo per l'infrastruttura (in questo modo non è necessario restituirlo).

    
risposta data 14.11.2017 - 07:12
fonte
1

Ho anche avuto difficoltà con questa domanda. Ho visto tutti i tipi di risposte che potrebbero avere problemi. I GUID richiederebbero una modifica del DB per archiviarli per selezionarli nuovamente, il che significa che stiamo cambiando il modello DB solo per aderire a CQS. HiLo funziona se puoi farlo, ma non tutti possono iniziare a lavorare con un nuovo modello di dati da zero e potrebbero utilizzare sistemi ERP che non lo consentiranno. Alcuni dicono che dovresti inviare un evento di notifica, che potrebbe funzionare, ma è molto complicato se vuoi usare quell'elemento direttamente nella risposta, ad esempio, da una chiamata di web api. Un'altra opzione è aggiornare il comando stesso con i dati, che funziona, ma poi devi sapere che il valore esiste. Infine, hai l'intero problema di Queue / Stack in cui trovare i dati che devi prima modificare qualcosa.

In definitiva, sono giunto alla conclusione che sono disponibili le opzioni Command, Query e CommandQuery. Ho interfacce di gestione per tutti i 3. Tecnicamente non romperà CQS perché questo è CQCQS. :) Detto questo, quando uso un CommandQuery nel mio codice, ci deve essere una ragione estremamente buona e deve essere documentato perché. CQS è un ottimo modello, ma uno schema non dovrebbe intralciare quello che stai cercando di ottenere alla fine. Preferirei semplicemente avere l'impostazione delle linee guida per tutti gli scenari che possono accadere, quindi abbiamo una soluzione quando non abbiamo una scelta e finiamo con 10 modi diversi per gestire lo stesso problema.

    
risposta data 11.01.2018 - 18:56
fonte
0

Dopo aver lavorato su questo problema in uno scenario di live fire per un po ', e riflettendo su di esso per un paio di mesi, penso che ci sia una soluzione chiara che non preveda la violazione dei principi di CQS o "palming a card" come @VoiceOfUnreason mettilo nella risposta che ho accettato.

Supponiamo che stiamo eseguendo un'operazione su un servizio che restituisce un risultato (database, API di terze parti, ecc.). Questa operazione è in effetti un comando. Tuttavia, sappiamo che ci sarà un risultato: sappiamo in anticipo di aver eseguito l'operazione.

Tradizionalmente, senza riguardo per CQS, potremmo fare qualcosa di simile (perdona la sintassi PHP, è il mio veleno):

interface UserPersistenceService
{
    public function saveUser(User $user): int;
}

$userID = $this->userPersistenceService()->saveUser($user);

Ora, con un comando, non dovremmo avere un tipo di ritorno int sul metodo saveUser() (che farebbe una query). I comandi, per definizione, possono modificare oggetti esistenti ma devono avere un tipo di reso vuoto. Poiché sappiamo già che ci sarà un risultato di questa operazione, perché non creare un risultato dell'operazione vuota, passarlo come secondo parametro nel comando di salvataggio e quindi interrogare quel risultato per userID dopo che l'operazione è stata eseguita?

interface UserPersistenceService
{
    public function saveUser(User $user, SaveOperation $saveOperation): void;
}

class SaveOperation()
{
    private $isProcessed = false;
    private $result;

    public function processResult($result): void
    {
        $this->Result = $result;
        $this->isProcessed = true;
    }

    public function result(): DatabasePersistenceResult
    {
        if($this->isProcessed() === false) {
            throw new ResultNotProcessedException();
        } else {
            return $this->result;
        }
    }
}

$saveOperation = new SaveOperation();
$this->userPersistenceService()->saveUser($user, $saveOperation);
if($saveOperation->result()->wasSuccessful()) {
    $userID = $saveOperation->result()->lastInsertID();
} else {
    throw new SaveOperationFailedException();
}

Per me questo sembra un buon approccio - non stiamo violando i principi di CQS, stiamo evitando i problemi derivanti dall'uso dei GUID, e inoltre ci stiamo permettendo un controllo molto maggiore quando gestiamo i risultati di operazioni esterne potenzialmente fragili (ossia API non raggiungibili, bug in API di terze parti, contesa del database, ecc.)

    
risposta data 11.01.2018 - 23:22
fonte

Leggi altre domande sui tag