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) :
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.