Search code examples
djangovue.jsgraphqlapollo-clientgraphene-django

Cache update in a many 2 many relation


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


Solution

  • 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.