Welcome to Part 3 of this mulit-part series. In this part of the series, we’re going to be looking at how to write our Models for our database, as well as our schemas and resolver functions for GraphQL.
In the first two parts of the series, we discussed how to set up our flask backend api, how to connect it to a postgres database, and how to structure our files for an evolving application using an application factory.
As a reminder, we will need to have the following packages installed for this portion of the tutorial:
pip install flask flask_sqlalchemy flask-graphql graphene
I’ve covered flask and flask_sqlalchemy in part one. We will be using flask-graphql and graphene in this part of the tutorial. I was using Flask-GraphQL==2.0.1
and graphene==2.1.9
at the time this article was written.
Let’s jump right in!
OUR FIRST MODEL
Findr / backend / models.py:
In our models.py file, let’s start with the following lines of code:
from backend import db
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(
db.String(64), unique=True, index=True)
def __repr__(self):
return f"<User {self.email} >"
Let’s break apart what’s happening here.
First we have to import our SQLAlchemy that we initiated as db in our application factory (See previous article). Then we create a User Class that extends SQLAlchemy’s Model class. We created a name for our table using the double underscore method for tablename. We created a unique ID for each user that we created by adding primary_key
to that Column. We created an email address column as well, and said that it needs to be unique, as two users can’t have the same email address. Then finally, we used the double underscore repr method so that when we try to print our user in the terminal, it’ll display something legible. Also, as you may have noticed, we are using the db.Column, db.Integer, and db.String methods from SQLAlchemy. There are a few others that we will use throughout this, but also feel free to check the documentation . It’s always a good idea to look through the documentation of a technology you are learning!
So, now we have built our first model! Our database needs to be updated and let it know that our new model exists so that we can start adding users to our database. Open up your terminal and make sure you are in the correct directory ~/Documents/Development/Findr . Also, we want to make sure that our virtual environment is active! We can access it by typing source flaskenv/bin/activate
in our terminal. One we are ready, we can start making our migrations for our database models.
MAKING DATABASE MIGRATIONS
We are going to need another library to help us out here. Let’s install Flask-Migrate.
python3 -m pip install flask-migrate
Also, in our app.py file in the root directory, we want to make sure we add the following lines of code if we don’t have them already:
from flask_migrate import Migrate
# …
migrate = Migrate(app,db)
Now let’s start making our migrations. In our terminal, we will initialize our database for migrations:
flask db init
This command creates a migrations directory, where all our migration scripts will be kept. In order to make our first migrations script we will run the following command:
flask db migrate -m “inital migration”
If we take a look at our files, we should see a migrations directory was added and inside of that directory is a subdirectory called versions. This is where each of the scripts are stored as we make migrations. These are important because they allow us to upgrade or downgrade our migrations – meaning we can move forward with a migration or we can backtrack if something went wrong or we don’t want to make that change.
Let’s open up the migrations directory and look inside the versions subdirectory. There should be just a single file for now, with a name in a hexadecimal code format – a bunch of letters and numbers. Open that file and take a look. You will see two functions: def upgrade( )
and def downgrade( )
which are used to move forward with the migration or resort back to the state it was in before we ran the flask db migrate
command. Since we just made one our first migration nothing should be wrong, so let’s go ahead and upgrade with the following command:
flask db upgrade
Anytime we want to add more migrations, we should always follow this procedure:
Make the necessary changes in the database model.
Generate a migration with the flask db migrate
command.
Review the generated migration script and correct it if it has any inaccuracies.
Apply the changes to the database with the flask db upgrade
command.
ADDING OUR FIRST USER
It’s time to actually get some data in that database now!
Let’s go to our terminal and open up the flask shell for our application by entering the following:
flask shell
Once we are inside the flask shell, we will want to enter the following:
from app import db, User
user_brandon = User(email=’myemail@gmail.com’)
db.session.add(user_brandon)
db.session.commit( )
So, we imported our database and our User Model from our application factory, created a user with an email address, added that user to our session, then committed it to the database. Pretty straight forward! We won’t have to do this ourselves for every user that is created, eventually we will create a function that will add the new users to the database for us through GraphQL.
ADDITIONAL MODELS
Before we get started on the GraphQL portion of this tutorial, let’s add just a bit more complexity to our models so we can really get a good understanding of how GraphQL works.
Let’s make our models.py file look like the following:
from backend import db
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(
db.String(64), unique=True, index=True)
#THIS IS NEW BELOW
profile_id = db.Column(
db.Integer, db.ForeignKey('profiles.id'))
profile = db.relationship(
"Profile", backref='user')
def __repr__(self):
return f"<User {self.email} >"
class Profile(db.Model):
__tablename__ = 'profiles'
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(20))
last_name = db.Column(db.String(20))
skills = db.relationship("Skill")
def __repr__(self):
return f"<Profile {self.first_name} {self.last_name}: #{self.id} >"
class Skill(db.Model):
__tablename__ = 'skills'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20), unique=True,
index=True, nullable=False)
profile_id = db.Column(
db.Integer, db.ForeignKey('profiles.id'))
def __repr__(self):
return f"<Skill {self.name} >"
Let’s break this apart. We added two new models: Profile and Skill. So every profile, will have a user that is attached it – that is where the extra two lines in the User model come from:
profile_id = db.Column(db.Integer, db.ForeignKey('profiles.id'))
profile = db.relationship("Profile", backref='user')
We are creating a Many-to-One relationship in our database. There are many users, each of which has a profile. We create a ForeignKey that tells us the exact ID of the profile that user has. Also, we let the database know that there is a relationship that we can access now by calling User.profile
to get information about that profile.
For more information on database relationships, check out the SQLAlchemy documentation about Basic Relationship Patterns.
SETTING UP GRAPHQL
If you’re interested in some more information about why to use GraphQL instead of a REST API, check out this article.
Time to set up our application to handle our GraphQL!
First we need to go to our backend / __init__.py file
where we created our application factory, and add the following lines of code:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from config import config
#THIS LINE IS NEW
from flask_graphql import GraphQLView
db = SQLAlchemy()
def create_app(config_name):
…
from .schema import schema
app.add_url_rule(
'/graphql',
view_func=GraphQLView.as_view(
'graphql',
schema=schema,
graphiql=True # for having the GraphiQL interface
)
)
…
return app
We imported our schema from our schema.py file, but we haven’t created that yet so we will need to do that next! The rest of the code allows us to use GraphQL as a view function so the frontend can make the call to the backend and talk to GraphQL directly. We also set graphiql=True
so we can use the grapgiQL interface that will let us see and practice using our queries – more on this later.
Next, let’s open up our backend/schema.py file. This is where our GraphQL schema will live. Schemas are an essential part of a GraphQL server implementation. Our schema is going to be made up of two components: the queries and the mutations. Queries are what we use to call the data that we are looking for. Mutations are used to create new data or edit existing data. For now, we’ll just focus on the queries portion.
Let’s write the following code:
Findr / backend / schema.py
import graphene
from .graphql.query import Query
schema = graphene.Schema(query=Query)
Again, you’ll notice that we made a call to yet another file, namely the query.py file. We use this separate schema file as a way to stay organized. Eventually we will also add our mutations to this file as well, so it is important to have all of our schema information in one location. This helps the app scale well for future changes.
GRAPHQL QUERIES
Let’s open up our query.py file and add the following lines of code:
Findr / backend / graphql / query.py
import graphene
from graphene import relay
from graphene_sqlalchemy import SQLAlchemyConnectionField
from ..models import Profile as ProfileModel, \
User as UserModel, \
Role as RoleModel
from ..graphql.objects import UserObject as User, \
ProfileObject as Profile \
class Query(graphene.ObjectType):
node = relay.Node.Field()
users = graphene.List(
lambda: User, email=graphene.String(), user_id=graphene.Int())
def resolve_users(self, info, email=None):
query = User.get_query(info)
if email:
query = query.filter(UserModel.email == email)
return query.all()
all_users = SQLAlchemyConnectionField(User.connection)
We are making some good progress here, we wrote our first resolver function! A resolver is a function that resolves a value for a type or field in a schema. Resolvers can return objects or scalars like Strings, Numbers, Booleans, etc. If an Object is returned, execution continues to the next child field. If a scalar is returned (typically at a leaf node), execution completes. If null is returned, execution stops there.
Our users resolver function allows us to query all of the Users from our database, then filter by email if we choose to enter an email address in our GraphQL query (we’ll see this shortly as well). You can notice that one of the parameters of the resolve_users
function is email=None
. This is our default value which means that if we don’t input an email address in our query, then we will just return all users in the database. It is easy to see how we can add more filters once we start to add attributes to our User models, Profile models, etc.
I would also like to point out the all_users
variable and the users
variable. all_users
basically does the same thing as our users resolver function, however we can only search for all of the users, we cannot filter. Our users
variable tells GraphQL that when we do a query on users, we will return the information in a list, with the ability to use the email parameter and/or the user_id parameter.
A query of:
{
users {
email
userId
}
}
Will return something like:
{
"data": {
"users": [
{
"email": "bj.redmond19@gmail.com",
"userId": 3
},
{
"email": "bredmond1019@gmail.com",
"userId": 2
}
]
}
}
Let’s finish off this by creating a resolver function for our profile object as well:
class Query(graphene.ObjectType):
...
profiles = graphene.List(
lambda: Profile, id=graphene.Int()
)
def resolve_profiles(self, info, id=None):
query = Profile.get_query(info)
if id:
query = query.filter(
ProfileModel.id == id)
return query.all()
…
We applied the same principles here as for the users.
We’re almost ready to test our first query. We just have one more piece to the puzzle – our GraphQL Objects. If you noticed in the code above, we imported things from two different files. One was our models.py file, which we’ve built and talked about already. The other was a objects.py file that we still need to create.
GRAPHQL OBJECTS
Let’s open up our objects.py file.
Findr / backend / graphql / object.py
import graphene
from graphene import relay
from graphene_sqlalchemy import SQLAlchemyObjectType
from ..models import User as UserModel
class UserObject(SQLAlchemyObjectType):
user_id = graphene.Int(source='id')
class Meta:
model = UserModel
interfaces = (relay.Node, )
Before we add the other objects to this file, let’s discuss what we have so far. We imported our models from our models.py file. Then we are creating our class UserObject
, which we can see extends the SQLAlchemyObjectType – a class provided by graphene-sqlalchemy to integrate SQLAlchemy and GraphQL. The user_id
attribute tells graphene to use the id associated with the model in the database, rather than the unique id graphql provides. You don’t need to use this attribute if you don’t want to, I find it useful for my applications. Finally, we are classing the inner class Meta that allows us to set different options. We are telling graphene_sqlalchemy to use the UserModel and to use the Relay Node.
Now that we have a basic understanding, let’s add some more code to this file:
class ProfileObject(SQLAlchemyObjectType):
class Meta:
model = ProfileModel
interface = (relay.Node, )
skills = graphene.List(lambda: SkillObject, name=graphene.String(
))
def resolve_skills(self, info, name=None):
query = SkillObject.get_query(info=info)
query = query.filter(
SkillModel.profile_id == self.id)
if name:
query = query.filter(SkillModel.name == name)
return query.all()
class SkillObject(SQLAlchemyObjectType):
class Meta:
model = SkillModel
interface = (relay.Node, )
class SkillInput(graphene.InputObjectType):
name = graphene.String()
preferred_skill = graphene.Boolean()
Similarly to our UserObject we created above, we have our ProfileObject. However, this time we added a resolver function resolve_skills
inside of our ProfileObject. Here we are using the exact same logic as before with our resolve_users
function we created in our query.py file. We put this resolver function inside of the ProfileObject so that we know the skills query will take place under the ProfileObject rather than in its own query. Let’s take a look at what the GraphQL query would look like.
{
profiles {
firstname
skills {
name
}
}
}
We can see that the skills query takes place directly inside the profiles query. Notice that profiles is at the top of the chain of queries, just like users was. That’s because the profiles resolver function was created in the queries class at the top level of our code. Hopefully now you can see how to implement hierarchical queries.
This query would return something like:
{
"data": {
"profiles": [
{
"firstName": "Brandon",
"skills": [
{
"name": "GraphQL"
},
{
"name": "React"
},
{
"name": "Flask"
}
]
}
]
}
}
A few more points to dissect from our objects.py file is that we added a Skill class and a SkillInput class. The Skill class is straight forward and follows from our User class created earlier. The SkillInput class is going to be useful in our mutations that will follow later.
TESTING OUR GRAPHQL
Alright! Everything is in place and we are ready to run our flask server and make our first query!
NOTE When I first installed graphql and graphene, I wasn’t able to view the following link you’re about to try. That was because I had originally installed graphene==3.0.0 and I had to make sure that I was using an earlier version. As noted in the beginning of this article, I was using Flask-GraphQL==2.0.1
and graphene==2.1.9
at the time this article was written. I would make sure you have those versions running or you may have to do some troubleshooting if you don’t want to revert back to the older versions. Once you do get it running, continue to the following paragraph.
Go ahead and go to your terminal, make sure you are in the home directory, and run flask run
The usual response should appear and we can open up our browser and head to http://localhost:5000/graphql
. Here we should see an empty GraphQL query page where we can test our queries and mutations, also just play around with it and learn!
So far, all we’ve created was one User and added it to our database (unless you’ve added more on your own). We can run our first query:
{
users {
email
userId
}
}
And we should see that single user be returned on the right hand side along with the email and the userId.
GRAPHQL MUTATIONS
Home stretch! Mutations are the last component we’re going to talk about in this article. After this you will have a good understanding of the fundamentals of how to use GraphQL!
If we want to add more users, profiles, skills, etc, then we are going to need to utilize GraphQL’s mutations. Let’s jump right in by opening up our mutations.py file.
Findr / backend / graphql / mutations.py
import graphene
from backend import db
from ..graphql.objects import UserObject as User, ProfileObject as Profile, SkillInput
from ..models import User as UserModel, Profile as ProfileModel, Skill as SkillModel
class UserMutation(graphene.Mutation):
class Arguments:
email = graphene.String(required=True)
user = graphene.Field(lambda: User)
def mutate(self, info, email):
user = UserModel(email=email)
db.session.add(user)
db.session.commit()
return UserMutation(user=user)
class ProfileMutation(graphene.Mutation):
class Arguments:
first_name = graphene.String(required=True)
last_name = graphene.String(required=True)
user_id = graphene.Int(required=True)
skills = graphene.List(SkillInput)
profile = graphene.Field(lambda: Profile)
def mutate(self, info, first_name, last_name, user_id, skills):
user = UserModel.query.get(user_id)
profile = ProfileModel(first_name=first_name, last_name=last_name)
skill_list = [SkillModel(name=input_skill.name) for input_skill in skills]
profile.skills.extend(skill_list)
db.session.add(profile)
user.profile = profile
db.session.commit()
return ProfileMutation(profile=profile)
class Mutation(graphene.ObjectType):
mutate_user = UserMutation.Field()
mutate_profile = ProfileMutation.Field()
This looks like a lot but it’s really not that bad. First, we import our objects and models as we’ve done before. Then, we create two classes: UserMutation and ProfileMutation. Within each class there is an inner class Arguments which are just the parameters for the mutation that we will input through GraphQL (I’ll demonstrate in just a bit). We tell graphene the name of the mutation field (user, profile) we are mutating by assigning those variables the graphene.Field( )
values. Finally, we create a function called mutate
that takes each of the attributes listed in the Arguments inner class as a parameter. We take those inputs and use them to update our models in our database with the new information and commit them to the database.
We create a Mutation Class at the end of this file that we will need to add to our schema. Let’s open up the schema.py file again.
Findr / backend / schema.py
import graphene
from .graphql.query import Query
from .graphql.mutations import Mutation
schema = graphene.Schema(query=Query, mutation=Mutation)
Import the mutations that we’ve created and then add them to the end of the schema after our Query class we created earlier.
Let’s make a mutation to test that it worked. Go back to http://localhost:5000/graphql
after making sure that your flask server is still running, otherwise run it again.
mutation {
mutateUser(email: "myemail@gmail.com") {
user {
email
}
}
}
Should return:
{
"data": {
"mutateUser": {
"user": {
"email": "myemail@gmail.com"
}
}
}
}
One thing to note about mutations is that it also returns a query of the information you created for that user. This can be useful in your application so you don’t have to make multiple queries for a new user, etc.
Run a query for users and you should see the new user added to the database!
Let’s create a new profile as well:
mutation {
mutateProfile(
firstName:"Brandon",
lastName:"Smith",
userId:3,
skills: [{name:"Python"}, {name:"JavaScript"}]
)
{
profile {
firstName
lastName
skills {
name
}
}
}
}
Here we can see where we used the SkillInput class from before because we added two skills in the form of a list. Check out the objects.py file from earlier. Also, it’s important to match the userId with the correct user. Typically in your application, you will have that information already, or you will need to perform a query first to get the information.
There we have it!
To wrap up what we’ve done in this article, we’ve:
- Created Models for our database
- Created our Migrations folder to update our models
- Added users to our database from the flask shell
- Connected Flask to GraphQL
- Created GraphQL Objects, Queries, and Mutations
- Ran our own queries and mutations in graphiQL
I hope this article was helpful. Feel free to leave comments or questions!
The next part of the series will be on creating a React App utilizing Babbel and Webpack5 instead of just using create react app. See you there!
Top comments (2)
Cool tutorial.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.