I have two models Parent and Child in my database, there's a one to many relationship between these two models i.e. one parent can have multiple children.
from flask import Flask, request
from flask_restful import Resource
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from flask_restful import Api
from marshmallow import fields
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///relationships.db"
db = SQLAlchemy(app)
ma = Marshmallow(app)
api = Api(app, prefix="/api")
class Parent(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20), nullable=False, unique=True)
children = db.relationship("Child", backref="parent")
class Child(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20), nullable=False, unique=True)
parent_id = db.Column(db.Integer, db.ForeignKey("parent.id"))
class ChildSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Child
ordered = True
include_fk = True
dump_only = ("id",)
load_instance = True
class ParentSchema(ma.SQLAlchemyAutoSchema):
children = fields.Pluck(ChildSchema, "name", many=True)
class Meta:
model = Parent
ordered = True
include_relationships = True
dump_only = ("id",)
load_instance = True
class ParentResource(Resource):
#classmethod
def get(cls, _id: int):
parent_schema = ParentSchema()
return parent_schema.dump(parent.query.filter_by(id=_id).first()), 200
#classmethod
def put(cls, _id: int):
parent_json = request.get_json()
parent_schema = ParentSchema()
parent_input_data = parent_schema.load(parent_json)
parent = Parent.query.filter_by(id=_id).first()
parent.name = parent_input_data.name
child_names = [child.name.lower() for child in parent.children]
# Check if child is not already in parent children list
for child_input in parent_input_data.children:
if child_input.name.lower() not in child_names:
parent.children.append(child_input)
db.session.add(parent)
db.session.commit()
return {"message": "Updated"}, 200
api.add_resource(ParentResource, "/parent/<int:_id>")
if __name__ == "__main__":
db.create_all()
app.logger.info("Starting app...")
app.run("127.0.0.1", 3003)
When I try to update the parent by adding some new children, I get a unique key constraint error as it seems that SQLAlchemy is trying to run an insert query in the parent table rather than trying to update the parent table record. There is already a record with the same name 'ABCD' in the parent table i.e. I just tried to leave the parent name as is and just update the children.
This is the input I have given to the PUT request.
{
"name": "ABCD",
"children": [
"Tom",
"Spot"
]
}
Can some one please help me understand where I am going wrong ? When I try to update the parent without trying to update the children, the update seems to work as expected. The issue happens only when I try to update the child relationship.
Try changing your query from using INSERT INTO to using UPDATE. INSERT INTO attempts to add a new record while UPDATE will modify existing records.
Related
I'm trying to get Marshmallow-SQLAlchemy to deserialize an object with a nested object without specifying the foreign key for the nested object (which should be the primary key of the parent object). Here's a standalone example:
# Python version == 3.8.2
from datetime import datetime
import re
# SQLAlchemy == 1.3.23
from sqlalchemy import func, create_engine, Column, ForeignKey, Text, DateTime
from sqlalchemy.ext.declarative import as_declarative, declared_attr
from sqlalchemy.orm import relationship, sessionmaker
# marshmallow==3.10.0
# marshmallow-sqlalchemy==0.24.2
from marshmallow import fields
from marshmallow.fields import Nested
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
################################################################################
# Set up
################################################################################
engine = create_engine("sqlite:///test.db")
Session = sessionmaker()
Session.configure(bind=engine)
session = Session()
################################################################################
# Models
################################################################################
#as_declarative()
class Base(object):
#declared_attr
def __tablename__(cls):
# From https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', cls.__name__)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
#declared_attr
def updated(cls):
return Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False)
class Account(Base):
id = Column(Text, primary_key=True)
name = Column(Text, nullable=False)
tags = relationship("AccountTag", backref="account")
class AccountTag(Base):
account_id = Column(Text, ForeignKey('account.id'), primary_key=True)
Key = Column(Text, primary_key=True)
Value = Column(Text, nullable=False)
################################################################################
# Schemas
################################################################################
class AutoSchemaWithUpdate(SQLAlchemyAutoSchema):
class Meta:
load_instance = True
sqla_session = session
updated = fields.DateTime(default=lambda: datetime.now())
class AccountSchema(AutoSchemaWithUpdate):
class Meta:
model = Account
include_relationships = True
tags = Nested("AccountTagSchema", many=True)
class AccountTagSchema(AutoSchemaWithUpdate):
class Meta:
model = AccountTag
include_fk = True
################################################################################
# Test
################################################################################
Base.metadata.create_all(engine)
account_object = AccountSchema().load({
"id": "ABC1234567",
"name": "Account Name",
"tags": [
{
"Value": "Color",
"Key": "Blue"
}
]
})
session.merge(account_object)
session.commit()
And here's the error I'm getting:
Traceback (most recent call last):
File "example.py", line 88, in <module>
account_object = AccountSchema().load({
File "C:\python\site-packages\marshmallow_sqlalchemy\schema\load_instance_mixin.py", line 92, in load
return super().load(data, **kwargs)
File "C:\python\site-packages\marshmallow\schema.py", line 727, in load
return self._do_load(
File "C:\python\site-packages\marshmallow\schema.py", line 909, in _do_load
raise exc
marshmallow.exceptions.ValidationError: {'tags': {0: {'account_id': ['Missing data for required field.']}}}
I feel like I'm trying to do something intuitive but I'm not sure anymore. I'm sure that I'm close here but am having no luck getting this to work. Help is much appreciated.
You are getting the error because you've specified include_fk in the Meta class for AccountTagSchema.
You can inspect the fields that have been generated for the schema:
print(AccountTagSchema._declared_fields["account_id"])
# <fields.String(default=<marshmallow.missing>, attribute=None, validate=[], required=True, load_only=False, dump_only=False, missing=<marshmallow.missing>, allow_none=False, error_messages={'required': 'Missing data for required field.', 'null': 'Field may not be null.', 'validator_failed': 'Invalid value.', 'invalid': 'Not a valid string.', 'invalid_utf8': 'Not a valid utf-8 string.'})>
Notice that it generates account_id with required=True, this is due to the sqlalchemy column that it represents being NOT NULL as it is part of the primary key.
So the simplest thing is to remove include_fk from the schema meta:
class AccountTagSchema(AutoSchemaWithUpdate):
class Meta(AutoSchemaWithUpdate.Meta):
model = AccountTag
# include_fk = True <--- remove
...however, run the script and you'll run into another problem:
sqlalchemy.orm.exc.UnmappedInstanceError: Class 'builtins.dict' is not mapped
This means that we end up passing a dict to SQLAlchemy session where it is expecting a mapped Base subclass.
The reason for this is that when a child class inherits from a base schema, e.g., AutoSchemaWithUpdate in this case, the child class does not automatically inherit the parent's meta configuration. The docs provide a couple of strategies for this, the simplest being that the child's Meta class should also inherit from the parent's Meta class:
class AccountSchema(AutoSchemaWithUpdate):
class Meta(AutoSchemaWithUpdate.Meta): # <--- this here
model = Account
include_relationships = True
tags = Nested("AccountTagSchema", many=True)
Once we do that for both AccountSchema and AccountTagSchema we are ready to run the script again and it works...the first time. Immediately run the script again, and another error occurs:
AssertionError: Dependency rule tried to blank-out primary key column 'account_tag.account_id' on instance '<AccountTag at 0x7f14b0f9b670>'
This is a consequence of the design decision to have the loaded AccountTag instances unidentifiable (i.e., excluding the primary key from the payload) and the decision to include the foreign key field as part of the primary key for AccountTag.
SQLAlchemy cannot identify that the newly created AccountTag instances are the same as the ones that already exist, so it first tries to disassociate the original account tags from the account by setting the value of the foreign key field to None. However, this isn't allowed as the foreign key is also the primary key and cannot be set NULL.
The solution for this is described here and involves setting an explicit cascade on the relationship:
class Account(Base):
id = Column(Text, primary_key=True)
name = Column(Text, nullable=False)
tags = relationship("AccountTag", backref="account", cascade="all,delete-orphan")
Now run the script again, and it will work every time.
I have two models in my flask app which uses sql_alchemy
The two models location and message have a one to many relationship.
class LocationModel(db.Model):
__tablename__ = 'location'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
def __init__(self, name):
self.name = name
class MessageModel(db.Model):
__tablename__ = 'message'
id = db.Column(db.Integer, primary_key=True)
location_id = db.Column(db.Integer,db.ForeignKey('location.id'), nullable=False)
location = db.relationship("LocationModel", back_populates="messages")
content = db.Column(db.String)
def __init__(self, message_id, content):
self.message_id = message_id
self.content = content
I would like to create an endpoint in the app that allows a user to provide location_name and content and it then creates a new location using the name and a new message using content and sets the foreign key location_id for location as being the id of the new location.
I tried to follow Inserting new records with one-to-many relationship in sqlalchemy
I created a class method
#classmethod
def add_both(cls, name, content):
l = cls(name)
m = MessageModel(content=content)
l.messages.append(m)
db.session.add(l)
db.session.add(m)
db.session.commit()
But I get an error because __init__ is missing the required location_id
Is there a better way to do this?
I'm building a Flask-Restful API using python and sqlalchemy, and I'm trying to join two tables from different databases. It appears that I'm only able to search for tables in one database at a time. Am I missing something?
from flask_sqlalchemy import SQLAlchemy
from flask import Flask, jsonify, request
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://username:password#host:8000/database1'
app.config['SQLALCHEMY_BINDS'] = {
'database2': 'mysql://username:password#host:8000/database2'
}
db = SQLAlchemy(app)
db.create_all(bind='database2')
class Table1(db.Model):
__tablename__ = "table1"
__table_args__ = {'schema':'database1'}
location_id = db.Column(db.Integer, primary_key=True)
def __init__(self, location_id):
self.location_id = location_id
def __repr__(self):
return '{}'.format(self.location_id)
class Table2(db.Model):
__bind_key__ = "database2"
__tablename__ = "table2"
__table_args__ = {'schema':'database2'}
other_id = db.Column(db.Integer, primary_key=True)
location_id = db.Column(db.Integer, db.ForeignKey('database1.table1.location_id'))
def __init__(self, other_id, location_id):
self.other_id = other_id
self.location_id = location_id
def __repr__(self):
return '{}'.format(self.other_id)
#app.route('/', methods=['GET'])
def returnRes():
session = db.session
q = session.query(table1).join(table2, table1.location_id==table2.location_id).all()
return str(q)
In my browser, I am getting the error:
'sqlalchemy.exc.ProgrammingError: (_mysql_exceptions.ProgrammingError) (1146, "Table 'database1.table2' doesn't exist").
Both tables indeed exist, as when I change my query to
q = session.query(table2).join(table1, table2.location_id==table1.location_id).all()
I get an error that database2.table1 doesn't exist.
I'm using python==3.6.1, Flask==0.11.1 and Flask-SQLAlchemy==2.1
Adding a data base schema argument to my table classes and adding a foreign key fixed this problem. I found the answer at this link: https://github.com/mitsuhiko/flask-sqlalchemy/issues/172
I've updated the question to reflect the answer in case it helps anyone else.
I'm not sure if the binds are redundant, but I've left them in because they don't seem to interfere with anything.
You need to add Schema declarations. It is strange that Flask-SQLAlchemy doesn't mention this at all in their documentation.
The Foreign Keys are unnecessary for this to work.
class Table1(db.Model):
__tablename__ = "table1"
__table_args__ = {'schema':'database1'}
#...
def __init__(self, location_id):
self.location_id = location_id
def __repr__(self):
return '{}'.format(self.location_id)
class Table2(db.Model):
__bind_key__ = "database2"
__tablename__ = "table2"
__table_args__ = {'schema':'database2'}
#...
so long as your config file looks like this
SQLALCHEMY_DATABASE_URI = 'postgres:////path/to/database1'
SQLALCHEMY_BINDS = {
'database2': 'mysqldb:////path/to/database2'
}
I'm trying to build a REST API application in Python 3.4, using Flask, SQLAlchemy and Marshmallow.
In my model I have a User class with a one to many relationship with a MailAddress class.
If I run a GET request, I manage to read data from the DB, and the data is correctly returned as a JSON string.
Instead, if I run a POST request with the JSON serialization of my User object with some MailAddresses, I get this error:
File "X:\test\...\site-packages\sqlalchemy\orm\collections.py", line 785, in bulk_replace
constants = existing_idset.intersection(values or ())
File "X:\test\...\site-packages\sqlalchemy\util\_collections.py", line 612, in intersection
result._members.update(self._working_set(members).intersection(other))
TypeError: unhashable type: 'dict'
I've tried adding a __hash__ function to my model classes (as suggested at sqlalchemy: TypeError: unhashable type creating instance, sqlalchemy) but that didn't help.
Here is a complete code example that shows this problem:
from flask import Flask, request
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from marshmallow import fields
from sqlalchemy import Table, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
class Config(object):
SQLALCHEMY_DATABASE_URI = '<CONNECTION STRING HERE>'
SQLALCHEMY_TRACK_MODIFICATIONS = False
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
ma = Marshmallow(app)
# Model
class MailAddress(db.Model):
__tablename__ = 'mail_addresses'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'))
mail_type = Column(String(200), nullable=False)
mail = Column(String(200), nullable=False)
def __init__(self, mail, mail_type):
self.mail = mail
self.mail_type = mail_type
class MailAddressSchema(ma.ModelSchema):
class Meta:
model = MailAddress
class User(db.Model):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(200), nullable=False)
mail_addresses = relationship('MailAddress', backref='user')
def __init__(self, name, mail_addresses):
self.name = name
self.mail_addresses = mail_addresses
def __hash__(self):
return hash(self.name)
class UserSchema(ma.ModelSchema):
mail_addresses = fields.Nested(MailAddressSchema, many = True, only=('mail', 'mail_type'))
class Meta:
model = User
# Routes
user_schema = UserSchema()
#app.route('/api/v0/user', methods=['GET'])
def user_get():
users = db.session.query(User).all()
return user_schema.jsonify(users, many = True), 200
#app.route('/api/v0/user', methods=['POST'])
def user_create():
new_instance = user_schema.make_instance(request.json)
db.session.add(new_instance)
db.session.commit()
return user_schema.jsonify(new_instance), 201
# Main
if __name__ == '__main__':
app.run('localhost', 5555)
Is there something I'm missing?
Use load instead make_instance
#app.route('/api/v0/user', methods=['POST'])
def user_create():
new_instance, errors = user_schema.load(request.json)
db.session.add(new_instance)
db.session.commit()
return user_schema.jsonify(new_instance), 201
I have a class with a one-many relationship. I would like to return all the parent’s children in the relationship ; specifically I’d like to return all the JSONB objects in my children tables.
These are my class:
class Parent(db.Model):
__tablename__ = ‘parent220416'
id = db.Column(db.Integer, primary_key=True)
children = db.relationship(‘Child’, backref=‘Parent’, lazy='dynamic')
class Child(db.Model):
__tablename__ = ‘child220416'
id = db.Column(db.Integer, primary_key=True)
parentid = db.Column(db.Integer, db.ForeignKey('words220416.id'))
data = db.Column(JSONB)
Then with Flask Restful, I’m trying to select all the child like this:
class ParentChild(Resource):
def get(self, id):
result = db.session.query(Parent).get(id)
result_child = result.children
return {'child': result_child}
There is an error:
raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x106178da0> is not JSON serializable
If you want to get all of the data objects for each Child of the Parent. you could do the following query:
result_child = db.session.query(Child.data).filter(Child.parentid == id).all()
If you want to use the children relationship, it is possible to iterate over it:
result_child = [child.data for child in result.children]