Drogon Gives You 4 Ways to Do One Thing — Here’s the Right One

I’ve been learning how to use Drogon’s ORM, and noticed something weird. Drogon lets you do the same database queries four different ways. Three of them are in the documentation. The fourth one isn’t — and it’s the one you actually want.

Let’s go through all four, why they exist, and which one you should be using in your code.

Method 1: Easy, but Blocks the Thread

I’m going to stick to just one database operation: finding a single record. This example code uses the first version of findOne():

/** Respond to a get user request.
 */
void getUserBlocking(const HttpRequestPtr& req,
                     std::function<void(const HttpResponsePtr&)>&& callback)
{
    // FIXME: Do authorization checking here. You do NOT want everyone to be able
    // to get user info!

    auto username = req->getParameter("username");

    try {
        auto dbClient = app().getDbClient();
        Mapper<Users> mapper(dbClient);
        auto user = mapper.findOne(Criteria(Users::Cols::_username,
                                             CompareOperator::EQ,
                                             username));

        auto resp = HttpResponse::newHttpJsonResponse(user.toJson());
        callback(resp);
    }
    catch (const UnexpectedRows& e) {
        // User not found
        auto resp = HttpResponse::newHttpJsonResponse(Json::Value());
        resp->setStatusCode(k404NotFound);
        resp->setBody("{\"error\": \"User not found\"}");
        callback(resp);
    }
    catch (const std::exception& e) {
        // Database or other errors
        LOG_ERROR << "Error getting user: " << e.what();
        auto resp = HttpResponse::newHttpJsonResponse(Json::Value());
        resp->setStatusCode(k500InternalServerError);
        resp->setBody("{\"error\": \"Internal server error\"}");
        callback(resp);
    }
}

It’s looking up a user by username. The code is very straightforward. FindOne() looks up the user in the users table, and returns it. If no user is found, then it throws an exception

This variation has one big downside: database lookups take time, and findOne() blocks execution until it’s done. That’s bad news for a webserver with multiple requests coming in. You could use multiple threads to handle multiple simultaneous HTTP requests, but that’s rather resource-intensive.

Method 2: Asynchronous Callbacks

That’s why the non-blocking callback variation of findOne() exists. You give findOne() callback functions to call when it’s done. One for success, the other for failure.

/** Respond to a get user request (non-blocking version).
 */
void getUserNonBlocking(const HttpRequestPtr& req,
                        std::function<void(const HttpResponsePtr&)>&& callback)
{
    // FIXME: Do authorization checking here. You do NOT want everyone to be able
    // to get user info!

    auto username = req->getParameter("username");

    auto dbClient = app().getDbClient();
    Mapper<Users> mapper(dbClient);
    
    // Use the async version with callback
    mapper.findOne(
        Criteria(Users::Cols::_username, CompareOperator::EQ, username),
        [callback = std::move(callback)](Users user) {
            // Success: user found
            auto resp = HttpResponse::newHttpJsonResponse(user.toJson());
            callback(resp);
        },
        [callback = std::move(callback), username](const DrogonDbException& e) {
            // Error handler - check if it's an UnexpectedRows exception
            if (dynamic_cast<const UnexpectedRows*>(&e)) {
                // User not found
                auto resp = HttpResponse::newHttpJsonResponse(Json::Value());
                resp->setStatusCode(k404NotFound);
                resp->setBody("{\"error\": \"User not found\"}");
                callback(resp);
            } else {
                // Database or other errors
                LOG_ERROR << "Error getting user '" << username << "': " << e.base().what();
                auto resp = HttpResponse::newHttpJsonResponse(Json::Value());
                resp->setStatusCode(k500InternalServerError);
                resp->setBody("{\"error\": \"Internal server error\"}");
                callback(resp);
            }
        }
    );
}

The database lookup is done asynchronously, so the server thread can handle other incoming requests while the database lookup is happening. Awesome! We have performance!

Unfortunately, the code is getting uglier, and we’re only handling one database operation. Let’s say the request has multiple things that need to be done. One callback, calls another database operation with more callbacks, which in turn calls another one once it’s done. It’s easy to end up in callback hell.

Method 3: Futures

So, there’s a third option: findFutureOne():

/** Respond to a get user request (future version).
 * 
 * Note: This uses .get() which blocks the thread until the result is ready,
 * similar to the blocking example. For truly non-blocking behavior, use
 * co-routines.
 */
void getUserFuture(const HttpRequestPtr& req,
                   std::function<void(const HttpResponsePtr&)>&& callback)
{
    // FIXME: Do authorization checking here. You do NOT want everyone to be able
    // to get user info!

    auto username = req->getParameter("username");

    try {
        auto dbClient = app().getDbClient();
        Mapper<Users> mapper(dbClient);
        
        // Get the future and wait for the result with .get()
        auto future = mapper.findFutureOne(
            Criteria(Users::Cols::_username, CompareOperator::EQ, username)
        );
        
        // .get() blocks until the result is ready
        auto user = future.get();

        auto resp = HttpResponse::newHttpJsonResponse(user.toJson());
        callback(resp);
    }
    catch (const UnexpectedRows& e) {
        // User not found
        auto resp = HttpResponse::newHttpJsonResponse(Json::Value());
        resp->setStatusCode(k404NotFound);
        resp->setBody("{\"error\": \"User not found\"}");
        callback(resp);
    }
    catch (const std::exception& e) {
        // Database or other errors
        LOG_ERROR << "Error getting user '" << username << "': " << e.what();
        auto resp = HttpResponse::newHttpJsonResponse(Json::Value());
        resp->setStatusCode(k500InternalServerError);
        resp->setBody("{\"error\": \"Internal server error\"}");
        callback(resp);
    }
}

This one returns a “future” object immediately, which allows your code to continue doing other tasks while waiting for the database operation to complete. The code is looking nicer.

However, calling get() on that future will block execution, until the database lookup is complete. Trying to interleave multiple operations to avoid blocking the thread gets tricky. Hmmm. Not so great after all.

Method 4: Coroutines

This is where the fourth option comes in: co-routines:

/** Respond to a get user request (coroutine version).
 */
Task<HttpResponsePtr> getUserCoroutine(HttpRequestPtr req,
                        std::function<void(const HttpResponsePtr&)> callback)
{
    // FIXME: Do authorization checking here. You do NOT want everyone to be able
    // to get user info!

    auto username = req->getParameter("username");

    try {
        auto dbClient = app().getDbClient();
        CoroMapper<Users> mapper(dbClient);
        
        // Use co_await to asynchronously wait for the result
        auto user = co_await mapper.findOne(
            Criteria(Users::Cols::_username, CompareOperator::EQ, username)
        );

        auto resp = HttpResponse::newHttpJsonResponse(user.toJson());
        callback(resp);
    }
    catch (const UnexpectedRows& e) {
        // User not found
        auto resp = HttpResponse::newHttpJsonResponse(Json::Value());
        resp->setStatusCode(k404NotFound);
        resp->setBody("{\"error\": \"User not found\"}");
        callback(resp);
    }
    catch (const std::exception& e) {
        // Database or other errors
        LOG_ERROR << "Error getting user '" << username << "': " << e.what();
        auto resp = HttpResponse::newHttpJsonResponse(Json::Value());
        resp->setStatusCode(k500InternalServerError);
        resp->setBody("{\"error\": \"Internal server error\"}");
        callback(resp);
    }
}

I was first introduced to coroutines via the Go programming language. With co-routines, you can write code that looks almost exactly like the first variation, except for this little co_await here. This tells the compiler that it can pause this co-routine while it’s waiting for a result, and run a different co-routine while it’s waiting. So the code is as asynchronous as the callback version, but as easy to understand as the original blocking findOne() call. Nice!

This is the one you should be using in new code.

Why are there 4 Ways to Do the Same Thing in Drogon C++?

So why have the first three then? For the same reason that I didn’t use co-routines for ZitaFTP Server: co-routines weren’t available when they started writing Drogon. I remember vividly, that co-routines were about to be added to C++ when I started working on ZitaFTP. It was so frustrating to know that co-routines were almost there, but I couldn’t use them because compiler support for them was still a few years away.

Drogon had the same problem. Co-routines weren’t available when they got started (round 2019, IIRC). And so we end up with three legacy ways to do the same thing, and co-routines. The legacy methods are there for existing code, and those using older compilers.

For new projects, use co-routines.

Leave a Comment

Your email address will not be published. Required fields are marked *

 


Shopping Cart
Scroll to Top