2

I'm using a combination of Django, GraphQL (graphene-django), VueJS and Apollo Client for my project. Everything is working fine until I tried to manage many2many relations. Usually when I make an update mutation to an object that is already in the cache, it is detected and the data that is displayed is updated. The problem is that in my new case, I'm updating the many2many relation between my two objects and it doesn't update, while it is correctly done in database (and even in cache, when I print it in the console, which is weird). Typically, I'm trying to add/remove users in/from a group. Maybe I've been watching my code for a too long period and I can't see something obvious.. Anyway here is my code :

My Django models :

class Group(models.Model):

  def _get_users_count(self):
      return self.user_ids.count()

  name = models.CharField(max_length=255)
  user_ids = models.ManyToManyField(User, related_name="group_ids", db_table="base_group_user")
  users_count = property(fget=_get_users_count, doc="type: Integer")

  def __str__(self):
      return self.name

class User(AbstractUser):

  def _get_full_name(self):
      return "%s %s" % (self.first_name, self.last_name.upper())

  full_name = property(fget=_get_full_name, doc="type: String")

  def __str__(self):
      return self.full_name

  def _has_group(self, group_name):
      return self.group_ids.filter(name=group_name).exists()

My schemas :

class GroupType(DjangoObjectType):

  class Meta:
      model = Group
      fields = ('id', 'name', 'user_ids',)

  users_count = graphene.Int()

class UserType(DjangoObjectType):

  class Meta:
      model = User
      fields = (
          'id',
          'username',
          'password',
          'first_name',
          'last_name',
          'is_active',
          'group_ids',
      )

  full_name = graphene.String()

My mutators :

class AddUserInGroup(graphene.Mutation):
  id = graphene.ID()
  name = graphene.String()

  class Arguments:
    group_id = graphene.ID()
    user_id = graphene.ID()

  class Meta:
    output = GroupType

  def mutate(self, info, **kwargs):
      group = get_object_or_404(Group, id=kwargs['group_id'])
      user = get_object_or_404(User, id=kwargs['user_id'])
      group.user_ids.add(user)
      return group


class RemoveUserFromGroup(graphene.Mutation):
  id = graphene.ID()
  name = graphene.String()

  class Arguments:
    group_id = graphene.ID()
    user_id = graphene.ID()

  class Meta:
    output = GroupType

  def mutate(self, info, **kwargs):
    group = get_object_or_404(Group, id=kwargs['group_id'])
    user = get_object_or_404(User, id=kwargs['user_id'])
    group.user_ids.remove(user)
    return group

My js consts (gql queries) :

const ADD_USER_IN_GROUP_MUTATION = gql`
  mutation addUserInGroupMutation ($groupId: ID!, $userId: ID!) {
    addUserInGroup(
      groupId: $groupId,
      userId: $userId
    ) {
      id
      name
      usersCount
      userIds {
        id
        username
      }
    }
  }
`

const REMOVE_USER_FROM_GROUP_MUTATION = gql`
  mutation remoteUserFromGroupMutation ($groupId: ID!, $userId: ID!) {
    removeUserFromGroup(
      groupId: $groupId,
      userId: $userId
    ) {
      id
      name
      usersCount
      userIds {
        id
        username
      }
    }
  }
`

const GROUPS_QUERY = gql`
  query {
    groups {
      id
      name
      usersCount
      userIds {
        id
        username
      }
    }
  }
`

My template (sample of) : These are 2 columns. When I remove a user, it is supposed to be removed from the left column and it should appear in users that are not in the group (right column) and when I add a user it should go from the right column to the left one.

     <b-tab>
      <template v-slot:title>
        Users
        <b-badge class="ml-2">{{ group.usersCount }}</b-badge>
      </template>
      <b-row>
        <b-col>
          <b-table :items="group.userIds" :fields="table_fields_users_in">
            <template v-slot:cell(actionOut)="row">
              <b-button @click="removeUserFromGroup(row.item)" size="sm" variant="danger" title="Remove">
                <font-awesome-icon :icon="['fas', 'minus-circle']"/>
              </b-button>
            </template>
          </b-table>
        </b-col>
        <b-col>
          <b-table :items="users" :fields="table_fields_users_out">
            <template v-slot:cell(actionIn)="row">
              <b-button @click="addUserInGroup(row.item)" size="sm" variant="success" title="Add">
                <font-awesome-icon :icon="['fas', 'plus-circle']"/>
              </b-button>
            </template>
          </b-table>
        </b-col>
      </b-row>
    </b-tab>

My datas : (so that you can understand where they come from). Note that I should refine 'users' to users that are not yet / no longer in the group.

props: {
 group: Object,
 editing: {
   type: Boolean,
   default: false
 }
},
apollo: { users: USERS_QUERY },

and finally my Methods :

