Fluent is actually damaged
The extra I exploit the Fluent ORM framework the extra I notice how arduous it’s to work with it. I am speaking a couple of explicit design problem that I additionally talked about in the way forward for server facet Swift article. I actually don’t love the thought of property wrappers and summary database fashions.
What’s the issue with the present database mannequin abstraction? Initially, the non-compulsory ID property is complicated. For instance you do not have to offer an identifier if you insert a report, it may be an nil worth and the ORM system can create a novel identifier (beneath the hood utilizing a generator) for you. So why do we’ve got an id for create operations in any respect? Sure, you would possibly say that it’s attainable to specify a customized identifier, however actually what number of occasions do we want that? If you wish to establish a report that is going to be one thing like a key, not an id discipline. 🙃
Additionally this non-compulsory property may cause another points, when utilizing fluent you may require an id, which is a throwing operation, alternatively you may unwrap the non-compulsory property in the event you’re certain that the identifier already exists, however this isn’t a protected strategy in any respect.
My different problem is expounded to initializers, in the event you outline a customized mannequin you at all times have to offer an empty init() {}
methodology for it, in any other case the compiler will complain, as a result of fashions should be courses. BUT WHY? IMHO the rationale pertains to this problem: you may question the database fashions utilizing the mannequin itself. So the mannequin acts like a repository that you should utilize to question the fields, and it additionally represents the the report itself. Is not this towards the clear rules? 🤔
Okay, one last item. Property wrappers, discipline keys and migrations. The core members at Vapor instructed us that this strategy will present a protected solution to question my fashions and I can make sure that discipline keys will not be tousled, however I am really combating versioning on this case. I needed to introduce a v1, v2, vN construction each for the sphere keys and the migration, which really feels a bit worse than utilizing uncooked strings. It’s over-complicated for certain, and it feels just like the schema definition is combined up with the precise question mechanism and the mannequin layer as nicely.
Sorry people, I actually admire the trouble that you have put into Fluent, however these points are actual and I do know you can repair them on the long run and make the developer expertise rather a lot higher.
make Fluent a bit higher?
On the quick time period I am making an attempt to repair these points and fortuitously there’s a good strategy to separate the question mechanism from the mannequin layer. It’s referred to as the repository sample and I might like to offer an enormous credit score to 0xTim once more, as a result of he made a cool reply on StackOverlow about this subject.
Anyway, the principle concept is that you simply wrap the Request
object right into a customized repository, it is often a struct, then you definitely solely name database associated queries inside this particular object. If we check out on the default undertaking template (you may generate one by utilizing the vapor toolbox), we are able to simply create a brand new repository for the Todo fashions.
import Vapor
import Fluent
struct TodoRepository {
var req: Request
init(req: Request) {
self.req = req
}
func question() -> QueryBuilder<Todo> {
Todo.question(on: req.db)
}
func question(_ id: Todo.IDValue) -> QueryBuilder<Todo> {
question().filter(.$id == id)
}
func question(_ ids: [Todo.IDValue]) -> QueryBuilder<Todo> {
question().filter(.$id ~~ ids)
}
func checklist() async throws -> [Todo] {
attempt await question().all()
}
func get(_ id: Todo.IDValue) async throws -> Todo? {
attempt await get([id]).first
}
func get(_ ids: [Todo.IDValue]) async throws -> [Todo] {
attempt await question(ids).all()
}
func create(_ mannequin: Todo) async throws -> Todo {
attempt await mannequin.create(on: req.db)
return mannequin
}
func replace(_ mannequin: Todo) async throws -> Todo {
attempt await mannequin.replace(on: req.db)
return mannequin
}
func delete(_ id: Todo.IDValue) async throws {
attempt await delete([id])
}
func delete(_ ids: [Todo.IDValue]) async throws {
attempt await question(ids).delete()
}
}
That is how we’re can manipulate Todo fashions, any more you do not have to make use of the static strategies on the mannequin itself, however you should utilize an occasion of the repository to change your database rows. The repository might be hooked as much as the Request object by utilizing a standard sample. The most straightforward means is to return a service each time you want it.
import Vapor
extension Request {
var todo: TodoRepository {
.init(req: self)
}
}
In fact this can be a very primary resolution and it pollutes the namespace beneath the Request object, I imply, in case you have numerous repositories this generally is a downside, however first let me present you learn how to refactor the controller by utilizing this easy methodology. 🤓
import Vapor
struct TodoController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
let todos = routes.grouped("todos")
todos.get(use: index)
todos.put up(use: create)
todos.group(":todoID") { todo in
todo.delete(use: delete)
}
}
func index(req: Request) async throws -> [Todo] {
attempt await req.todo.checklist()
}
func create(req: Request) async throws -> Todo {
let todo = attempt req.content material.decode(Todo.self)
return attempt await req.todo.create(todo)
}
func delete(req: Request) async throws -> HTTPStatus {
guard let id = req.parameters.get("todoID", as: Todo.IDValue.self) else {
throw Abort(.notFound)
}
attempt await req.todo.delete(id)
return .okay
}
}
As you may see this manner we had been capable of get rid of the Fluent dependency from the controller, and we are able to merely name the suitable methodology utilizing the repository occasion. Nonetheless if you wish to unit check the controller it isn’t attainable to mock the repository, so we’ve got to determine one thing about that problem. First we want some new protocols.
public protocol Repository {
init(_ req: Request)
}
public protocol TodoRepository: Repository {
func question() -> QueryBuilder<Todo>
func question(_ id: Todo.IDValue) -> QueryBuilder<Todo>
func question(_ ids: [Todo.IDValue]) -> QueryBuilder<Todo>
func checklist() async throws -> [Todo]
func get(_ ids: [Todo.IDValue]) async throws -> [Todo]
func get(_ id: Todo.IDValue) async throws -> Todo?
func create(_ mannequin: Todo) async throws -> Todo
func replace(_ mannequin: Todo) async throws -> Todo
func delete(_ ids: [Todo.IDValue]) async throws
func delete(_ id: Todo.IDValue) async throws
}
Subsequent we’ll outline a shared repository registry utilizing the Utility
extension. This registry will enable us to register repositories for given identifiers, we’ll use the RepositoryId struct for this objective. The RepositoryRegistry
will be capable to return a manufacturing unit occasion with a reference to the required request and registry service, this manner we’re going to have the ability to create an precise Repository based mostly on the identifier. In fact this entire ceremony might be averted, however I needed to provide you with a generic resolution to retailer repositories beneath the req.repository
namespace. 😅
public struct RepositoryId: Hashable, Codable {
public let string: String
public init(_ string: String) {
self.string = string
}
}
public ultimate class RepositoryRegistry {
non-public let app: Utility
non-public var builders: [RepositoryId: ((Request) -> Repository)]
fileprivate init(_ app: Utility) {
self.app = app
self.builders = [:]
}
fileprivate func builder(_ req: Request) -> RepositoryFactory {
.init(req, self)
}
fileprivate func make(_ id: RepositoryId, _ req: Request) -> Repository {
guard let builder = builders[id] else {
fatalError("Repository for id `(id.string)` just isn't configured.")
}
return builder(req)
}
public func register(_ id: RepositoryId, _ builder: @escaping (Request) -> Repository) {
builders[id] = builder
}
}
public struct RepositoryFactory {
non-public var registry: RepositoryRegistry
non-public var req: Request
fileprivate init(_ req: Request, _ registry: RepositoryRegistry) {
self.req = req
self.registry = registry
}
public func make(_ id: RepositoryId) -> Repository {
registry.make(id, req)
}
}
public extension Utility {
non-public struct Key: StorageKey {
typealias Worth = RepositoryRegistry
}
var repositories: RepositoryRegistry {
if storage[Key.self] == nil {
storage[Key.self] = .init(self)
}
return storage[Key.self]!
}
}
public extension Request {
var repositories: RepositoryFactory {
software.repositories.builder(self)
}
}
As a developer you simply should provide you with a brand new distinctive identifier and lengthen the RepositoryFactory along with your getter to your personal repository kind.
public extension RepositoryId {
static let todo = RepositoryId("todo")
}
public extension RepositoryFactory {
var todo: TodoRepository {
guard let end result = make(.todo) as? TodoRepository else {
fatalError("Todo repository just isn't configured")
}
return end result
}
}
We are able to now register the FluentTodoRepository object, we simply should rename the unique TodoRepository struct and conform to the protocol as an alternative.
public struct FluentTodoRepository: TodoRepository {
var req: Request
public init(_ req: Request) {
self.req = req
}
func question() -> QueryBuilder<Todo> {
Todo.question(on: req.db)
}
}
app.repositories.register(.todo) { req in
FluentTodoRepository(req)
}
We’re going to have the ability to get the repository by means of the req.repositories.todo
property. You do not have to vary anything contained in the controller file.
import Vapor
struct TodoController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
let todos = routes.grouped("todos")
todos.get(use: index)
todos.put up(use: create)
todos.group(":todoID") { todo in
todo.delete(use: delete)
}
}
func index(req: Request) async throws -> [Todo] {
attempt await req.repositories.todo.checklist()
}
func create(req: Request) async throws -> Todo {
let todo = attempt req.content material.decode(Todo.self)
return attempt await req.repositories.todo.create(todo)
}
func delete(req: Request) async throws -> HTTPStatus {
guard let id = req.parameters.get("todoID", as: Todo.IDValue.self) else {
throw Abort(.notFound)
}
attempt await req.repositories.todo.delete(id)
return .okay
}
}
One of the best a part of this strategy is you can merely change the FluentTodoRepository
with a MockTodoRepository
for testing functions. I additionally like the truth that we do not pollute the req.* namespace, however each single repository has its personal variable beneath the repositories key.
You may provide you with a generic DatabaseRepository
protocol with an related database Mannequin kind, then you could possibly implement some primary options as a protocol extension for the Fluent fashions. I am utilizing this strategy and I am fairly pleased with it to date, what do you assume? Ought to the Vapor core staff add higher help for repositories? Let me know on Twitter. ☺️