SQLAlchemy
SQLAlchemy is the Python SQL toolkit and Object Relational Mapper that gives application developers the full power and flexibility of SQL.
Pre-Requisite
This notes will explain some of the codes in mini project 2 that interacts with the database using SQLAlchemy and Flask. However, it is useful that you go through these two tutorials:
We will not explain the explanation that is already written in that tutorial.
Config File and db Object
One change from the previous mini project is the present of config.py
in the root folder.
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'very-secret-key'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or\
'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
This configuration file is needed by SQLAlchemy. The way it is written is as a class Config
. This class is imported inside app/__init__.py
.
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
application = Flask(__name__)
application.config.from_object(Config)
db = SQLAlchemy(application)
migrate = Migrate(application, db)
Notice that after we create the Flask application called application
, we call application.config.from_object(Config)
.
After the configuration, we can then create the SQLAlchemy
instance from application
and assign the name db
. We will use this db
object instance when we interact with the database in our Python code.
The last line is for convenient purpose as we use Flask-Migrate package to migrate the database whenever we change our model or table.
Model, View, and Controller
Flask allows you to write your project using Model-View-Controller (MVC) software design pattern.
In using MVC design pattern, we separate our codes into three main components:
- Model: is the main component of the pattern containing the data structures. The model should be independent of the user interface.
- View: is what the user sees and represents information to the user.
- Controller: is what manipulates the data based on the user interaction with the user interface.
In our mini project, we have the following files where we write all these components:
models.py
: contains our model which defines all the table in our SQL database.templates
folder: contains all the HTML files that provide the views to the users.routes.py
: contains the view controllers. Some view controllers interact with the model.
Models
Our database tables have the following relationships:
You may want to look into this diagram and open models.py
at the same time.
Tables in models.py
The User
class in models.py
has username
and password_hash
as its attributes. In our code, we name this table with a lowercase name user
table. The table user
has one-to-many relationship with the question
table. Every question has an author of that question.
class User(UserMixin, db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer,
primary_key=True)
username = db.Column(db.String(64),
index=True,
unique=True)
password_hash = db.Column(db.String(128))
questions = db.relationship('Question',
backref='from_user',
lazy='dynamic')
challenges = db.relationship('Challenge',
secondary=association_table,
backref=db.backref('to_user'),
lazy='dynamic')
records = db.relationship('TimeRecord',
backref=db.backref('user_challenge'),
lazy='dynamic')
The user
table has many-to-many relationship with the challenge
table through the association
table. This means that one user can have many challenges and one challenge can be sent to many users.
When a user attempt one challenge, it will be recorded inside timerecord
table. Each entry in timerecord
table contains information of which challenge and who the user who submitted the answer. It also records the elapsed_time
.
When a user creates a question, it has to fill in the expression
field. The server then calculates the answer
using your EvaluateExpression
class which is part of your Exercise 2. Moreover, a user chooses another users to create a challenge for them and so it creates one entry in the challenge
table. This means that question
table and challenge
table has a one-to-one relationship.
For one challenge, different users may attempt to answer and creates different timerecord
entries. The same user can attempt it several times as well. This means that the challenge
table has a one-to-many relationship with the timerecord
table since one challenge can have many records for the submissions.
See models.py
and refer to the following documentations in the References to understand the rest of the codes.
References
Querying the Database
We will explain how we use the db
object inside app/routes.py
.
Users and Hall of Fame Page - Querying All Entries
The controller for the Users page is defined inside users()
function in routes.py
.
def users():
users = User.query.all()
mergesort(users, lambda item: item.username)
usernames = [u.username for u in users]
return render_template('users.html', title='Users',
users=usernames)
- The first line uses the class
User
defined inmodels.py
.User
is an instance ofdb.Model
which has aquery
object as its attribute. Thisquery
object has a methodall()
. What this line does is to query theuser
table and retrieve all its entries. - In the second and third line, we actually use the
username
attribute of theUser
object. Recall inmodels.py
thatusername
is one of the attributes defined in the classUser
.
Similarly, we can see a similar code to query all entries in the Hall of Fame page. You can check the function halloffame()
inside routes.py
.
def halloffame():
challenges = Challenge.query.all()
- The above line retrieves all the challenges in the
challenge
table.
Question and Challenge Pages - Adding Entry into Database
Let's go to the controller for the Question page. This allows user to create a question and store it into the database.
def questions():
questions = current_user.questions.all()
...
users = User.query.all()
userlist = [(u.username, u.username) for u in users]
...
- In the first line, the variable
current_user
contains theUser
object which is logged into the page. If you refer toUser
class definition inmodels.py
you will see aquestions
field being defined. Accessing this attribute gives you a query object that allows you to access thequestion
table for that user. Since aquery
object has a methodall()
you can use it to get all the questions created by the current user. - The second and third lines are similar to the Users page where we retrieve all the users and their usernames from the
user
table.
if form.validate_on_submit():
question = Question(expression=form.expression.data)
evalans = EvaluateExpression(form.expression.data)
question.answer = evalans.evaluate()
question.author = current_user.id
challenge = Challenge(question=question)
username_to = []
for name in form.assign_to.data:
username_to.append(User.query.filter_by(username=name).first())
challenge.to_user = username_to
db.session.add(question)
db.session.add(challenge)
db.session.commit()
- When the user submit the question by clicking the submit button, the value of
form.validate_on_submit()
isTrue
. - Upon submission, we want to create a new entry into the
question
table. - To create a new entry, we first create a new object instance:
question = Question(expression=form.expression.data)
where we initialize theexpression
with the string entered by the user in the form. - We then create the object instance for
EvaluateExpression
so that we can call itsevaluate()
method. This will give us the answer which we set here:question.answer = evalans.evaluate()
. - We also want to create another record in the
challenge
table. Therefore, we create the instance:challenge = Challenge(question=question)
. We set thequestion
field in thechallenge
table with the newly createdquestion
. - The next few lines reads the names chosen in the field where the challenge is to be sent to and retrieve the
User
object based on theusername
:User.query.filter_by(username=name).first()
. Note that thequery
object has another method calledfilter_by()
which can be used to retrieve the records based on certain filter conditions. Thefirst()
method returns the firstUser
object found by the query that matches the filter condition. - To modify the database, we have to add a
session
andcommit()
them. This is shown in the last three lines where we add a new entry to thequestion
andchallenge
table and callcommit()
.
db.session.add(question)
db.session.add(challenge)
db.session.commit()
We can see similar things inside the Challenge page controller in challenge()
function inside routes.py
. When a user submit an anwer, the following code will be executed:
if form.validate_on_submit():
record = TimeRecord()
record.elapsed_time = int(form.elapsed_time.data)
record.challenge_id = int(form.challenge_id.data)
record.user_id = current_user.id
answer = form.answer.data
challenge = Challenge.query.filter_by(id=form.challenge_id.data).first()
if int(answer) == int(challenge.question.answer):
db.session.add(record)
db.session.commit()
- The first line creates a
TimeRecord
object after a user submit an answer. - The second and third lines initialize the
elapsed_time
andchallenge_id
attributes of theTimeRecord
object from the data in the submitted form. The next line initialize theuser_id
attribute using thecurrent_user
object'sid
. - We then want to check if the answer is correct. So it retrieves the answer submitted by the user, i.e.
answer = form.answer.data
, with the one from the database. - We get the answer fro the database by first finding the entry in the
challenge
table that matches thechallenge_id
:Challenge.query.filter_by(id=form.challenge_id.data).first()
. - Since every entry in
challenge
id table is associated with an entry in thequestion
table, we can access the questions' answer using:challenge.question.answer
. - The last two lines above add the
TimeRecord
object into the database.