GraphQL and Adonis


#1

Just started playing with GraphQL and Adonis using this package:

Everything works flawlessly :slight_smile: …but…

Too many sql queries hit DB when using graphql logic.

For example:
I have 5 models (home, hood, city, user and tag).
I want to list 10 homes, together with hood, city, user and tag data for each home.

With classic approach:

Home.query()
.with('hood').with('city').with('user').with('tags')
.paginate(1, 10)

Db queries ~5 times thanks to .with method which takes all home ids and queries hoods where in thoose ids…

…and graphQL for same task does about 40!! queries.

Is anyone there having idea how to make smarter resolvers? Something like https://github.com/facebook/dataloader on top of Lucid?


For anyone there interested in my approach (I’m a newbie), here is simplified code of home and hood schema and resolver.

Ignore PageIt helper in code… It’s not important.
This is simplified code, real code contains more schemas and resolvers, but logic is the same…

#app/Schema/Home.graphql
type Query {
    home(id: ID!): Home
    homes(page: Int=1, limit: Int=10): [Home]!
}

type Home {
    id: ID!
    title: String!
    tags(page: Int=1, limit: Int=10): [Tag]!
}
#app/Schema/Hood.graphql
type Query {
    hood(id: ID!): Hood
    hoods(page: Int=1, limit: Int=10): [Hood]!
}

type Hood {
    id: ID!
    title: String!
    homes(page: Int=1, limit: Int=10): [Home]!
}
#app/Resolvers/Home.js
const Helper = use('App/Helpers/Common')
const Home = use('App/Models/Home')

module.exports = {
    Query: {
        async home(root, args, ctx) {
            const home = await Home.find(args.id)

            return home && home.toJSON()
        },

        async homes(root, args, ctx) {
            const pageLimits = Helper.pageIt(args, true)

            const homes = await Home.query()
                .offset(pageLimits.offset)
                .limit(pageLimits.limit)
                .fetch()

            return homes.toJSON()
        }
    },

    Home: {
        async hood(root, args, ctx) {
            const home = new Home()
            home.newUp(root)

            const hood = await home.hood().fetch()
            return hood.toJSON()
        }
    }
}
#app/Resolvers/Hood.js
const Helper = use('App/Helpers/Common')
const Hood = use('App/Models/Hood')

module.exports = {
    Query: {
        async hood(root, args, ctx) {
            const hood = await Hood.find(args.id)

            return hood && hood.toJSON()
        },

        async hoods(root, args, ctx) {
            const pageLimits = Helper.pageIt(args, true)

            const hoods = await Hood.query()
                .offset(pageLimits.offset)
                .limit(pageLimits.limit)
                .fetch()

            return hoods.toJSON()
        }
    },

    Hood: {
        async homes(root, args, ctx) {
            const hood = new Hood()
            hood.newUp(root)

            const pageLimits = Helper.pageIt(args, true)

            const homes = await hood.homes()
                .offset(pageLimits.offset)
                .limit(pageLimits.limit)
                .fetch()

            return homes.toJSON()
        }
    }

}

#2

Hi,

Facebook created dataLoader for this exact thing. Maybe it will solve your problem.


#3

Yeah, I know… that’s why I mentioned dataloader in my post :smiley:

I was wondering if anyone figured out a way to use this together with Adonis/Lucid…


#4

Oh sorry didn’t see it! :man_facepalming:t5:


#5

This is generic problem I see people falling into with GraphQL. I personally stay away from it and never got a chance to work on a project that maturely uses GraphQL.

Maybe someone with relevant experience can shed some light on same


#6

Hey @hlozancic!

This is a known issue when you use GraphQL but there’s easy thing you can do to avoid it. This is pure theory, I haven’t test it myself.

The 4th argument of your resolver is GraphQLResolveInfo, this can help you to know if you are requesting relation or not (that let you eager/lazy load them).

This package is going to evolve to help you handle eager/lazy loading.
I have pushed a commit into the example application to show the issue.

I’ll keep you in touch when the update is release :+1:


#7

Whao! This is awesome! Can’t wait to try new releases :smiley:

I’ll check/test your demo app and think about 4th argument :slight_smile:


#8

Did you forget to push? The last push seems to be from 8 days ago. I am also curious about this.


#9

Nope, working on other things.

Will see if I can push something this weekend :+1:


#10

I am talking about this…


#11

Yeah, I have pushed it some time ago.

This is the commit I’m referring to https://github.com/RomainLanz/adonis-graphql-demo/commit/1f456e95b30b77870b6e59f37bfdccbfab6cccb0

If you run the seed and then requests

query {
  users {
    username
    email
    posts {
      title
    }
  }
}

You’ll see in the console how many queries is used. When eager loading will be handled this number will go down.


#12

I wonder is there any difference between these two?
I like the second one more.

Product: {
    async user (_) {
      try {
        const product = new Product()
        product.newUp(_)
        const user = await product.user().fetch()
        return user.toJSON()
      } catch (error) {
        throw new GraphQLError('Error on fetching user for an product', {
          name: error.name,
          message: error.message
        })
      }
    }
  }
Product: {
    async user ({ user_id }) {
      try {
        const user = await User.findOrFail(user_id)
        return user.toJSON()
      } catch (error) {
        throw new GraphQLError('Error on fetching user for an product', {
          name: error.name,
          message: error.message
        })
      }
    }
  }

Adonis Roadmap?
#13

@hlozancic were you able to reduce number of queries to a reasonable number?


#14

At the end I ditched graphql… Too many problems with performance when used with sql etc.

I use this now https://github.com/rhwilr/adonis-bumblebee and its great.


#15

Any thoughts on this @romain.lanz ?


#16

Any thoughts on what? @peter

You need to be careful about O(n+1) issue with GraphQL, use package like https://github.com/facebook/dataloader to batch the query and do some kind of “eager loading”.


#17

I ment this.


#18

It all depends on you.

I personally prefer doing a simple RPC API than building a GraphQL API.


#19

Any tip on examples for RPC with Adonis?

And what will happen with your project adonis-graphql @romain.lanz ?


#20

This project is good to be used for the moment !

I’ll provide some change to handle more easily some common mistake made with GraphQL (like eager-loading) and maybe see if I can improve it.

Since I’m not a big user of GraphQL I’ll probably need some help from the community! :purple_heart: