When $class is required and why is it sometimes not required for 'identical' objects?

I'm having issues getting some Fluent transactions working. I have a number of objects, but the screenshot below references 3 - the two on the right windows are as far as I can tell, identical (at least as far as what I think matters) the one on the left window is referencing them through an optional parent relationship.

Where I'm getting confused, is some objects require the $class.id and some want just class.id - and I can't figure out the pattern for that. This may and may not what is causing my update/insert issues but I want to better understand this to rule it out before addressing that next.

I don't know the Fluent SDK in detail, but I believe you may have misunderstood what the $ prefix there actually is. It's basically a shorthand for the projectedValue of a property-wrapped... er, property.
What that concretely is depends on how the property wrapper is implemented. SwiftUI's @State property wrapper, for example, gives you a Binding, I assume Fluent does something similar to connect objects in a meaningful way.

Your screenshots do not show it, but taking the left one as an example tells me that your User type apparently has a property named status that is wrapped in some property wrapper. As a result, the User type then basically also gets a property named $status, and that is the one whose id property then gets assigned a value in line 100. Fluent then probably relays that somehow to the actual status value in some meaningful way.
This also means that the id you assign to is not the property you see in your UserStatus class there. It is a property of the type of the projected value of the property wrapper instance. However, that seems to match the type updates.status?.id gives you, whether that's correct or not.
In line 112, apparently, these do not match for some reason, so the fix-it suggest you to use the actual property (and not the wrapper) as the compiler sees that that one has a matching type, I guess?

It's a bit late here, so I am not 100 % sure I haven't overlooked something obvious, but I wanted to at least let you know you're dealing with apples and pears here... :slight_smile:

Thanks for the reply - I don't actually disagree with you on the apples v pears but I can't find it - everything I can see points to apples v apples - except the behavior.

You are correct that the left column is trying to set variables on the user object, definition to follow. As far as I can tell, all the property wrappers are the same - in fact most of them were cut and pasted from 1 or 2 sources, with the ID field being the same across all objects and it is almost if not always an @OptionalParent object that I'm trying to access the ID property on.

I'm still rather new, so no doubt I'm dealing with a PEBCAK error, but I just can't find it the differences, and I usually do so well with those Highlights magazines!

Ultimately I have determined that the save/update issues are when translating from a DTO to the model - DTO has a valid ID but it gets lost in translation to the model and therefore not saved. My best guess (without opening a whole other topic inside of this one) is that I'm not assigning it correctly, likely due to these inconsistencies.

I just found one - OptionalParent vs OptionalField for gender - I'm not actively using that field yet but that could be an issue. I've been testing with owner and measurementSystem and those are both OptionalParent.

Okay, I looked into fluent a little bit and regarding the projectedValue it's easy: The property wrappers simply return self for that, so e.g. $gender in your case returns a value type OptionalField<Gender>, which is a typealias for OptionalFieldProperty<User, Gender>, I believe...
This does not look right to me, as Gender is a model itself, you probably want to have a relation between those. OptionalField is supposed to hold actual values from the database's table, not data that is actually rows from another table. Relations in fluent are expressed by a group of property wrappers, as I see it: Parent<To>, OptionalParent<To>, OptionalChild<To>, Children<To>, and Siblings<To, Through>. See here and here.

On that note, it also strikes me as odd that User has two (optional) parents, could it be you're misunderstanding the property wrapper? @OptionalParent(key: "status") var status: UserStatus? does not mean that a User is the parent of UserStatus, it's the other way around. But you don't want the UserStatus to be the parent of the User, right? At least the definition of UserStatus has the User defined as its parent, but you can't really have both be parents...

You most likely want to keep UserStatus to have the @OptionalParent wrapper (maybe even not as optional? Does it even make sense to have a UserStatus without a User?). User then needs to have a property wrapped in @OptionalChild(for:).
This is also a great example of the difference between properties and their wrappers, as @OptionalChild(for:) expects not a Model conforming type as for parameter, but the @<Optional>Parent property wrapper itself. That makes sense, because the child isn't just interested in what type its parent has, but also which relation the parent uses to refer to it. Fluent expresses this, apparently, with the wrapper's and not just the property's key path like this: @OptionalChild(for: \.$owner) var status: UserStatus?.
(Side note: the \.$owner is a short form of \UserStatus.$owner which is a way to get a key path, in this case of type KeyPath<UserStatus, OptionalParentProperty<UserStatus, User> ... I know this gets complex, but property wrappers do a lot of work...)

This is becoming pretty long, I hope I'm not over-explaining things here, but I am a) not really familiar with Fluent myself, and b) have the hunch that you may still be missing the necessary understanding of the difference between a property and its wrapper.

As I see it from my glance of the Fluent documentation, the provided property wrappers fall into various categories that express basically the structure of your object graph in relation to how the data is expressed in the database (i.e. the scheme).
Wrappers like @Field denote simple data entries, basically just holding the "table column name" in e.g. an SQL database. @ID is a little special as it provides the primary key, but that's in the end also just a field.

@Parent works differently: In your object graph (i.e. your Swift model type that becomes the "child") you want a concrete object of whatever other model type is appropriate to function as your instances' parent. That is the property itself.
However, the database cannot store that parent "inside" the child (i.e. in a column of the table that holds the other child data). Instead, it actually needs to store a reference, the identifier of the parent. The parent itself then lives in its own table.
Especially when one parent can have multiple children that's important, but even in 1-to-1 relations that's done this way (otherwise you'd just have one big table in the end).
This allows Fluent to read a child's parent by simply looking into that other table (the one holding all existing parent values) and select the one with the correct identifier (that it gets from the child itself).

This is why you have to use $...: The property wrapper provides you with "access to the relation itself" (which is, as said, basically stored in the child) when you want to "set the parent". You're not setting the identifier on the parent, you're actually setting something on yourself ("inside your child"), at least from the database's point of view. And you set that "thing" to the parent ('s identifier).
The property (i.e. your variable without the $) then becomes the parent "magically" (Fluent takes care of that)[1].

The @Child is even weirder, in some sense. You must give that property wrapper the information about the parent-relation, i.e. the relevant @Parent property wrapper defined in the child's class.
This is because in the database nothing is actually stored in the parent's table itself. Instead, if you assign a child instance to the wrapped property in a parent instance, even though this assignment you mutate the parent, the database instead requires you to mutate the child's row: You write the parent's identifier in the according field. And that was, as described above defined via the @Parent-wrapped property.


All in all I think you may want to refactor your data model in general. Specifically it strikes me as odd to have multiple parents, but in addition it seems fishy to change an existing instance's id in a merge function like in your first screenshot.
While you may change an instance's parent or child, I assume doing so directly via the identifier happens mostly in an init method, at least you have to be certain the new identifier exists.

