Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to resolve multiple fields in one function? #29

Open
singingwolfboy opened this issue Jun 13, 2018 · 5 comments
Open

Is it possible to resolve multiple fields in one function? #29

singingwolfboy opened this issue Jun 13, 2018 · 5 comments

Comments

@singingwolfboy
Copy link
Contributor

Is it possible to resolve multiple fields at once in a single custom resolver function? I realize this is an unusual request, so let me explain my use-case.

I want to create an application that can track changes over time, and respond to date-based queries. For example, lets say I want to track how people move over time. I want to allow queries that look like this:

query {
  people {
    name
    homeAddress
  }
}

Then, I could specify a date in a HTTP header, include that value in the context object, and use that date when making queries. The entities look like this:

@Entity()
export class Person extends BaseEntity {
  @PrimaryGeneratedColumn() id: number;

  // these are all saved in the PersonKVString model
  name: string;
  homeAddress: string;

  @OneToMany(type => PersonKVString, kvs => kvs.person)
  personKVStrings: PersonKVString[];
}
@Entity()
export class PersonKVString extends BaseEntity {
  @PrimaryGeneratedColumn() id: number;

  @ManyToOne(type => Person, person => person.personKVStrings)
  person: Person;

  @Column() key: string;
  @Column() value: string;

  @Column({ name: "start_on", nullable: true })
  startOn: Date;
  @Column({ name: "end_on", nullable: true })
  endOn: Date;
}

As you can see, the PersonKVString entity holds a key-value pairing, along with a time duration that the key-value pairing is valid. I currently have a custom resolver that looks like this:

const resolverQuery = (
  key: string,
  people: Person[],
  date: string,
  entityManager: EntityManager
): Promise<string[]> => {
  const personIds = people.map(person => person.id);
  return entityManager
    .createQueryBuilder(PersonKVString, "kvs")
    .innerJoinAndSelect(
      "kvs.person",
      "person",
      "person.id IN (:...personIds) AND COALESCE(kvs.start_on, '-infinity') <= :date AND COALESCE(kvs.end_on, 'infinity') >= :date",
      { personIds, date }
    )
    .where(`kvs.key = '${key}'`)
    .orderBy("kvs.start_on", "DESC", "NULLS LAST")
    .getMany()
    .then(ary_kvs => {
      return people.map(person => {
        const kvs = ary_kvs.find(kvs => kvs.person.id === person.id);
        return kvs ? kvs.value : null;
      });
    });
};

@Resolver(Person)
export class PersonResolver implements ResolverInterface<Person> {
  constructor(private entityManager: EntityManager) {}

  @Resolve()
  name(people: Person[], args: any, context: any) {
    const date = context.date || "today";
    return resolverQuery("name", people, date, this.entityManager);
  }

  @Resolve()
  homeAddress(people: Person[], args: any, context: any) {
    const date = context.date || "today";
    return resolverQuery("homeAddress", people, date, this.entityManager);
  }
}

This works, but as you can see, it generates one complex resolver query for every field that the client requests. Ideally, I'd like to generate only one query, which fetches the PersonKVString entities for every field requested. I figured that I might be able to do that, if I could write one resolver that handles every requested field at once.

Any thoughts? Am I solving this problem the wrong way?

@pleerock
Copy link
Contributor

I think you can use lazy initialization pattern and store your loaded data inside resolver class once it requested and reuse this data in any resolver method.

Since resolvers are services, and services are scoped (scoped to request) this "cached" data inside resolver class will persist during a single user request.

@singingwolfboy
Copy link
Contributor Author

singingwolfboy commented Jun 13, 2018

@pleerock: Thanks, but that doesn't actually solve the problem. Rather than one database query per field, I want to make one database query that will return all fields that the GraphQL query requested. Caching the data doesn't solve this problem, it just means that every field won't be queried more than once.

@pleerock
Copy link
Contributor

I want to make one database query that will return all fields that the GraphQL query requested.

by "all fields" do you mean data from multiple tables?

@singingwolfboy
Copy link
Contributor Author

When I run with ORM logging turned on, and I send this query:

query {
  people {
    name
    homeAddress
  }
}

I get this result:

query: SELECT "Person"."id" AS "Person_id" FROM "person" "Person"
query: SELECT "kvs"."id" AS "kvs_id", "kvs"."key" AS "kvs_key", "kvs"."value" AS "kvs_value", "kvs"."start_on" AS "kvs_start_on", "kvs"."end_on" AS "kvs_end_on", "kvs"."personId" AS "kvs_personId", "person"."id" AS "person_id" FROM "person_kv_string" "kvs" INNER JOIN "person" "person" ON "person"."id"="kvs"."personId" AND ("person"."id" IN ($1) AND COALESCE(kvs.start_on, '-infinity') <= $2 AND COALESCE(kvs.end_on, 'infinity') >= $3) WHERE "kvs"."key" = 'name' ORDER BY kvs.start_on DESC NULLS LAST -- PARAMETERS: [1,"today","today"]
query: SELECT "kvs"."id" AS "kvs_id", "kvs"."key" AS "kvs_key", "kvs"."value" AS "kvs_value", "kvs"."start_on" AS "kvs_start_on", "kvs"."end_on" AS "kvs_end_on", "kvs"."personId" AS "kvs_personId", "person"."id" AS "person_id" FROM "person_kv_string" "kvs" INNER JOIN "person" "person" ON "person"."id"="kvs"."personId" AND ("person"."id" IN ($1) AND COALESCE(kvs.start_on, '-infinity') <= $2 AND COALESCE(kvs.end_on, 'infinity') >= $3) WHERE "kvs"."key" = 'homeAddress' ORDER BY kvs.start_on DESC NULLS LAST -- PARAMETERS: [1,"today","today"]

Notice how the database query for fetching PersonKVString is being executed twice -- once for name and once for homeAddress. Ideally, it should be executed only once, and WHERE "kvs"."key" = 'name' would be replaced with WHERE "kvs"."key" IN ('name', 'homeAddress'). Does that make sense?

@singingwolfboy
Copy link
Contributor Author

Is it possible to write a custom resolver for an entity, like my Person entity, instead of for a field on that entity? That would solve this problem, as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants