Categories
Technology

Async/await in Vapor

Swift 5.5 with the new async/await features is going to be released quite soon, and I’m very excited. And while all the new async APIs in UIKit and SwiftUI are neat, they don’t actually catch my eye all that much. As of now, async/await will only be available on the fall-2021 Apple OS releases, and following the general pattern of “current iOS minus one” that means async/await will be available starting… fall of 2022.

However, that’s not the only place I use Swift! I also dabble in server-side Swift when I can, and over there, it’s ready-ish to use now.1

And hey, would you look at that, Vapor’s already got a PR open for adding async variants of the core APIs. Neat! Let’s see how this is implemented.

And, hey, it’s… pretty simple, really! All it takes is getting into the (pre-release!) _NIOConcurrency module, and suddenly any EventLoopFuture has a new function: public func get() async throws -> T. The implementation of that is also lovely and simple, thanks to Swift’s withUnsafeThrowingContinuation and the way SwiftNIO already works.

Now, where I’m more excited about this than Vapor is in Fluent — a lot of the server-side work I do is in C#, using the excellent Entity Framework Core. Database requests are straightforward: MyModel model = await db.MyModels.FirstOrDefaultAsync(m => m.id == id);

With the non-async version of Vapor and Fluent, this is… a bit more difficult to work with. MyModel.find(id, on: req.db).flatMap { model in ... }

At first glance it doesn’t look too bad, but notice the flatMap — we can’t just continue writing our code, we have to move into a new scope. Hello, pyramid of doom.

But now?2 Now, we can await things. Fluent looks more like Entity Framework all of a sudden: let model = try await MyModel.find(id, on: req.db)

That’s already a much nicer API to work with, and it delivers on the promise of async/await: much simpler flow of code. I’m excited to untangle some of my more complex flatMap pyramids.

What I’m still curious about, and will need to do some testing with, is to see how well it does with concurrency. It’s a known issue in Entity Framework that you can’t run multiple requests against a single DbContext; what I’m wondering is if Fluent has this same limitation, or if I can safely do something like:

async let model1 = try MyModel.find(id1, on: req.db)
async let model2 = try MyModel.find(id2, on: req.db)
let result = MyResult(model1: await model1, model2: await model2)

That, I don’t yet know! I’ll have to do some experimenting and try it — which may be a future blog post. Still, even if I have to immediately await everything, the improvements to my code’s legibility will be worth it.

  1. Disclaimer: Swift 5.5 is, as of this writing, pre-release; similarly, the versions of Swift-NIO and Vapor that provide async APIs are pre-release. Don’t use these in production.
  2. Well, soon: this as another draft PR as of this writing.
Categories
Technology

Default Values in Vapor Fluent

My recent tinkering has been with Vapor, and while I mostly like their Fluent ORM, it has some rough edges and semi-undocumented behavior. At some point, I’ll feel confident enough in what I’ve learned through trial and error (combined with reading the source code – open source!) to actually make some contributions to the documentation, but for now, I’m going to throw some of the things I struggled with up here.

If you’re using a migration to add a column, and specifically want it to be non-null, you’ll need a default value. My first approach was to do a three-step migration, adding the column as nullable, then filling the default value on all rows, and then setting the column to be non-null, but that didn’t feel right. Eventually, though, I figured out how to express a DEFAULT constraint in Fluent:

let defaultValueConstraint = SQLColumnConstraintAlgorithm.default(/* your default value here */)

Then, in your actual schema builder call:

.field("column_name", /* your type */, .sql(defaultValueConstraint), .required)

Note that SQLColumnConstraintAlgorithm isn’t available from the Fluent module, you’ll need to import SQLKit first.

And here, a full worked example:

import Vapor
import Fluent
import SQLKit

struct DemoMigration: Migration {
    func prepare(on database: Database) -> EventLoopFuture<Void> {
        let defaultValueConstraint = SQLColumnConstraintAlgorithm.default(false)
        return database.schema(DemoModel.schema)
            .field(DemoModel.FieldKeys.hasBeenTouched, .bool, .sql(defaultValueConstraint), .required)
            .update()
    }
    
    func revert(on database: Database) -> EventLoopFuture<Void> {
        database.schema(DemoModel.schema)
            .deleteField(DemoModel.FieldKeys.hasBeenTouched)
            .update()
    }
}

(For context, I’m in the habit of having a static var schema: String { "demo_model" } and a struct FieldKeys { static var hasBeenTouched: FieldKey { "has_been_touched" } } within each of my Fluent models – it keeps everything nice and organized, and avoids having stringly-typed issues all over the place.)