Flask-SQLAlchemy

Set up flask with SQLAlchemy

Integrates SQLAlchemy with Flask

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
 
app: Flask = Flask(__name__)
db: SQLAlchemy = SQLAlchemy()
# The import of MySQLAlchemyConfiguration must be after the initialization of db (SQLAlchemy instance) 
# because MySQLAlchemyConfiguration refers to db
import MySQLAlchemyConfiguration
# The init() function sets up our tables/models/repository
MySQLAlchemyConfiguration.init()

MySQLAlchemyConfiguration file:

from PersonRepository import PersonRepository
from flask_example import db, app
 
person_repository: PersonRepository
 
 
def init():
    global person_repository
    # configure the SQLite database, relative to the app instance folder
    app.config["SQLALCHEMY_DATABASE_URI"] = 'postgresql+psycopg2://postgres:secret@localhost/flask_db'
    # initialize the app with the extension
    db.init_app(app)
    with app.app_context():
        db.create_all()
    person_repository = PersonRepository()

Models and Tables

Generalities

Subclass db.Model class to define models, or the db.Table class to create tables.
Both handle Flask-SQLAlchemy’s bind keys to associate with a specific engine.

Generate tables from model

Use create_all() to create the models and tables after defining them:

with app.app_context():
    db.create_all()

Beware:
create_all() doesn’t update existing tables.
To update them you have to do that manually or with a script outside SQLAlchemy.

ORM model static definitions

Specify relationship of models

The relationship.backref keyword is deprecated, relationship.back_populates with explicit relationship() constructs should be preferred.
Using individual relationship() constructs provides advantages including that both ORM mapped classes will include their attributes up front as the class is constructed, rather than as a deferred step, and configuration is more straightforward as all arguments are explicit.

Static configuration: loading Relationships with the attribute lazy

relationship() as an attribute lazy that defines how the relationship will be loaded.
Default value is select.

Here are the possible values:

select: loaded lazily when the property is first accessed, using a separate SELECT statement, or identity map fetch for simple many-to-one references.

immediate: loaded as the parents are loaded, using a separate SELECT statement, or identity map fetch for simple many-to-one references.

joined: loaded eagerly in the same query as that of the parent, using a JOIN or LEFT OUTER JOIN. Whether the join is outer or not is determined by the relationship.innerjoin parameter.

subquery: loaded eagerly as the parents are loaded, using one additional SQL statement, which issues a JOIN to a subquery of the original statement, for each collection requested.

selectin: loaded eagerly as the parents are loaded, using one or more additional SQL statements, which issues a JOIN to the immediate parent object, specifying primary key identifiers using an IN clause.

noload: no loading should occur at any time. The related collection will remain empty. The noload strategy is not recommended for general use.

raise: lazy loading is disallowed; accessing the attribute, if its value were not already loaded via eager loading, will raise an InvalidRequestError. This strategy can be used when objects are to be detached from their attached Session after they are loaded.

raise_on_sql: lazy loading that emits SQL is disallowed.
Accessing the attribute, if its value were not already loaded via eager loading, will raise an InvalidRequestError, if the lazy load needs to emit SQL.

Example: defining models and their relationships

Define a person model with a one-to-many addresses relationship:

Explanations:
back_populates=’person’: allows objects on both sides of each relationship() to synchronize in-Python state changes
. It is generally specified on both sides.

lazy=’select’ : Specify is the default static loading of the relationship.
Here the select value means to load the relationship only if it is accessed/required.

cascade= »all, delete-orphan »: Specify how the persistence operations are cascaded on the relationship.
The default value is save-update, merge.
The all symbol is a synonym for save-update, merge, refresh-expire, expunge, delete.
And using it in conjunction with delete-orphan indicates that the child object should follow along with its parent in all cases, and be deleted once it is no longer associated with that parent.

from dataclasses import dataclass
from typing import List
 
from flask_example import db
from models.Address import Address
 
 
@dataclass
class Person(db.Model):
    id: int = db.Column(db.Integer, primary_key=True)
    firstname: str = db.Column(db.String)
    lastname = db.Column(db.String)
    addresses = db.relationship('Address',
                                back_populates='person',
                                lazy='select',
                                cascade="all, delete-orphan")
 
    # This constructor is not mandatory, but to have a check about parameter passed if we want to create 
    # manually a person instance, it is better to define it
    def __init__(self, firstname: str, lastname: str, addresses: List[str] = None):
        print(f'Person CONSTRUCTOR')
        self.lastname = lastname
        self.firstname = firstname
        if not addresses:
            addresses = []
        self.addresses = addresses
 
    def add_address(self, address: Address):
        self.addresses.append(address)
 
    def __repr__(self) -> str:
        return f'Person: id={self.id}, firstname={self.firstname}, lastname={self.lastname},' \
            # f'addresses={self.addresses}'

Define an address model with a many-to-one person relationship:

from dataclasses import dataclass
 
from flask_example import db
 
 
@dataclass
class Address(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    country_code: str = db.Column(db.String)
    zip_code = db.Column(db.String)
    street = db.Column(db.String)
    person = db.relationship('Person', back_populates='addresses')
 
    person_id = db.Column(db.Integer, db.ForeignKey('person.id'), nullable=False)
 
    def __init__(self, country_code: str, zip_code: str, street: str):
        print(f'Address CONSTRUCTOR')
        self.zip_code = zip_code
        self.country_code = country_code
        self.street = street
 
    def __repr__(self) -> str:
        return f'Address: id={self.id}, country_code={self.country_code}, zip_code={self.zip_code}, ' \
               f'street={self.street}'

Query examples

Function that insert or update a person

The merge() operation will delete all existing addresses rows and will recreate new addresses rows because we specified in the Person.addresses relationship mapping:
addresses = db.relationship('Address', ... cascade="all, delete-orphan")
Without this cascade configuration, new rows addresses are also created but the old rows will also stay in the database while not used any longer.

    def save(self, person: Person):
        if person.id:
            db.session.merge(person)
        else:
            db.session.add(person)
        db.session.commit()

Find person by id function that fetches the addresses relationship on demand

According to a boolean parameter, it loads only the entity requested or the entity plus its relationship.
We use the option joinedload to load eagerly and inside the same query the relationship.

    from typing import Optional
    from sqlalchemy.orm import joinedload
    from flask_example import db
    from models.Person import Person
 
    def find_by_id(self, person_id: int, fetch_address: bool) -> Optional[Person]:
        person: Person
        """:type: sqlalchemy.orm.Session"""
        query: db.Query = db.session.query(Person)
        if fetch_address:
            query = query.options(joinedload(Person.addresses))
        person: Person = query.get(person_id)
        return person

Find all persons functions with optional filters on person and its addresses relationship attributes

2 main goals:
– Retrieve all person objects.
– Retrieves person objects matching with person attributes and address attributes passed as parameters.
To perform the matching with address attribute, a join is performed.

from typing import List
import flask_sqlalchemy
import sqlalchemy
from flask_example import db
from models.Person import Person
 
    def find_all(self, lastname: str = None, country_code: str = None) -> List[Person]:
        """:type: sqlalchemy.orm.Session"""
        query: flask_sqlalchemy.query.Query = db.session.query(Person)
        if lastname:
            query = query.filter_by(lastname=lastname)
        if country_code:
            query = query.join(Person.addresses).filter_by(country_code=country_code)
        print(f'query={query}, type={type(query)}')
        persons: List[Person] = query.all()
        # SELECT person.id AS person_id, person.firstname AS person_firstname, person.lastname AS 
        # person_lastname
        # FROM person JOIN address ON person.id = address.person_id
        # WHERE person.lastname = %(lastname_1)s AND address.country_code = %(country_code_1)s
        # sqlalchemy.engine.Engine._exec_single_context  [cached since 9.263s ago] {'lastname_1': 'Jon',
        # 'country_code_1': 'FR'}
        return persons

Configuring Logging

Two ways

– Python’s standard logging module
– Use parameters create_engine.echo and create_engine.echo_pool on create_engine() which allow immediate logging to sys.stdout.

how to use it?

sqlalchemy.engine: SQL echoing.
Set to logging.INFO for SQL query output or logging.DEBUG for query + result set output.
Equivalent to echo=True and echo="debug" on create_engine.echo, respectively.

sqlalchemy.pool : connection pool logging.
Set to logging.INFO to log connection invalidation and recycle events or set to logging.DEBUG to additionally log all pool checkins and checkouts.
Equivalent to pool_echo=True and pool_echo="debug" on create_engine.echo_pool, respectively.

sqlalchemy.dialects : logging for SQL dialects.

sqlalchemy.orm : logging ORM functions.
Set to logging.INFO to log some top-level information on mapper configurations.

Declare a SQLAlchemy logger

import logging
 
logging.basicConfig()
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)

marshmallow-sqlalchemy extension

It is a very helpful extension when we want to serialize/deserialize models to/from json.

Ce contenu a été publié dans Non classé. Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *