#Eagerly Loading Nested Data

1 messages ยท Page 1 of 1 (latest)

ornate scroll
#

Hello, I need help to eagerly-load nested data. This is a simplified version of the models:

final class User: Model, Content, Equatable, Sendable {    
    @ID(key: .id) var id: UUID?
    
    @Children(for: \.$user) var joinedInstitutions: [InstitutionEntity]
}

final class InstitutionEntity: Model, Content, Equatable, Hashable {    
    @ID(key: .id) var id: UUID?
    
    @Parent(key: "institutionID") var institution: Institution
}

final class Institution: Model, Content, Equatable, Hashable, Sendable {    
    @ID(key: .id) var id: UUID?
    
    @Timestamp(key: "created_at", on: .create) var createdAt: Date?
    @Timestamp(key: "updated_at", on: .update) var updatedAt: Date?
}

When I load a User, I'd like to load all its joinedInstitutions, and in turn, each of the joinedInstitution's institution. I have tried the following:

extension Request {
    func loadUserWithACLInfo() async throws -> User {
        /**
        Verify the user has an `id`
        */
        let userID = try auth.require(User.self).requireID()
        
        /**
        Load the user with `joinedInstitutions`
        */
        guard let userWithACLInfo = try await User
            .query(on: db)
            .filter(\.$id == userID)
            .with(\.$joinedInstitutions) { joinedInstitution in
                joinedInstitution.with(\.$institution)
            }
            .first() else {
                throw Abort(.notFound, reason: "User not found")
            }
            
        return userWithACLInfo
    }
}

But I get the following error:

Cannot find 'joinedInstitution' in scope

Any ideas?

#

The following works, but I'm not sure if it's the right way to do it:

extension Request {
    func loadUserWithACLInfo() async throws -> User {
        let userID = try auth.require(User.self).requireID()
        
        guard let userWithACLInfo = try await User
            .query(on: db)
            .filter(\.$id == userID)
            .with(\.$joinedInstitutions)
            .first() else {
                throw Abort(.notFound, reason: "User not found")
            }
            
        // Load `Institution` for each `joinedInstitution` asynchronously
        
        try await withThrowingTaskGroup(of: Void.self) { group in
            for joinedInstitution in userWithACLInfo.joinedInstitutions {
                group.addTask {
                    try await joinedInstitution.$institution.load(on: self.db)
                }
            }
            try await group.waitForAll()
        }
        
        return userWithACLInfo
    }
}
cunning niche
#

It's quite possibly the most painful way possible to do it, but it's not strictly wrong... ๐Ÿ˜… This, however, does the same thing a lot more easily:

extension Request {
    func loadUserWithACLInfo() async throws -> User {
        let userID = try auth.require(User.self).requireID()
        
        guard let userWithACLInfo = try await User
            .query(on: db)
            .filter(\.$id == userID)
            .with(\.$joinedInstitutions) { $0
                 .with(\.$institution)
            }
            .first() else {
                throw Abort(.notFound, reason: "User not found")
            }
        return userWithACLInfo
    }
}

Also, for the record, when "non-eager loading" (e.g. as you were doing), it is strongly recommended to use .get() rather than .load() (the latter is public only due to design flaws in Fluent)

#

You can nest eager loaders in this fashion to unlimited depth, and it still only adds one query per invocation of .with() regardless of nesting. (However it's not recommended to nest more than three or four levels deep, generally speaking.)

ornate scroll
#

Yeah, I'm not happy with my brute-force solution, but I couldn't get past various compiler errors. I tried so many things!

cunning niche
#

I promise Fluent 5 will be much more intuitive in this way as well as all the others ๐Ÿ™‚

ornate scroll
#

The compiler is still not happy...

analog foxBOT
cunning niche
#

Sorry, my syntactical bad!

#

I wrote . both after $0 and before with() ๐Ÿ˜… ๐Ÿคฆโ€โ™€๏ธ

#

Delete either of the .s and it should work

#

Edited my example to be correct ๐Ÿ˜…

ornate scroll
#

I might need more coffee...

cunning niche
#

Huh. That should work. Possibly I'm the one in need of coffee ๐Ÿ˜ฐ One moment...

ornate scroll
#

Adding parentheses makes it happier... almost there.

cunning niche
#

That's not happier, that's just hiding the chaining from the error site ๐Ÿ˜ฐ

ornate scroll
#

๐Ÿ˜‚

cunning niche
#

The syntax is correct - Fluent's own tests show it:

            let galaxies = try await Galaxy.query(on: self.database)
                .with(\.$stars) {
                    $0.with(\.$planets) {
                        $0.with(\.$moons)
                        $0.with(\.$tags)
                    }
                }
                .all()
ornate scroll
#

Darn. I'll try a few more things.

cunning niche
#

What is the complete error message that starts in your screenshot as "Anonymous closure argument not cont..."?

#

OH!!!!

#

@ornate scroll I am an absolute fool, you're correct about parenthesis, just wrong about where to place them.

#

Either this:

        guard let userWithACLInfo = (try await User
            .query(on: db)
            .filter(\.$id == userID)
            .with(\.$joinedInstitutions) { $0
                 .with(\.$institution)
            }
            .first()) else {
                throw Abort(.notFound, reason: "User not found")
            }

or this:

        guard let userWithACLInfo = try await User
            .query(on: db)
            .filter(\.$id == userID)
            .with(\.$joinedInstitutions, { $0
                 .with(\.$institution)
            })
            .first() else {
                throw Abort(.notFound, reason: "User not found")
            }

Trailing closure arguments in conditional clauses aren't things the compiler likes ๐Ÿ˜…

#

My apologies for the ridiculous miscue ๐Ÿ˜†

ornate scroll
#

Aaaaahhh! The first one!

#

No, please! As always, thanks so much for the help.

cunning niche
#

Glad I could help, even if it ended up a little belated as usual! ๐Ÿคฃ ๐Ÿ™‚

wooden heart
#

@ornate scroll @cunning niche in those cases can be useful to split statementes and just execute the query inside control statement. This works since is query builder (builder pattern).

ornate scroll
#

I curently have 69551 lines of code... and it's always the little details ๐Ÿ˜‰

cunning niche
#

The classical builder pattern is probably going away for Fluent 5; a considerable number of people have expressed interest in the result builder pattern (i.e. having queries as an embedded DSL as with LINQ rather than the traditional Java StringBuilder pattern)

#

(If that's how I end up going, however, there will be a compatibility layer to help ease migrating old code to the new version.)

wooden heart