Home > database >  Updating other Tables Automatically in Flask SQLAlchemy
Updating other Tables Automatically in Flask SQLAlchemy

Time:06-02

I am learning Flask/SQLAlchemy and am trying to create a simple database. Some of the tables can be computed based on the other tables, but I'm not quite sure where/how to add that logic.

As a simplified problem, let's say there are three tables in this database (* is primary key, ** is foreign key):

User: | id* | name | username | password |
Transactions: | id* | user_id** | date | quantity | value |
Holdings: | id* | user_id** | date | holdings |

I am then using flask-admin to manually add an entry in the Transactions table. Let's suppose that we added the entry: user_id = 1, date = '5/31/2022', quantity = 10, value = $30.

The goal here is that when this entry is added, Flask knows to automatically add another entry to the Holdings table by calculating some Python function. Let's suppose that Holdings.holdings = Transactions.quantity * Transactions.value, so that the goal is to automatically add the following entry: user_id = 1, date = '5/31/2022', holdings = $300 to the Holdings table.

Any suggestions on how to do this?

In Code:

from flask import Flask
from flask_login import UserMixin

app = Flask(__name__)
# Flask boilerplate code, register blueprints, etc

db = SQLAlchemy()

# Database Models

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128), nullable=False)
    username = db.Column(db.String(128), unique=True, nullable=False)
    password = db.Column(db.String(128), nullable=False)

class Transactions(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    user = db.relationship('User')

    date = db.Column(db.DateTime(timezone=True), nullable=False)
    transaction = db.Column(db.Float())

class Holdings(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    user = db.relationship('User')

    date = db.Column(db.DateTime(timezone=True), nullable=False)
    holdings = db.Column(db.Float())


db.init_app(app)


Update This is largely based on the logic of @jorzel answer, with some changes as I forgot to mention I am using flask_sqlalchemy.

@event.listens_for(Transactions, 'after_insert')
def UpdateHoldings(mapper, connection, transaction):

    # Adding new entry
    holdings_val = transaction.quantity * transaction.value
    holdings = Holdings(user_id = transaction.user_id,
                        date = transaction.date,
                        holdings = holdings_val)
    db.session.add( holdings )

    # (Not mentioned in OP, but if we want to update some table)
    # ( And assuming this table does not have date field )
    another_tbl = AnotherTable.__table__
    connection.execute(
        another_tbl.update().where(another_tbl.c.user_id == 
        transaction.user_id).values( holdings = holdings_val )
    )

Note: For some reason, db.session.commit throws a This session is closed error. However not including it seems to work.

CodePudding user response:

I have prepared a simple example using sqlalchemy event after_insert handling.

from datetime import datetime
from sqlalchemy import event, MetaData
from sqlalchemy.orm import sessionmaker
from tutorial.models import Transaction, Holding, User

DB_URI = "sqlite://"

engine = create_engine(DB_URI)
metadata = MetaData()
Base = declarative_base(metadata=metadata)


@contextmanager
def transaction_scope(session):
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise


def get_session(bind):
    Session = sessionmaker(bind)
    return Session()


@event.listens_for(Transaction, 'after_insert')
def receive_after_insert(mapper, connection, target):
    with get_session(connection) as session:
        with transaction_scope(session):
            transaction = target
            holding = Holding(
                user_id=transaction.user_id, 
                holdings=transaction.value * transaction.quantity, 
                date=transaction.date
            )
            session.add(holding)

def add_user(session) -> int:
    with transaction_scope(session):
        user = User(name='Test', username='test', password='secret')
        session.add(user)
        session.flush()
    return user.id

def tutorial_after_insert(with_raise=False):
    with get_session(engine) as session:
        user_id = add_user(session)
        with transaction_scope(session):
            transaction = Transaction(
                quantity=10,
                value=11.2,
                user_id=user_id,
                date=datetime.utcnow()
            )
            session.add(transaction)
            session.flush()
            if with_raise:
                raise Exception('exception')

try:
    tutorial_after_insert(with_raise=False)
except Exception as e:
    print(e)
finally:
    with get_session(engine) as session:
        print(session.query(Holding).all())
        print(session.query(Transaction).all())

You will get following result if no exception is raised:

[Holding(id=1, user_id=1, value=112.0, date=2022-06-01 08:27:26.307473)]
[Transaction(id=1, user_id=1, quantity=10, value=11.2, date=2022-06-01 08:27:26.307473)]

If an exception occurs, no record would be added.

Similar example I have added to this repo: https://github.com/jorzel/sqlalchemy-events-tutorial

  • Related