SQLAlchemy Mixin
A pack of framework-agnostic, easy-to-integrate for SQLAlchemy ORM.
Heavily inspired by Django ORM
and Eloquent ORM
easy integration to your existing project like FastApi:
from sqlalchemy_mixins import BaseMixin
class User(Base, BaseMixin):
pass
Table of Contents
- Installation
- Quick Start
- Framework-agnostic
- Usage with Flask-SQLAlchemy
- Features
- Active Record
- CRUD
- Querying
- Eager Load
- Django-like queries
- Filter and sort by relations
- Automatic eager load relations
- All-in-one: smart_query
- Beauty __repr__
- DateMixin
Installation
Use pip
pip install SqlalchemyMixin
Quick Start
Framework-agnostic
Here's a quick demo of what our mixins can do.
bob = User.create(name='Bob')
post1 = Post.create(body='Post 1', user=bob, rating=3)
post2 = Post.create(body='long-long-long-long-long body', rating=2,
user=User.create(name='Bill'),
comments=[Comment.create(body='cool!', user=bob)])
print(Post.where(rating__in=[2, 3, 4], user___name__like='%Bi%').all())
print(Comment.with_joined('user', 'post', 'post.comments').first())
print(User.with_subquery('posts', 'posts.comments').first())
print(Post.sort('-rating', 'user___name').all())
print("Created Bob at ", bob.created_at)
Usage with Flask-SQLAlchemy
import sqlalchemy as sa
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy_mixins import BaseMixin
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
db = SQLAlchemy(app)
class BaseModel(db.Model, BaseMixin):
__abstract__ = True
pass
class User(BaseModel):
name = sa.Column(sa.String)
BaseModel.set_session(db.session)
db.create_all()
user = User.create(name='bob')
print(user)
*** Autocommit ***
This library relies on SQLAlchemy's autocommit
flag. It needs to be set to True when initializing the session i.e:
session = scoped_session(sessionmaker(bind=engine, autocommit=True))
BaseModel.set_session(session)
or with Flask-SQLAlchemy
db = SQLAlchemy(app, session_options={'autocommit': True})
Features
Main features are
Active Record
provided by ActiveRecordMixin
SQLAlchemy's Data Mapper
pattern is cool, but
Active Record
pattern is easiest and more DRY.
Well, we implemented it on top of Data Mapper!
All we need is to just inject session into ORM class while bootstrapping our app:
BaseModel.set_session(session)
CRUD
We all love SQLAlchemy, but doing CRUD
is a bit tricky there.
For example, creating an object needs 3 lines of code:
bob = User(name='Bobby', age=1)
session.add(bob)
session.flush()
Well, having access to session from model, we can just write
bob = User.create(name='Bobby', age=1)
that's how it's done in Django ORM
and Peewee
update and delete methods are provided as well
bob.update(name='Bob', age=21)
bob.delete()
And, as in Django
and Eloquent,
we can quickly retrieve object by id
User.get(1)
and fail if such id doesn't exist
User.get_or_abort(123987)
Querying
As in Flask-SQLAlchemy,
Peewee
and Django ORM,
you can quickly query some class
User.query
Also we can quickly retrieve first or all objects:
User.first()
User.all()
Eager load
provided by EagerLoadMixin
Nested eager load
If you use SQLAlchemy's eager loading,
you may find it not very convenient, especially when we want, say,
load user, all his posts and comments to every his post in the same query.
Well, now you can easily set what ORM relations you want to eager load
User.with_({
'posts': {
'comments': {
'user': JOINED
}
}
}).all()
or we can write class properties instead of strings:
User.with_({
User.posts: {
Post.comments: {
Comment.user: JOINED
}
}
}).all()
Subquery load
Sometimes we want to load relations in separate query, i.e. do subqueryload.
For example, we load posts on page like this,
and for each post we want to have user and all comments (and comment authors).
To speed up query, we load comments in separate query, but, in this separate query, join user
from sqlalchemy_mixins import JOINED, SUBQUERY
Post.with_({
'user': JOINED,
'comments': (SUBQUERY, {
'user': JOINED
})
}).all()
Here, posts will be loaded on first query, and comments with users - in second one.
See SQLAlchemy docs
for explaining relationship loading techniques.
Quick eager load
For simple cases, when you want to just
joinedload
or subqueryload
a few relations, we have easier syntax for you:
Comment.with_joined('user', 'post', 'post.comments').first()
User.with_subquery('posts', 'posts.comments').all()
Note that you can split relations with dot like post.comments
due to this SQLAlchemy feature
Filter and sort by relations
provided by SmartQueryMixin
Django-like queries
We implement Django-like
field lookups
and
automatic relation joins.
It means you can filter and sort dynamically by attributes defined in strings!
So, having defined Post
model with Post.user
relationship to User
model,
you can write
Post.where(rating__gt=2, user___name__like='%Bi%').all()
Post.sort('-rating', 'user___name').all()
(___
splits relation and attribute, __
splits attribute and operator)
If you need more flexibility, you can use low-level filter_expr
method session.query(Post).filter(*Post.filter_expr(rating__gt=2, body='text'))
, see example.
It's like filter_by
in SQLALchemy, but also allows magic operators like rating__gt
.
Note: filter_expr
method is very low-level and does NOT do magic Django-like joins. Use smart_query
for that.
All relations used in filtering/sorting should be explicitly set, not just being a backref
In our example, Post.user
relationship should be defined in Post
class even if User.posts
is defined too.
So, you can't type
class User(BaseModel):
user = sa.orm.relationship('User', backref='posts')
and skip defining Post.user
relationship. You must define it anyway:
class Post(BaseModel):
user = sa.orm.relationship('User')
For DRY-ifying your code and incapsulating business logic, you can use
SQLAlchemy's hybrid attributes
and hybrid_methods.
Using them in our filtering/sorting is straightforward (see examples and tests).
Automatic eager load relations
Well, as SmartQueryMixin
does auto-joins for filtering/sorting,
there's a sense to tell sqlalchemy that we already joined that relation.
So that relations are automatically set to be joinedload if they were used for filtering/sorting.
So, if we write
comments = Comment.where(post___public=True, post___user___name__like='Bi%').all()
then no additional query will be executed if we will access used relations
comments[0].post
comments[0].post.user
All-in-one: smart_query
Filter, sort and eager load in one smartest method.
provided by SmartQueryMixin
In real world, we want to filter, sort and also eager load some relations at once.
Well, if we use the same, say, User.posts
relation in filtering and sorting,
it should not be joined twice.
That's why we combined filter, sort and eager load in one smartest method:
Comment.smart_query(
filters={
'post___public': True,
'user__isnull': False
},
sort_attrs=['user___name', '-created_at'],
schema={
'post': {
'user': JOINED
}
}).all()
As developers, we need to debug things with convenience.
When we play in REPL, we can see this
>>> session.query(Post).all()
[<myapp.models.Post object at 0x04287A50>, <myapp.models.Post object at 0x04287A90>]
Well, using our mixin, we can have more readable output with post IDs:
>>> session.query(Post).all()
[<Post #11>, <Post #12>]
Even more, in Post
model, we can define what else (except id) we want to see:
class User(BaseModel):
__repr_attrs__ = ['name']
class Post(BaseModel):
__repr_attrs__ = ['user', 'body']
Now we have
>>> session.query(Post).all()
[<Post #11 user:<User #1 'Bill'> body:'post 11'>,
<Post #12 user:<User #2 'Bob'> body:'post 12'>]
And you can customize max __repr__
length:
class Post(BaseModel):
# ...
__repr_max_length__ = 25
# ...
>>> long_post
<Post #2 body:'Post 2 long-long body' user:<User #1 'Bob'>>
DateMixin
provided by DateMixin
You can view the created and updated timestamps.
bob = User(name="Bob")
session.add(bob)
session.flush()
print("Created Bob: ", bob.created_at)
print("Pre-update Bob: ", bob.updated_at)
time.sleep(2)
bob.name = "Robert"
session.commit()
print("Updated Bob: ", bob.updated_at)