5

I've been searching for weeks for an answer to this question, which seems as though it would be common across pretty much all applications and therefore a problem right at the forefront of CQS.

CQS dictates that a function should either:

  • Change state and return void (command), or
  • Change nothing and return a result (query)

So, let's say we want to create a new data entity, persist it in the database, then issue a notification of the auto-incremented ID.

First, we create the object - at this point it has no unique identifier. We then save it to the database, which generates a unique identifier via the auto-incremented ID field. The query execution result is returned to the application, returning the resultant database row, which can then be interrogated to determine the ID. (q.v. PostgreSQL INSERT...RETURNING)

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?

  • 2
    First of all, you are confusing CQRS and CQS. What you mean is CQS. The easiest way to deal with generated IDs is not to use them. UUIDs, HI-LO, and sequences can be used instead. – abuzittin gillifirca Nov 14 '17 at 5:07
  • 1
    I've updated it to say CQS, sorry about that. I can't change the tag because CQS doesn't exist as a tag here (weird). The "just don't use them" approach isn't really helpful here. I've simplified the problem down to a particular use case, the scenario where you want to synchronously use the execution result is still valid. Take for example making a cURL request, where you immediately want to work with a return set in JSON format. – e_i_pi Nov 14 '17 at 5:19
  • 4
    The choice to use or not use database generated id's is yours. If you use them, then make the command return a value. You are the master of your architecture. A good question might be, "What do I lose by returning a value from a command?" – joshp Nov 14 '17 at 5:25
  • 4
    If your conditions are that you must change state and return a new ID generated from the database, then you cannot meet the stated conditions for CQS. Don't worry; you won't rip a hole in the universe. – Robert Harvey Nov 14 '17 at 5:33
  • 1
    What is wrong in the trivial approach to make the command void Insert(MyObject obj) do the insert and set obj.ID to the autogenerated ID? So the caller can easily determine the ID from the object afterwards? – Doc Brown Nov 14 '17 at 7:24
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?

By issuing a query, just like it says on the tin:

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

The tricky part is figuring out what save() is doing in this case.

Key idea - we're modifying the data store (by inserting a new row), and we are modifying the local copy of the object (by passing the auto incremented id to it). So we're really dealing with the orchestration of two commands

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

But, these signatures aren't quite right; they don't have the affordances we need to exchange the data between them. We can achieve that with a callback

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

And thus the save() command would look something like:

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

This might look more familiar if we change up the spelling slightly

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

In other words, we're sending a message to the DB, and describing in the message inboxes that the database should use to forward additional messages.

As noted in the comments, Seemann is a pretty good read

He does, I think, palm a card, though... where does the GUID come from? Guid.NewGuid() is semantically safe, but it's not really side effect free. So if we were really going to CQS all the things, the signature should really look like

void Guid::NewGuid(callback)

See Applications and their side effects; as is the case with the Stack example, it can make sense to trade "purity" for "clarity" and "familiarity".

  • 1
    Thanks, I read both posts linked here and mentioned in the comments, and I think the take away from all this is that you can be ideological, or you can relax the rules and wear the cost of the trade off. As I mentioned in comments earlier, this example (database IDs) is really just a boiled down use case. When we start talking about third party APIs that auto-generate IDs, where you can't explicitly declare a GUID, then it becomes a bit more intricate. Also, it gets more complex when you have "create" calls travelling up and down n-Tier layers. Thanks for your answer, great and very thorough! – e_i_pi Nov 14 '17 at 22:13
2

Strictly speaking, if you return the auto-generated ID then you break the CQS principle. There are cases when you just cannot not break it. The stack is another example. The pop operation is always a command and a query. This does not mean that stacks are wrong, they are good when you really need them.

