[ad_1]
Fluent is basically damaged
The extra I take advantage of the Fluent ORM framework the extra I understand how arduous it’s to work with it. I am speaking a couple of specific design challenge that I additionally talked about within the way forward for server facet Swift article. I actually don’t love the concept of property wrappers and summary database fashions.
What’s the issue with the present database mannequin abstraction? To begin with, the elective ID property is complicated. For instance you do not have to supply an identifier once you insert a document, it may be an nil
worth and the ORM system can create a novel identifier (underneath the hood utilizing a generator) for you. So why do we now have an id for create operations in any respect? Sure, you may say that it’s potential to specify a customized identifier, however actually what number of occasions do we want that? If you wish to establish a document that is going to be one thing like a key, not an id area. 🙃
Additionally this elective property may cause another points, when utilizing fluent you’ll be able to require an id, which is a throwing operation, alternatively you’ll be able to unwrap the elective property should you’re certain that the identifier already exists, however this isn’t a protected strategy in any respect.
My different challenge is expounded to initializers, should you outline a customized mannequin you at all times have to supply an empty init() {}
methodology for it, in any other case the compiler will complain, as a result of fashions must be courses. BUT WHY? IMHO the explanation pertains to this challenge: you’ll be able to question the database fashions utilizing the mannequin itself. So the mannequin acts like a repository that you need to use to question the fields, and it additionally represents the the document itself. Is not this in opposition to the clear rules? 🤔
Okay, one final thing. Property wrappers, area keys and migrations. The core members at Vapor advised us that this strategy will present a protected method to question my fashions and I can make sure that area keys will not be tousled, however I am truly scuffling with versioning on this case. I needed to introduce a v1, v2, vN construction each for the sector keys and the migration, which truly feels a bit worse than utilizing uncooked strings. It’s over-complicated for certain, and it feels just like the schema definition is blended up with the precise question mechanism and the mannequin layer as nicely.
Sorry people, I actually respect the trouble that you’ve got put into Fluent, however these points are actual and I do know that you could repair them on the long run and make the developer expertise loads higher.
Find out how to make Fluent a bit higher?
On the quick time period I am attempting to repair these points and fortuitously there’s a good strategy to separate the question mechanism from the mannequin layer. It’s known as the repository sample and I would like to provide an enormous credit score to 0xTim once more, as a result of he made a cool reply on StackOverlow about this subject.
Anyway, the primary thought is that you just wrap the Request
object right into a customized repository, it is normally a struct, you then solely name database associated queries inside this particular object. If we check out on the default venture template (you’ll be able to generate one by utilizing the vapor toolbox), we will 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 record() async throws -> [Todo] {
strive await question().all()
}
func get(_ id: Todo.IDValue) async throws -> Todo? {
strive await get([id]).first
}
func get(_ ids: [Todo.IDValue]) async throws -> [Todo] {
strive await question(ids).all()
}
func create(_ mannequin: Todo) async throws -> Todo {
strive await mannequin.create(on: req.db)
return mannequin
}
func replace(_ mannequin: Todo) async throws -> Todo {
strive await mannequin.replace(on: req.db)
return mannequin
}
func delete(_ id: Todo.IDValue) async throws {
strive await delete([id])
}
func delete(_ ids: [Todo.IDValue]) async throws {
strive 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 need to use an occasion of the repository to change your database rows. The repository will be hooked as much as the Request object by utilizing a standard sample. The most straightforward manner 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 underneath the Request object, I imply, when you have a lot of repositories this generally is a drawback, however first let me present you how one can refactor the controller by utilizing this straightforward methodology. 🤓
import Vapor
struct TodoController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
let todos = routes.grouped("todos")
todos.get(use: index)
todos.submit(use: create)
todos.group(":todoID") { todo in
todo.delete(use: delete)
}
}
func index(req: Request) async throws -> [Todo] {
strive await req.todo.record()
}
func create(req: Request) async throws -> Todo {
let todo = strive req.content material.decode(Todo.self)
return strive 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)
}
strive await req.todo.delete(id)
return .okay
}
}
As you’ll be able to see this manner we had been capable of eradicate the Fluent dependency from the controller, and we will merely name the suitable methodology utilizing the repository occasion. Nonetheless if you wish to unit check the controller it’s not potential to mock the repository, so we now have to determine one thing about that challenge. 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 record() 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 Software
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 of 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
primarily based on the identifier. In fact this complete ceremony will be prevented, however I wished to provide you with a generic resolution to retailer repositories underneath the req.repository
namespace. 😅
public struct RepositoryId: Hashable, Codable {
public let string: String
public init(_ string: String) {
self.string = string
}
}
public last class RepositoryRegistry {
personal let app: Software
personal var builders: [RepositoryId: ((Request) -> Repository)]
fileprivate init(_ app: Software) {
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 {
personal var registry: RepositoryRegistry
personal 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 Software {
personal 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 must provide you with a brand new distinctive identifier and prolong the RepositoryFactory along with your getter in your personal repository sort.
public extension RepositoryId {
static let todo = RepositoryId("todo")
}
public extension RepositoryFactory {
var todo: TodoRepository {
guard let consequence = make(.todo) as? TodoRepository else {
fatalError("Todo repository just isn't configured")
}
return consequence
}
}
We are able to now register the FluentTodoRepository
object, we simply must rename the unique TodoRepository struct and conform to the protocol as a substitute.
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.submit(use: create)
todos.group(":todoID") { todo in
todo.delete(use: delete)
}
}
func index(req: Request) async throws -> [Todo] {
strive await req.repositories.todo.record()
}
func create(req: Request) async throws -> Todo {
let todo = strive req.content material.decode(Todo.self)
return strive 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)
}
strive await req.repositories.todo.delete(id)
return .okay
}
}
The most effective a part of this strategy is that you could 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 underneath the repositories key.
You may provide you with a generic DatabaseRepository
protocol with an related database Mannequin sort, then you possibly can implement some primary options as a protocol extension for the Fluent fashions. I am utilizing this strategy and I am fairly pleased with it thus far, what do you suppose? Ought to the Vapor core workforce add higher help for repositories? Let me know on Twitter. ☺️
[ad_2]