fluent icon indicating copy to clipboard operation
fluent copied to clipboard

Populate a parent-child relationship after initial query

Open AndrewBoryk opened this issue 10 months ago • 12 comments

Please summarize your feature request

Being able to populate a parent-child relationship on a record that was already previously queried

Describe the functionality you're seeking in more detail

I want to avoid populating all the fields on a record every single time that I am fetching it. In addition, sometimes I'd like to be able to just use the User that I get from require during authentication.

The issue is, that if I have a parent-child (or sibling) relationship to that record, it isn't loaded up when I use that require.

So the alternative I have is getting the id from that User, and then querying for that user again to get it populated.

Is there a functionality for this already? If not, it would be nice to populate as needed on a record via query after it's initiatial query

Have you considered any alternatives?

No response

AndrewBoryk avatar Jan 19 '25 21:01 AndrewBoryk

All relation properties (@Parent, @Children, etc.) have a .get() method which loads that relation from the database. Example:

final class User: Model, @unchecked Sendable {
    static let schema = "users"

    @ID
    var id: UUID?

    @Parent(key: "something_id")
    var something: Something
    
    init() {}
}

func someRoute(_ req: Request) async throws -> String {
    let user = try req.auth.require(User.self)
    
    // For convenience, the `get()` method returns the value of the loaded relation;
    // it can also be accessed via the `user.something` property.
    try await user.$something.get(on: req.db)
}

gwynne avatar Jan 19 '25 21:01 gwynne

@gwynne Gotcha, that returns the value of the loaded relation, if it is already loaded. But if we want to populate that value, that would work? Or do I need to include the reload parameter? I will test it out, thanks for sharing tho

AndrewBoryk avatar Jan 23 '25 00:01 AndrewBoryk

It will always load and populate the relation if it is not already loaded.

If reload is set to true, it will load and populate the relation even if it has been loaded already.

gwynne avatar Jan 23 '25 04:01 gwynne

    // For convenience, the `get()` method returns the value of the loaded relation;
    // it can also be accessed via the `user.something` property.
    try await user.$something.get(on: req.db)

When can the value be accessed via the property ? Presumably only after a call to get() has been made ?

duncangroenewald avatar Feb 21 '25 20:02 duncangroenewald

Assuming the property had not previously been loaded, yes, that is correct.

gwynne avatar Feb 21 '25 21:02 gwynne

@gwynne - I have another question related to updating these Fluent models.

Using the example above let's say I want to change the parent "something" - I would have expected that one could simple do the following:

user.something = newSomething

try await user.save(on: database)

but apparently this doesn't work and one needs to set the id like so

user.$something.id = newSomething.id!
try await user.save(on: database)

// AND THEN SEPARATELY set the object
user.$something.value = newSomething

// OR fetch the property

try await user.$something.get(reload: true, on: database)

Am I missing something here or is there an easier way? Bear in mind I am trying to use FLUENT with SwiftUI so all my objects need to be updated on the Main thread. I am also creating the UUID() because I couldn't seem to access the ID of newly created objects using save(on:) - although I am a little hazy now on exactly what the issue was.

Currently I do it like so

Task {
   // May have been created previously but here for illustration (user created IDs)
   let newSomething = Something(id: UUID(), name: "Name",...)
   
   try await newSomething.create(on: database)  
   // save(on:) seems to behave a little different but haven't gotten to the bottom of that yet

  ...other stuff 

   // and much later lets change the something on the user to the newSomething (say from a UI pick list action)

   user.$something.id = newSomething.id!
   try await user.save(on: database)

   // AND THEN SEPARATELY set the object on the main thread
   await MainActor.run {
   user.objectWillChange.send()
   user.$something.value = newSomething
   }
 
}

duncangroenewald avatar Mar 22 '25 00:03 duncangroenewald

Similarly it seems one has to jump through a few hoops to load relationships on related objects.

            // Can't use this because there is no .with() option to eager load relationships on the fetched objects
            //let _ = try await invoice.$transactions.get(on: self.database)
            
            // When we use this the relationships are not populated on the parent so can't be accessed
            let trx = try await invoice.$transactions.query(on: self.database).with(\.$account).all()
           
            await MainActor.run {
                invoice.objectWillChange.send()
                invoice.$transactions.value = trx   // we manually !? attach the related objects
            }

Am I missing something or is there an easier way to achieve this ?

duncangroenewald avatar Mar 22 '25 00:03 duncangroenewald

Am I missing something or is there an easier way to achieve this ?

Currently there's no way to eager load relations on the relations helper and set the properties on a higher level property.

And saving a model with relations set doesn't work either, mainly due to restrictions in how property wrappers work

0xTim avatar Mar 26 '25 16:03 0xTim

@0xTim - another question for you as I think I have completely misunderstood Fluent.

Say I have a parent child model something like this

/// Represents an invoice, bill or credit note
final class Invoice: Model, @unchecked Sendable, ObservableObject, Hashable {
    // Name of the table or collection.
    static let schema = "invoices"
    
    // Unique identifier for this Assortment.
    @ID(key: .id)
    var id: UUID?
    
    @Field(key: "number")
    var number: Int
    
    
    @Field(key: "description")
    var descriptionText: String
    
    @Children(for: \.$invoice)
    var transactions: [Transaction]

   ...

}

final class Transaction: Model, @unchecked Sendable, ObservableObject, Hashable {
    // Name of the table or collection.
    static let schema = "transactions"
    
    // Unique identifier for this Assortment.
    @ID(key: .id)
    var id: UUID?
    
    @Parent(key: "invoice_id")
    var invoice: Invoice
    
    @Field(key: "number")
    var number: Int
    
    @Field(key: "date")
    var date: Date
    
    @Field(key: "description")
    var descriptionText: String
    

}

I was expecting that is I fetch the Invoice with transactions e.g.

let invoices = Invoice.query(on: database).with(.$transactions).all()

that I would use invoice.transactions to access the transactions but this is not the case. One has to use the invoice.$transactions.get() function.

Either way once cannot use the transaction.invoice property to access the parent invoice of the transactions.

It seems like Fluent is really just providing an easy way of generating SQL - it is not populating any of the model relationships automatically.

It's not really clear from the documentation - seems a bit strange given these relationships are being used to fetch related objects that one wouldn't automatically propagate the references to the related objects once they have been fetched.

Is there any overview document that outlines what is 'missing' from Fluent in order for it to be a more complete ORM - it would be useful for anyone considering using Fluent and help avoid a lot of 'discovery'.

Is there any plan to develop some of these capabilities ?

Thanks

duncangroenewald avatar Mar 30 '25 09:03 duncangroenewald

@duncangroenewald If you eager load the relations you can query them directly. That should work

0xTim avatar Mar 30 '25 09:03 0xTim

@duncangroenewald If you eager load the relations you can query them directly. That should work

Is this not eager loading ?

let invoices = Invoice.query(on: database).with(\.$transactions).all()

If so then transaction.invoice is not populated.

So one would need to do this

let invoices = Invoice.query(on: database).with(\.$transactions) { transaction in transaction.with(\.$invoice) }.all()

However in this case transaction.invoice returns a different object to the original invoice - Fluent will create a whole new copy of the same Invoice.

Happy to stand corrected on this but we really need the transaction.invoice to reference the same parent object.

duncangroenewald avatar Mar 30 '25 19:03 duncangroenewald

Ok I see what you mean - it is eager loaded but eager loading only works one way. transaction.invoice from that original query isn't really possible with Fluent and the current capabilities of reflection in Swift

0xTim avatar Apr 03 '25 16:04 0xTim