Oh, and one last remark in case you didn't know: Instead of screenshots, you can post code snippets with by wrapping them in two ` (that's a backtick) or even as blocks with triple ```. :smiley:


  1. Well, at least once everything is loaded, I haven't checked how exactly Fluent does that â†Šī¸Ž

Geno,

Thanks for all your research and help. Your first reply actually got me pointed in the right? direction and your follow up provides a lot of useful info as well.

I ended up finding that I had one of the references set as an @OptionalField instead of @OptionalParent. This combined with misuse of $ and ? I think I finally have it resolved.

FYI: I went with the screenshot to help show the error as well as side by side comparison, but yes normally I agree code formatting blocks are better.

All of the Fluent/Vapor docs were not incorrect, but were for a Model/Controller concept and didn't provide any examples for having a DTO in the middle.

Below is what I ended up finding that seems to work reliably after changing the incorrect property wrapper of Field to Parent.

Abridged Code:

final class StorageLocation: Model, @unchecked Sendable {
    
    static let schema = "cfg_storage_locations"
    
    @ID var id: UUID?
    @Field(key: "name") var name: String?
    @OptionalField(key: "description") var description: String?
    @OptionalField(key: "notes") var notes: String?
    @OptionalParent(key: "owner") var owner: User?
    @Field(key: "is_system") var isSystem: Bool?

init(id: UUID?,name: String?,description: String?,notes: String?,owner: User.IDValue?,isSystem: Bool?) {
        self.id = id
        self.name = name
        self.description = description
        self.notes = notes
        self.owner?.id = owner
        self.isSystem = isSystem ?? false
    }
    
    func toDTO() -> StorageLocationDTO {
        .init(
            id: self.id,
            name: self.name,
            description: self.description,
            notes: self.notes,
            owner: self.$owner.id,
            isSystem: self.isSystem,

extension StorageLocation {
    func mergeChanges(updates: StorageLocation) {
        name = updates.name ?? name
        description = updates.description ?? description
        notes = updates.notes ?? notes
        $owner.id = updates.$owner.id ?? $owner.id
        isSystem = updates.isSystem ?? isSystem
    }
=========
struct StorageLocationDTO: Content {
    var id: UUID?
    var name: String?
    var description: String?
    var notes: String?
    var owner: User.IDValue?
    var isSystem: Bool?
    
    func toModel() -> StorageLocation {
        let model = StorageLocation()
        
        model.id = self.id
        
        if let name = self.name {
            model.name = name
        }
        if let description = self.description {
            model.description = description
        }
        if let owner = self.owner {
            model.owner?.id = owner
        }
=========
struct StorageLocationController: RouteCollection {

    func update(req: Request) async throws -> StorageLocationDTO {
        guard let type = try await StorageLocation.find(req.parameters.get("recordID"), on: req.db) else {
            throw Abort(.notFound)
        }
        
        let updates = try req.content.decode(StorageLocationDTO.self).toModel()

        type.mergeChanges(updates: updates)

        try await type.save(on: req.db)
        return type.toDTO()

I don't believe that I'm misunderstanding the Parent concept - on the User object, I want to reference multiple different things - the one to many relationship is placed on the child (one OF the many) pointing TO the parent (the one).

No doubt there are ways I will find to improve this (including not having everything optional) but this seems to be a stable and solid starting point.

Just a heads up - if you have optional properties you must use @OptionalField for the property wrapper otherwise bad things :tm: can happen. This is a limitation of Swift's reflection and property wrappers from the time Fluent was created

1 Like

Good call, thanks for the heads up - I had made a couple optional as part of trying to fix the translation/save issue but I'll make sure that they're set with the appropriate wrapper tags and not JUST the ? flag.

I'm happy you can make progress! :smiley:
Property wrappers (and macros) can make these kinds of things very elegant and sleek, but they do come with overhead you need to learn.
In general the concept of Fluent seems very similar to what SwiftData (or CoreData, for that matter) does, but I can see you have more explicit control over when and how objects are loaded from the database when it comes to relations.

Well, but typically one object has only one parent, i.e. the parent is usually the "one" in the one-to-many relationship. And you cannot mark both "points" as parent, that does not make sense.
For me, it always helps a bit to think about how this can actually be stored in the database tables (regardless of whether the underlying DB is relational or not, I'm talking conceptually here): To persist the relation between two things it is sufficient to just store a reference to the other object in one of them. You do that by storing the parent's reference (it's identifier) in a field of the child. Vice versa would work for one-to-one relation, but not for one-to-many: You cannot store the children's references in a single field of the parent, as they're, well, multiple (sometimes people hack around this by encoding the list of child references, but that's not scalable and unclean).
In many-to-many relations you need a separate table then, but that's not the issue here. :grinning_cat:

That is not a problem at all. :smiley:
For fields, you can obviously have as many as you need. For relations, you can also have as many as you need and of course the User can be the parent of many other objects (several one-to-one or one-to-many relations). But that would mean that the @Parent property wrapper is defined on the children. The User model then needs to have either @OptionalChild (one-to-one) or @Children (one-to-many) referring to these children.

This is phrased a little weirdly, so let me reiterate: The relation between two models is also including both parts, you don't just place it on one of them. From what I see in Fluent, this means you have to use the @Children property-wrapper when defining the model of the parent (because "one parent points to many children"). In your case that seems to be the User model, so one of its properties needs to be wrapped with that.
@<Optional>Parent is then used on the models of which you have many (because "each of the many children points to one parent", you kind of read the "one-to-many" monicker backwards then).
The relationship is only defined once BOTH of these things have been done. One of the involved property wrappers alone does not properly define it and will likely lead to problems at runtime.
And this also means you (most likely) cannot use any other property wrapper (like the @OptionalField ones you seem to have used) to "model the many side".
It does not make much sense to say "I put the relationship on one of the involved models", a property wrapper alone does not constitute one.


Lastly, I am pretty sure your latest code snippet does not do what you think it does. In your StorageLocation's init, you pass an owner parameter and the type tells me that is the id of an already existing User instance and you want to be the parent of the StorageLocation your initializing. However, then you assign that owner identifier to the id property of the owner property of your instance. First, that only works since every Model in Fluent needs to have an empty initializer and your owner property is nil by default. Otherwise you'd get a "self used before all properties have been initialized" error. Second, since owner (the property, not the parameter) is as said nil, you're not establishing the parent-child relationship between these instances. Even if owner were not nil, but a valid User instance, you would just change that instance's id property, potentially changing the value of an unrelated object.
What you will want to do is write something like owner.map { self.$owner.id = $0 }. The naming is a little unfortunate, basically this means "if my owner parameter, an id, is not nil, set this to the id property of the relation".
Once Fluent does its magic, this results in the owner property itself become the instance you originally got that identifier from. This is what the property wrapper itself is supposed to do with its id field: This denotes "which User should be set to the actual property".

In the toDTO function this might actually not make a difference, assuming the objects are all loaded from the database at the point of calling. After all $owner.id holds the id of the instance that ultimately owner should have.
In the toModel function, however, you make the same mistake: model has just been created, so its owner is nil. So even if the other owner (the one in StorageLocationDTO that has type User.IDValue?) is not nil, the model.owner?.id optional chaining resolves to nil and does nothing, so you won't set that id (as there's nothing to set it to).
Instead, you want to again do model.$owner.id = owner (and perhaps change the names a bit to distinguish between ids and entire objects...). This tells fluent that this StorageLocation object's owner should be the User with the correct id (to be loaded from the database).

Oh, yes, I had overlooked that, but was assuming this is the entire point of having variants where it makes sense. :smiley: