PYTHONNANGCAO_7.Database Management with Flask-SQLAlchemy and Email Support with Flask-Mail
PYTHONNANGCAO_7.Database Management with Flask-SQLAlchemy and Email Support with Flask-Mail
Databases
SQL Databases
Figure 5-1 shows a diagram of a simple database with two tables that store users and user
roles. The line that connects the two tables represents a relationship between the tables.
NoSQL Databases
Databases that do not follow the relational model described in the previous
section are collectively referred to as NoSQL databases. One common
organization for NoSQL databases uses collections instead of tables and
documents instead of records. NoSQL databases are designed in a way that
makes joins difficult, so most of them do not support this operation at all.
SQL or NoSQL?
- SQL databases excel at storing structured data in an efficient and
compact form. These databases go to great lengths to preserve consistency,
even in the face of power failures or hardware malfunctions. The
paradigm that allows relational databases to reach this high level of
reliability is called ACID, which stands for Atomicity, Consistency,
Isolation, and Durability.
- NoSQL databases relax some of the ACID requirements and as a result
can sometimes get a performance edge.
For small to medium-sized applications, both SQL and NoSQL databases
are perfectly capable and have practically equivalent performance.
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] =\
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
def __repr__(self):
return '<Role %r>' % self.name
class User(db.Model):
tablename = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
def __repr__(self):
return '<User %r>' % self.username
Table 5-2. Most common SQLAlchemy column types
Type name Python type Description
Regular integer, typically 32 bits Short-range integer, typically
Integer int
16 bits
SmallInteger Int
BigInteger int or long Unlimited precision integer
Float float Floating-point number
Numeric decimal.Decimal Fixed-point number
String str Variable-length string
Text str Variable-length string, optimized for large or unbounded length
Unicode unicode
Variable-length Unicode string
UnicodeText unicode
bool Variable-length Unicode string, optimized for large or unbounded length
Boolean
datetime.date Boolean value
Date
datetime.time Date value
Time
datetime.datetime
DateTime Time value
datetime.timedelta
Interval str Date and time value
Enum Any Python object Time interval
PickleType List of string values Automatic
str
LargeBinary
Pickle serialization Binary blob
The remaining arguments to db.Column specify configuration options for each attribute. Table 5-3 lists some of the options available.
class User(db.Model):
# ...
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
Table 5-4. Common SQLAlchemy relationship options
Inserting Rows
The following example creates a few roles and users:
>>> from hello import Role, User
>>> admin_role = Role(name='Admin')
>>> mod_role = Role(name='Moderator')
>>> user_role = Role(name='User')
>>> user_john = User(username='john', role=admin_role)
>>> user_susan = User(username='susan', role=user_role)
>>> user_david = User(username='david', role=user_role)
The objects exist only on the Python side so far; they have not been
written to the database yet. Because of that, their id values have not yet
been assigned:
>>> print(admin_role.id)
None
>>> print(mod_role.id)
None
>>> print(user_role.id)
None
Changes to the database are managed through a database session, which
Flask- SQLAlchemy provides as db.session. To prepare objects to be
written to the data‐ base, they must be added to the session:
>>> db.session.add(admin_role)
>>> db.session.add(mod_role)
>>> db.session.add(user_role)
>>> db.session.add(user_john)
>>> db.session.add(user_susan)
>>> db.session.add(user_david)
Database sessions are also called transactions.
Or, more concisely:
>>> db.session.add_all([admin_role, mod_role, user_role,
...user_john, user_susan, user_david])
To write the objects to the database, the session needs to be committed
by calling its
commit() method:
>>> db.session.commit()
Check the id attributes again after having the data committed to see
that they are now set:
>>> print(admin_role.id)
1
>>> print(mod_role.id)
2
>>> print(user_role.id)
3
The db.session database session is not related to the Flask
session object discussed in Chapter 4.
Modifying Rows
The add() method of the database session can also be used to update
models. Con‐ tinuing in the same shell session, the following example
renames the "Admin" role to "Administrator":
>>> admin_role.name = 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
Deleting Rows
The database session also has a delete() method. The following example
deletes the
"Moderator" role from the database:
>>> db.session.delete(mod_role)
>>> db.session.commit()
Note that deletions, like insertions and updates, are executed only when
the database session is committed.
Querying Rows
Flask-SQLAlchemy makes a query object available in each model
class. The most basic query for a model is triggered with the all()
method, which returns the entire contents of the corresponding table:
>>> Role.query.all()
[<Role 'Administrator'>, <Role 'User'>]
>>> User.query.all()
[<User 'john'>, <User 'susan'>, <User 'david'>]
A query object can be configured to issue more specific database
searches through the use of filters. The following example finds all the
users that were assigned the "User" role:
>>> User.query.filter_by(role=user_role).all()
[<User 'susan'>, <User 'david'>]
It is also possible to inspect the native SQL query that SQLAlchemy generates for
a given query by converting the query object to a string:
>>> str(User.query.filter_by(role=user_role))
'SELECT users.id AS users_id, users.username AS users_username,
users.role_id AS users_role_id \nFROM users \nWHERE :param_1 =
users.role_id'
The following example issues a query that loads the user role with name "User":
>>> user_role = Role.query.filter_by(name='User').first()
Table 5-5 shows some of the most common filters available to queries. The
complete list is in the SQLAlchemy documentation.
Table 5-5. Common SQLAlchemy query filters
Option Description
filter() Returns anewquery that adds an additionalfilterto the original query
The shell context processor function returns a dictionary that includes the
database instance and the models. The flask shell command will
import these items auto‐ matically into the shell, in addition to app, which
is imported by default:
$ flask shell
>>> app
<Flask 'hello'>
>>> db
<SQLAlchemy engine='sqlite:////home/flask/flasky/data.sqlite'>
>>> User
<class 'hello.User'>
Creating a Migration Repository
To begin, Flask-Migrate must be installed in the virtual environment:
(venv) $ pip install flask-migrate
Example 5-8. hello.py: Flask-Migrate initialization
from flask_migrate import Migrate
# ...
The two environment variables that hold the email server username and
password need to be defined in the environment. If you are on Linux or
macOS, you can set these variables as follows:
(venv) $ export MAIL_USERNAME=<Gmail username>
(venv) $ export MAIL_PASSWORD=<Gmail password>
For Microsoft Windows users, the environment variables are set as
follows:
(venv) $ set MAIL_USERNAME=<Gmail username>
(venv) $ set MAIL_PASSWORD=<Gmail password>
Sending Email from the Python Shell
To test the configuration, you can start a shell session and
send a test email
(venv) $ flask shell
>>> from flask_mail import Message
>>> from hello import mail
>>> msg = Message('test email', sender='[email protected]',
... recipients=['[email protected]'])
>>> msg.body = 'This is the plain text body'
>>> msg.html = 'This is the <b>HTML</b> body'
>>> with app.app_context():
... mail.send(msg)
...
Integrating Emails with the Application
Example 6-3. hello.py: email support
from flask_mail import Message
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin
<[email protected]>'
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess
string' MAIL_SERVER = os.environ.get('MAIL_SERVER',
'smtp.googlemail.com') MAIL_PORT =
int(os.environ.get('MAIL_PORT', '587'))
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in
\ ['true', 'on', '1']
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
FLASKY_MAIL_SENDER = 'Flasky Admin <[email protected]>'
FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')
SQLALCHEMY_TRACK_MODIFICATIONS = False
@staticmethod
def init_app(app):
pass
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
config = {
'development': DevelopmentConfig, 'testing':
TestingConfig, 'production': ProductionConfig,
'default': DevelopmentConfig
}
Example 7-3. app/ init .py: application package constructor
from flask import Flask, render_template from
flask_bootstrap import Bootstrap from flask_mail
import Mail
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from config import config
bootstrap.init_app(app) mail.init_app(app)
moment.init_app(app) db.init_app(app)
return app
Implementing Application Functionality in a
Blueprint
Example 7-4. app/main/ init .py: main blueprint creation
from flask import Blueprint
return app
Example 7-6. app/main/errors.py: error handlers in main
blueprint
from flask import render_template
from . import main
@main.app_errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@main.app_errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
def test_app_is_testing(self):
self.assertTrue(current_app.config['TESTING'])
The tests are written using the standard unittest package from the Python standard library. The setUp() and
tearDown() methods of the test case class run before and after each test, and any methods that have a name that
begins with test_ are executed as tests.
If you want to learn more about writing unit tests with Python’s
unittest package, read the official documentation.
Example 7-10. flasky.py: unit test launcher command
@app.cli.command()
def test():
"""Run the unit tests."""
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
The implementation of the test() func‐ tion invokes the test runner from the
unittest package.
The unit tests can be executed as follows:
(venv) $ flask test
test_app_exists (test_basics.BasicsTestCase) ... ok
test_app_is_testing (test_basics.BasicsTestCase) ... ok
.----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
----
Database Setup
Regardless of the source of the database URL, the database tables must
be created for the new database. When working with Flask-Migrate to
keep track of migrations, database tables can be created or upgraded
to the latest revision with a single com‐ mand:
(venv) $ flask db upgrade
Running the Application
The refactoring is now complete, and the application can be started.
Make sure you have updated the FLASK_APP environment variable as
indicated in “Application Script” on page 93, and then run the
application as usual:
(venv) $ flask run
Having to set the FLASK_APP and FLASK_DEBUG environment variables
every time a new command-prompt session is started can get tedious,
so you should configure your system so that these variables are set by
default. If you are using bash, you can add them to your ~/.bashrc file.
Believe it or not, you have reached the end of Part I. You have now
learned about the basic elements necessary to build a web application
with Flask, but you probably feel unsure about how all these pieces fit
together to form a real application. The goal of Part II is to help with
that by walking you through the development of a complete
application.