addUserInGroup (user) {
  this.$apollo.mutate({
    mutation: ADD_USER_IN_GROUP_MUTATION,
    variables: {
      groupId: this.group.id,
      userId: user.id
    },
    update: (cache, { data: { addUserInGroup } }) => {
      console.log('cache : ') // currently used for debugging, will be removed
      console.log(cache) // currently used for debugging, will be removed
      console.log('query answer: ') // currently used for debugging, will be removed
      console.log(addUserInGroup) // currently used for debugging, will be removed
    }
  })
  console.log('Added ' + user.username + ' to ' + this.group.name + ' group.')
},
removeUserFromGroup (user) {
  this.$apollo.mutate({
    mutation: REMOVE_USER_FROM_GROUP_MUTATION,
    variables: {
      groupId: this.group.id,
      userId: user.id
    },
    update: (cache, { data: { removeUserFromGroup } }) => {
      console.log('cache : ') // currently used for debugging, will be removed
      console.log(cache)  // currently used for debugging, will be removed
      console.log('query answer : ')  // currently used for debugging, will be removed
      console.log(removeUserFromGroup)  // currently used for debugging, will be removed
    }
  })
  console.log('Removed ' + user.username + ' from ' + this.group.name + ' group.')
}

If you guys need more code (some parts I would have forgotten), do not hesitate to tell me and I'll provide it.

A sample of what I'm doing (so that you'll notice that my demo datas are literally made by smashing my keyboard) :

enter image description here

lbris
  • 1,068
  • 11
  • 34
  • if cache modified the problem is with component/view updating - force query refetch/read from cache? - don't know how in vue - I'm working with react – xadm Jan 27 '20 at 11:39
  • I thought when updating an object of known type and existing ID (that's what default cache mechanism is based on here, and it is working for simple model without m2m relation management), it would update by itself. I'll search deeper about a way to re-render manually, even if I'd prefer avoiding doing this. I'll keep you up-to-date with my research. – lbris Jan 27 '20 at 13:16
  • pagination/fetch more examples should contain this mechanism – xadm Jan 27 '20 at 13:26
  • You are right that the cache should be updated automatically in this case. What is the query you are using to fetch the groups initially? – Daniel Rearden Jan 27 '20 at 14:02
  • I added the initial query in the js consts part `GROUPS_QUERY`. Also I found this https://vuejs.org/v2/guide/list.html#Caveats which may explain why it does not update. – lbris Jan 27 '20 at 14:37
  • Hmm that looks right. So there's a handful of potential causes I can think of. If you're not using a smart query and only fetching the initial data once. If the cache keys don't match up -- if either query or mutation was missing the `id` field or each one was returning a different `__typename` for the group object. If you've set `addTypename` to false for your InMemoryCache instance. If you've misconfigured your `dataIdFromObject` function. – Daniel Rearden Jan 27 '20 at 14:42
  • You might also utilize the Apollo dev tools to inspect your cache and see if at least the cache itself is getting updated. If you can see the updates to your cache in the dev tools, then that points to some issue with your component with the query. – Daniel Rearden Jan 27 '20 at 14:44
  • I'm aware of these options to customize the ID that is used by Apollo and I didn't change them. I even made mutations to return always the same __typename, otherwise it used the class name as its __typename and nothing got updated at all (when I started the project). In the update:` part of mutations I print the cache in console to see what's in and I do see added/removed users movements. It is really the array `userIds` containing the users in the `group` that does not update automatically. Could the reason come from the link I gave 3 comments above ? – lbris Jan 27 '20 at 14:48
  • By the way my `usersCount` property is also updated in cache but not in the view whereas it's not an array (if we consider the link I gave again), which may exclude the reason I mentionned. – lbris Jan 27 '20 at 14:54
  • how `groups` and `group` are "propagated" ? I guess template doesn't work on separate `group` query then not directly connected to apollo cache – xadm Jan 27 '20 at 18:22
  • In this case there is no `groups` but only `group` since it is a form view of one group. And `group` comes from a prop. Typically, I'm in a list view of several groups, and when I click on one of them, I arrive on my form view of the clicked group. My list view and my form view are two different components and I give the clicked group to the form component through its props. – lbris Jan 28 '20 at 07:11
  • this probably breaks some observability chain/vue magic - use query for single group form, too – xadm Jan 28 '20 at 09:49
  • You mean that I should only give the id of the group via the prop, and then retrieving my group with a query that takes the id of that group to retrieve only this one. That might work, I'll try and let you know. The drawback is that it does 1 more query, instead what I already have in cache. – lbris Jan 28 '20 at 09:58
  • 1
    you can query cache only – xadm Jan 28 '20 at 12:56
  • Finally I ended up by letting it as it is and since cache was up-to-date, I simply splice/push from/to the arrays I'm using in my b-tables into the `update` method hook of my mutations and it does update the view as desired. – lbris Jan 30 '20 at 11:14

1 Answers1

0

Finally for my use case, I noticed that since there are lots of potential changes to data (in database), cache is not the most relevant or the most trustable source of data. Therefore I changed my default apollo fetchPolicy to 'network-only'.

const apolloProvider = new VueApollo({
  defaultClient: apolloClient,
  defaultOptions: {
    $query: {
      fetchPolicy: 'network-only'
    }
  }
})

As documented HERE and explained HERE.

So that each time I mount a component that has a apollo statement, it fetches the query from the network (from database). The only remaining moment when I have to do cache changes is when I want the current rendered component to dynamically update some elements.

lbris
  • 1,068
  • 11
  • 34