Types with no identifiers and data normalization

  1. When defining a type in the GraphQL schema, is it required to have an identifier? For example, in the following type, I don’t have an ID field because Net Worth is not an identifiable entity. So is this a valid type?
type NetWorth {
  investments: Float!
  cash: Float!
}
  1. Assuming that the above type is valid, how does it impact the Apollo cache and data normalization? I assume that NetWorth cannot be normalized because it does not have an ID field. Is this correct? And can Apollo use the cached value in a subsequent query?
1 Like

Hello, great question!

NetWorth is not required to have an id field or any other uniquely identifying field. When you query your server for an object that includes a field of type NetWorth, such as the following…

type User {
  id: ID!
  netWorth: NetWorth!
}

…then Apollo Client caches the NetWorth directly inside the parent object instead of using a reference to a separate cached object:

'User:5': {
  __typename: 'User',
  id: '5',
  netWorth: {
    __typename: 'NetWorth',
    investments: 100.0,
    cash: 50.0
  }
}

Apollo Client can absolutely use the cached value for subsequent queries, but only as it pertains to the parent object. Because it sounds like a NetWorth is directly tied to a particular parent object, this sounds acceptable for your use case.

Thanks @StephenBarlow. I can see how I might attach NetWorth to a user with an id to cache it. That’s an interesting idea. FYI, the way I am currently using it is without attaching it to a user. Here’s my query:

query GetNetWorth($userId: ID!) {
  netWorth(userId: $userId) {
    investments
    cash
  }
}

It seems to work. Could I think of this as a query returning just a scalar? Is this even legal in GraphQL (query returning a scalar)?

FYI, looking at Apollo DevTools, it looks like the client is caching NetWorth based on __typename and the query variable userId. For example, there are two cache entries below for two users:

netWorth({"userId": "u100"}):
  __typename:"NetWorth"
  investments: 10000
  cash: 200

netWorth({"userId": "u200"}):
  __typename:"NetWorth"
  investments: 5000
  cash: 100

To follow up:

  • It is absolutely valid for a top-level field (such as a field of Query) to return a scalar. I see what you mean by NetWorth “effectively” being a scalar, in that it’s a couple of flat values with no object associations. (Although GraphQL does still certainly consider it an object)

  • As you’re observing, by default Apollo Client caches a separate value for a particular field for each unique combination of arguments you provide for that field (you can configure this). In this case, you’re providing two different values for userId, so the results are cached separately. I assume these two values are still direct subfields of the Query object in your cache, however, because Apollo Client still needs an id to actually normalize them.

  • If your GetNetWorth query works for your use case, that’s great! One potential design advantage of making netWorth a field of the User type instead, however, is that it enables you to query for multiple properties of a user with a single operation, like so:

    query GetUserDetails($userId: ID!) {
      user(userId: $userId) {
        id
        firstname
        username
        netWorth {
          investments
          cash
        }
      }
    }
    
1 Like

@StephenBarlow, I really like your last point about treating netWorth as a field of User type. I never thought about designing the schema that way - but I can see the value of this approach. I was thinking of the schema more in “relational” terms so not putting any computed values in there. But I suppose GraphQL does not have any such restriction, so adding computed fields to the object works out very well.

Thanks again for your insights!

1 Like