I’ve been doing a lot more server-side programming in the last few years. Being able to write Swift on both sides is a real joy. I have a client with whom I’ve built a reasonably full-featured social app in Vapor, and all my personal stuff has been using Meridian, which has been going great. (I will have some contracting availability for server-side Swift coming up soon, so definitely get in touch if you have a project!)
However, one part of the process that I don’t enjoy much is using an ORM. There’s too much magic when working with them. I feel disconnected from the queries that are being run, and it’s too easy to accidentally add an n+1 query. I also don’t like how it turns a relational system into an object graph — I’d much prefer to work in terms of records with related IDs, rather than objects with children.
ORMs run some of the biggest sites and systems in the world — if you like them, keep using them. If they make you feel weird too, the rest of the post might be for you.
ORMs do give you one thing that is great: a single source of truth, which is the model definition in the application code. However, this single source of truth is not always trustworthy — there’s nothing to keep it in-line with what’s actually in the database.
For example, if a field in my model object goes from being optional to non-optional, things could work smoothly for almost every row, but if some old row has a stray null in the database, decoding the model object will fail and cause my application to do something unexpected.
The real problem here is that my application code is always changing and a home for bugs.
As much as I distrust my own application code, I’ve come to trust Postgres. Postgres is a reliable and sensible choice for a backend database. Postgres is laden with great features — nullability, strong foreign keys, data blobs, performant unbounded text, JSON, extensions for UUIDs, GIS, and on and on. I just love it. I use it for everything, and far prefer it to other options.
Postgres’s constraint system feels like a warm blanket in the same way that Swift’s type system does.
- If a field is never supposed to be null, you make it
NOT NULL
and you’re set. - If a field is a foreign key for for another table, mark it so and you won’t be able to delete a referenced row without deleting the referenced row (unless you choose some other behavior).
- If a field has arbitrary computable constraints (like a score that must be between 0 and 100), you can add those, too.
What I want is a way to marry Postgres’s constraints to Swift’s type system — some way to propogate Postgres’s guarantees into my own type system.
After watching a Gary Bernhardt screencast (video, 18m, corresponding blog post), I saw the path forward. (If you’ve got 18 minutes, watch this talk. It’s a wonder.)
Gary shows how he uses types (in TypeScript) to make a change flow from the database all the way through to the UI, using a TypeScript tool called schemats, which creates simple interfaces that represent each table. These simple structures can be decoded easily and always represent the state of the database.
I ported schemats
to Swift, to bring this same strategy to our favorite language. It’s called SchemaSwift. Working with it is pretty straightforward:
SchemaSwift \
--url="<POSTGRES_URL>" \
--override blog_posts.category=Category \
--output ~/Desktop/DatabaseModels.generated.swift \
When you run it, out pops a file with all your tables, represented as Swift structs:
// Generated code:
struct BlogPost: Codable {
static let tableName = "blog_posts"
let id: UUID
let content: String
let authorID: UUID
let category: Category?
enum CodingKeys: String, CodingKey {
case id = "id"
case content = "content"
case author = "author_id"
case category = "category"
}
}
The types it can infer as native Swift values are automatically handled, and you can override other fields with your own custom types. Nullability/optionals are brought over, so you’ll never decode an honest value when the database can potentially have a null in it. Postgres enums are turned into Swift enums as well.
It slots really nicely into Vapor’s SQLKit, using Codable:
let users = try db.select()
...
.all(decoding: BlogPost.self)
.wait()
(I also helped add support for this kind of decoding to another Swift Postgres libary. We have a great community.)
I’ve been using the tool for over 3 years now (I know, I’ve been very remiss in my blogging) and it’s been going great. Because you can see the output of the tool before you commit it, there’s very little risk in using it.
One final component that would normally be handled by ORM is migrations. For that, I want to explore mig, which would fill this gap nicely.
There’s plenty of future work here — running this at build time on the server (so that your build literally won’t complete if it’s building against a database that it can’t talk to!), using SPM’s extensible build tools, storing settings in a JSON file in your git root, type name prefixes and suffixes, and a module to hold all the generated code — these are all appealing ideas. If you end up needing or implementing one of these features, definitely drop me a line! I would love to integrate it.