If you don't want to break this fine principle then you can use GUIDs or UUIDs as entity IDs and let the auto-generated ID to be used only as a surrogate ID, useful only to the infrastructure (in this way you don't have to return it).

  • Thanks for you answer, I've up-voted it but chose the other answer, as it went into more detail, but I do appreciate the thoughts you've given here. As I mentioned in comments earlier, I'm not sold on the GUID/UUID solution, as it doesn't neatly fit with micro-service calls (where you can't pass a GUID and expect it to be stored), and it also feels like re-inventing the wheel. – e_i_pi Nov 14 '17 at 22:18
  • @e_i_pi Why do you think that GUIDs don't fit with microservice architectures? – Constantin Galbenu Nov 15 '17 at 1:59
  • I mean we can't necessarily provide our own GUID, then receive the GUID and the SaaS' identifier back, in order to identify the correct mapping. – e_i_pi Nov 15 '17 at 2:09
1

I've been struggling with this question as well. I've seen all kinds of answers that could have problems. GUIDs would require a DB change to store that to select it back out, which means we are changing our DB model just to adhere to CQS. HiLo works if you can do that, but not everyone gets to work with a new data model from scratch and could be using ERP systems that won't allow for that. Some say you should send out a notification event, which could work, but it feels very cumbersome if you want to use that item directly in the response from, say, a web api call. Another option is to update the command itself with the data, which works, but then you have to know that value exists. Finally, you have the whole Queue/Stack issue where to get data you have to change something first.

Ultimately, I came to the conclusion that I have Command, Query, and CommandQuery options available. I have handler interfaces for all 3. It technically won't break CQS because this is CQCQS. :) That being said, when I use a CommandQuery in my code, there has to be an extremely good reason for it and must be documented why. CQS is a great pattern, but a pattern shouldn't get in the way of what you are trying to achieve in the end. I'd rather just have the guidelines setup for all the scenarios that can happen so we have a solution when we don't have a choice and end up with 10 different ways to handle the same problem.

  • Thanks for the answer. For what I'm working on right now, I'm treating any "new", "build" or "create" data operation as a query. Why? Because you're not working on an existing object, but creating a new one. To me, this is a query - you're asking for something in return. Does it change an existing artefact? I don't think so. Until something is persisted, it doesn't exist. I'll post another alternative when I get to my PC, showing how you can adhere to CQS without using GUIDs at all. – e_i_pi Jan 11 '18 at 19:31
  • I understand what you are saying, but if you ran the create "query" multiple times, you'd get back different results (different PK). Thus, not sure how I feel about it. :) – Daniel Lorenz Jan 11 '18 at 19:41
0

After working on this problem in a live fire scenario for a while, and thinking on it for a couple of months, I think there is a neat solution that doesn't involve breaching the tenets of CQS or "palming a card" as @VoiceOfUnreason put it in the answer I accepted.

Let us assume that we are committing an operation to a service that returns a result (database, third party API, etc.). This operation is in effect a Command. Yet, we know there is going to be a result - we know this in advance of committing the operation.

Traditionally, without regard for CQS, we might do something like this (forgive the PHP syntax, it's my poison):

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

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

Now, with a Command, we shouldn't have an int return type on the saveUser() method (that would make it a query). Commands, by definition, may alter existing objects yet must have a void return type. Since we already know there is going to be a result of this operation, why not create a blank operation result, pass that as a second parameter in the save Command, and then interrogate that result for the userID after the operation is committed?

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();
}

To me this looks like a good approach - we aren't violating the principles of CQS, we're avoiding the problems that come with the use of GUIDs, and additionally we are allowing ourselves much greater control when handling the results of potentially frail external operations (i.e. API unreachable, bugs in third-party APIs, database contention, etc.)

  • The problem, at least from C# and my implementation, is that I pair this with decorators. That means that all commands/queries have to abide by the same interface so the chaining can happen correctly. While I could pass in an extra paremeter, I instead end up returning an IResult object from all queries/commands, but only for if it was successful or the specific error message. I can't put something on this return object that doesn't get used by everyone. So a Query would never return "new Id" and some tables need more than 1 pk column. That is why I use CommandQueries instead for this. – Daniel Lorenz Jan 12 '18 at 17:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.