Agenda
We will write a GraphQL compliant api for a Django project.
We will use two models through this post and expose the data for those models using GraphQL api.
- We will use
graphene
library to create our GraphQL service. - The GraphQL service will interact with Django models.
- We will expose this GraphQL service using Django.
This post builds on our last postwhich gives an introduction to Python graphene.
Setup
This post assumes that you have the project setup as described at https://docs.djangoproject.com/en/2.2/intro/tutorial01/.
You must have a polls
app in your project and the models Question
and Choice
.
# polls/models.py
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
def __str__(self):
return self.question_text
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
def __str__(self):
return self.choice_text
Schema
Create a file called mysite/graphql.py
. This assumes that your project name is mysite
created using django-admin startproject mysite
.
# mysite/graphql.py
from graphene import Schema, ObjectType, Field, String, List, Int
from polls.models import Question
class QuestionType(ObjectType):
question_text = String()
pub_date = String()
def resolve_question_text(question, info):
return question.question_text
def resolve_pub_date(question, info):
return question.pub_date.strftime('%Y-%m-%d')
class Query(ObjectType):
questions = List(QuestionType)
question = Field(QuestionType, id=Int())
def resolve_questions(root, info):
return Question.objects.all()
def resolve_question(root, info, id):
return Question.objects.get(id=id)
schema = Schema(query=Query)
We have named our root object type as Query
. It has two fields called questions
and question
.
We will use questions
to get list of all questions. We will use question
to get detail of a particular question.
Queries
Let’s make some queries from manage.py shell
.
Get question_text
and id
of all questions.
In [5]: schema.execute('{ questions{ID questionText} }').data
Out[5]:
OrderedDict([('questions',
[OrderedDict([('ID', 56),
('questionText', 'Is the color of sky blue')]),
OrderedDict([('ID', 57),
('questionText',
'Is Samsung more reliable than iPhone?')]),
OrderedDict([('ID', 61),
('questionText',
'Is the color of sky yellow')])])])
Get question_text
and pub_date
of all questions.
In [6]: schema.execute('{ questions{questionText pubDate} }').data
Out[6]:
OrderedDict([('questions',
[OrderedDict([('questionText', 'Is the color of sky blue'),
('pubDate', '2019-05-15')]),
OrderedDict([('questionText',
'Is Samsung more reliable than iPhone?'),
('pubDate', '2019-07-03')]),
OrderedDict([('questionText', 'Is the color of sky yellow'),
('pubDate', '2019-08-13')])])])
Get question_text
and pub_date
of question with id 56.
In [7]: schema.execute('{ question(id:56){questionText pubDate} }').data
Out[7]:
OrderedDict([('question',
OrderedDict([('questionText', 'Is the color of sky blue'),
('pubDate', '2019-05-15')]))])
Api
Let’s setup the api endpoint /graphql
. Create a view and a urlpattern.
# mysite/views.py
from django.views import View
from django.http import JsonResponse
from .graphql import schema
class GraphQLView(View):
def get(self, request, *args, **kwargs):
search = request.GET.get('search')
result = schema.execute(search)
return JsonResponse(result.data, safe=False)
Urlpattern would look like:
# mysite/urls.py
from . import views
urlpatterns = [
path('admin/', admin.site.urls),
path('graphql/', views.GraphQLView.as_view()),
]
Start the server
python manage.py runserver
With this setup, the following queries, which we tried on shell, should work.
http://localhost:8000/graphql/?search={ questions{ID questionText} }
http://localhost:8000/graphql/?search={ questions{questionText pubDate} }
http://localhost:8000/graphql/?search={ question(id:56){questionText pubDate} }
Aliasing
We can use Graphql aliases and get question detail for two different questions in a single api call.
http://localhost:8000/graphql/?search={second: question(id:57){questionText pubDate} first: question(id:56){questionText pubDate}}
This would have required two api calls in a classic REST architecture.
Nested fields
Let’s modify our GraphQL service so clients can get question related choices too along with question. This would need created a ChoiceType
.
class ChoiceType(ObjectType):
choice_text = String()
votes = Int()
def resolve_choice_text(choice, info):
return choice.choice_text
def resolve_votes(choice, info):
return choice.votes
Let’s add ChoiceType
to QuestionType
.
class QuestionType(ObjectType):
choices = List(ChoiceType)
def resolve_choices(question, info):
return question.choice_set.all()
I have skipped other fields ID
, pub_date
and question_text
here for brevity sake. Do not remove those fields and their resolver functions. Just add a choices
and resolve_choices
as done above.
Let’s restart the shell and query for a question detail and ask for choice_text
of associated choices too.
In [7]: search = '{question(id:56){questionText pubDate choices{choiceText}}}'
In [8]: schema.execute(search).data
Out[8]:
OrderedDict([('question',
OrderedDict([('questionText', 'Is the color of sky blue'),
('pubDate', '2019-05-15'),
('choices',
[OrderedDict([('choiceText', 'maybe')]),
OrderedDict([('choiceText', 'yes')]),
OrderedDict([('choiceText', 'no')])])]))])
The api call would look like:
http://localhost:8000/graphql/?search={question(id:56){questionText pubDate choices{choiceText}}}
Mutations
We want to provide a way to create Question. The way to do that with GraphQL is mutations.
Let’s write a mutation called CreateQuestion
. Model Question has fields question_text
and pub_date
. So mutation will need arguments question_text
and pub_date
.
Any Mutation
we write with python-graphene needs to have a method called mutate
. This is synonymous to how resolver functions are needed for query objecttypes.
Basic strucutre of a Mutation
looks like:
class CreateQuestion(Mutation):
class Arguments:
pass
def mutate(root, info, *args):
pass
Let’s write actual code of CreateQuestion
.
class CreateQuestion(Mutation):
class Arguments:
question_text = String()
pub_date = String()
question = Field(QuestionType)
def mutate(root, info, question_text, pub_date):
pub_date = datetime.datetime.strptime(pub_date, '%Y-%m-%d')
question = Question.objects.create(question_text=question_text, pub_date=pub_date)
return CreateQuestion(question=question)
We will have to add this mutation on schema
so that our GraphQL service understands that this mutation is an entry point.
class MyMutations(ObjectType):
create_question = CreateQuestion.Field()
schema = Schema(query=Query, mutation=MyMutations)
Mutation CreateQuestion
needs arguments question_text
and pub_date
to create a Question. These arguments will be passed to the mutate
function. That’s why you can see question_text
and pub_date
in function signature too. question
is the output field of our mutation. So the created Question instance is returned once the mutation resolves.
Let’s create some questions:
In [7]: from bombardill.graphql import schema
In [8]: mutation = """
...: mutation {
...: createQuestion(questionText: "Do you like rabbits?", pubDate: "2019-10-22") {
...: question {
...: questionText
...: pubDate
...: }
...: }
...: }
...: """
In [9]: schema.execute(mutation).data
Out[9]:
OrderedDict([('createQuestion',
OrderedDict([('question',
OrderedDict([('questionText',
'Do you like rabbits?'),
('pubDate', '2019-10-22')]))]))])
We sent a mutation and asked for questionText
and pubDate
fields of created question.
Let’s create one more question but only ask for questionText
field once the mutation resolves.
In [10]: mutation = """
...: mutation {
...: createQuestion(questionText: "Do you like hobbits?", pubDate: "2019-10-22") {
...: question {
...: questionText
...: }
...: }
...: }
...: """
In [11]: schema.execute(mutation).data
Out[11]:
OrderedDict([('createQuestion',
OrderedDict([('question',
OrderedDict([('questionText',
'Do you like hobbits?')]))]))])
Create choice
Let’s similarly add one more Mutation
called CreateChoice
and it on MyMutations
.
class CreateChoice(Mutation):
class Arguments:
question_id = Int()
choice_text = String()
choice = Field(ChoiceType)
def mutate(root, info, question_id, choice_text):
question = Question.objects.get(id=question_id)
choice = Choice.objects.create(question=question, choice_text=choice_text)
return CreateChoice(choice)
class MyMutations(ObjectType):
create_question = CreateQuestion.Field()
create_choice = CreateChoice.Field()
Let’s execute a query to create choice.
In [2]: from bombardill.graphql import schema
In [3]: mutation = """
...: mutation {
...: createChoice(choiceText: "yes", questionId: 56) {
...: choice {
...: choiceText
...: }
...: }
...: }
...: """
In [4]: schema.execute(mutation).data
Out[4]:
OrderedDict([('createChoice',
OrderedDict([('choice',
OrderedDict([('choiceText', 'yes')]))]))])
Similarly we can write mutations to update a question or delete a question.
Thank you for reading the Agiliq blog. This article was written by Akshar on Sep 14, 2019 in GraphQL , API , python , django .
You can subscribe ⚛ to our blog.
We love building amazing apps for web and mobile for our clients. If you are looking for development help, contact us today ✉.
Would you like to download 10+ free Django and Python books? Get them here