python-3.xsqlalchemyflask-sqlalchemyflask-restplusmarshmallow-sqlalchemy

Posting Nested models SQLAlchemy-Marshmallow


I am trying to wrap my head around SQLAlchemy in combination with Marshmallow. I had a Flask API that contains some Assets and Trading Pairs. I want bidirectional One-to-Many relationships between these models. I have the following code:

class Asset(db.Model):
    __tablename__ = 'asset'

    id  = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), unique=True, nullable=False)
    abbreviation = db.Column(db.String(20), unique=True, nullable=True)
    trading_bases = relationship("TradingPair", back_populates="base_asset", foreign_keys="TradingPair.base_id")
    trading_quotes = relationship("TradingPair", back_populates="quote_asset", foreign_keys="TradingPair.quote_id")


class TradingPair(db.Model):
    __tablename__ = 'trading_pair'

    id  = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), unique=True, nullable=False)

    # One to many (a pair can have only one base, but 1 asset can be the base of many pairs)
    base_id = db.Column(db.Integer, db.ForeignKey("asset.id"), nullable=False)
    base_asset = relationship("Asset", foreign_keys=[base_id], uselist=False, back_populates="trading_bases")

    # One to many (same reasoning)
    quote_id = db.Column(db.Integer, db.ForeignKey("asset.id"), nullable=False)
    quote_asset = relationship("Asset", foreign_keys=[quote_id], uselist=False, back_populates="trading_quotes")

With the following resource for trading pair POST:

def post(self, name):
    pair = TradingPair.query.filter_by(name=name).first()
    if pair:
        return {"Error": "This resource already exists"}, 409

    data = request.get_json()
    schema = TradingPairSchema()
    try:
        pair = schema.load(data, session=db.session)
        if not pair.name == name:
            return {"Error": f"{name} does not correspond to name in data"}
        db.session.add(pair)
        db.session.commit()
        return {"Success":f"Added: {pair.name}"} 
    except ValidationError as e:
        return {"Error": e.messages}, 400
    except:
        return {"Error":"Database error"}, 500

I expect SQLAlchemy to add new Assets that are POSTed as part of a new trading pair. However, if I want to post new pairs via the API using the following JSON:

{'name': 'DASHUSDT', 
 'base_asset': {
   'name': 'Dash', 
   'abbreviation': 'DASH'}, 
 'quote_asset': {
    'name': 'Tether', 
    'abbreviation': 
    'USDT'}}

This works properly and the pair gets added to the DB as expected. The problem occurs when I try to add another pair that contains Dash or Tether. The pair is added again to the DB and my uniqueness constraint on the Asset table is violated. How can I ensure that a new instance is not created but the existing asset is used?


Solution

  • I ended up with checking whether the assets exist and adding them to the database if they do not yet exist. The code I used in the POST function of the trading pair is:

            loaded = schema.load(data, session=db.session)
            if not loaded.name == name:
                return {"Error": f"{name} does not correspond to name in data"}
            base = Asset.query.filter_by(abbreviation=loaded.base_asset.abbreviation).first()
            if not base:
                base = Asset(name=loaded.base_asset.name , abbreviation=loaded.base_asset.abbreviation)
                db.session.add(base)
            quote = Asset.query.filter_by(abbreviation=loaded.quote_asset.abbreviation).first()
            if not quote:
                quote = Asset(name=loaded.quote_asset.name, abbreviation=loaded.quote_asset.abbreviation)
                db.session.add(quote)
            pair = TradingPair(name=name, base_asset=base, quote_asset=quote)
            db.session.add(pair)
            db.session.commit()
    

    This seems to work properly when the Asset already exist, but also does not crash when a new Asset is inserted via the POST of a trading pair. I could not find any documentation in either SQLAlchemy, Flask-SQLAlchemy or Marshmallow-SQLAlchemy on how this should be handled properly but for now, this works.