I have a following models with many-to-many relations:
dashboard_customer_association = Table(
"entry_customer",
Base.metadata,
Column("entry_id", ForeignKey("entry.id"), primary_key=True),
Column("customer_id", ForeignKey("customer.id"), primary_key=True),
)
class Customer(Base):
__tablename__ = "customer"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
name = Column(String(64), unique=True, index=True)
class Entry(Base):
__tablename__ = "entry"
id = Column(String(16), primary_key=True, index=True)
customer = relationship("Customer", secondary=dashboard_customer_association)
Here's my pydantic schema.
class Entry(BaseModel):
id: str
customer: List[str] = []
class Config:
orm_mode = True
I've managed to insert the data and create the customers alongside,
but the problem is when I'm trying to retrieve data:
pydantic.error_wrappers.ValidationError: 2 validation errors for Entry
response -> customer -> 0
str type expected (type=type_error.str)
response -> customer -> 1
str type expected (type=type_error.str)
I understand that the Customer object is not a string, so customer
field cannot be directly serialized as List[str], but I fail to see
how am I supposed to do the conversion.
I return the data with the following function:
def get_data(item_id):
instance = db.query(models.Entry).filter(models.Entry.id == item_id).first()
return instance
I was trying to set instance.customer = [customer.name for customer in instance.customer],
but SQLalchemy prevents that. What is the right way to do that?
The best way would be to simply match the schema to the returned data and have a Customer object as well.
However, if that is not an option, you can use a validator to change the content when it's being populated - i.e. just return a single value from your Customer object.
#validator('customer')
def customer_as_string(cls, v):
return v.name
Related
I have these models in my FastApi Python project with SqlAlchemy:
class Status(Enum):
SUCCESS = "SUCCESS"
FAILURE = "FAILURE"
class BarModel(Base):
__tablename__ = "Bar"
id = Column(Integer, ForeignKey("foo.id"), primary_key=True)
name = Column(String, nullable=True)
class FooModel(Base):
__tablename__ = "foo"
id = Column(Integer, autoincrement=True, primary_key=True)
flag_one = Column(Boolean, nullable=True, default=None)
bar = relationship("BarModel", lazy="joined", uselist=False)
#hybrid_property
def hybrid_enum(self) -> Status:
if (
self.flag_one
and self.bar is not None
and self.bar.name is not None
):
return Status.SUCCESS
else:
return Status.FAILURE
My hybrid_enum property works like a charm when reading data from the database.
However, when i want to do a filter query based on the hybrid property:
db.query(FooModel)
.filter(FooModel.hybrid_enum == Status.SUCCESS)
.all(),
I receive the following error:
AttributeError: Neither 'InstrumentedAttribute' object nor
'Comparator' object associated with FooModel.bar has an
attribute 'name'
I also tried to join the BarModel, but it results in the same error. What am i missing here?
I'm really newbie using SqlAlchemy, right now I'm using fastAPI, basically I need create an Indicator model which can have several nested parameters
this is my code
class Indicator(Base):
__tablename__ = "indicator"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
name = Column(String, unique=True, nullable=False)
indicator_lib_name = Column(String, unique=True, nullable=False)
properties = relationship("Property", backref="indicator", lazy=True,cascade="all, delete")
class Property(Base):
__tablename__ = "property"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
name = Column(String, unique=True, nullable=False)
value = Column(sqlalchemy_utils.ScalarListType, nullable=True)
indicator_id = Column(Integer, ForeignKey("indicator.id", ondelete="CASCADE"))
# indicator = relationship("Indicator", back_populates="properties")
from pydantic import BaseModel
class Property(BaseModel):
id: Optional[int]
name: str
value: List[Any]
class IndicatorCreate(BaseModel):
name: str
indicator_lib_name: Optional[str]
properties: List[Property]
# https://stackoverflow.com/questions/61144192/insert-a-nested-schema-into-a-database-with-fastapi
# https://dev.to/brandonwallace/one-to-many-relationships-complete-how-to-2nbi
def create_new_indicator(db: Session, indicator: IndicatorCreate):
indicator_data = indicator.dict()
properties = []
if indicator.properties:
properties = indicator_data.pop("properties", None)
new_indicator = Indicator(**indicator_data)
new_indicator.indicator_lib_name = indicator.indicator_lib_name or indicator.name
db.add(new_indicator)
db.commit()
db.refresh(new_indicator)
for prpty in properties:
# prpty["indicator"] = new_indicator
property_model = Property(**prpty, indicator=new_indicator)
db.add(property_model)
db.commit()
db.refresh(property_model)
db.refresh(new_indicator)
return new_indicator
#indicators_router.post("/", response_model=IndicatorBase)
def create_indicator(indicator: IndicatorCreate, db: Session = Depends(get_db)):
indicator_new = create_new_indicator(db, indicator)
print("indicator ", indicator_new)
return indicator_new
data = {
"name": "adx",
"indicator_name": "adx",
"properties": [{"name": "range", "value": [2]}],
}
def test_create_indicator(client):
global data
response = client.post("/indicators/", json.dumps(data))
data = response.json()
print(data)
assert response.status_code == 200
assert data["name"] == "adx"
assert data["id"] == 1
assert len(data["properties"]) == 1
the error is
> raise ValidationError(errors, field.type_)
E pydantic.error_wrappers.ValidationError: 1 validation error for IndicatorBase
E response -> properties -> 0
E value is not a valid dict (type=type_error.dict)
when I debug the code I can see the properties filled correctly, it's getting the data fine but the structure is InstrumentedList instead of List, why? how can I convert this into a List or a better question: which is the better approach to achieve this?
the way how I create the indicator and parameters is the better one? or is there a more concise way?
thank you so much guys, hope you can help me in my first steps with SQLAlchemy
Your Property schema isn't defined as loadable from ORM (i.e. with .foo instead of ['foo'] in the source object).
You can change this by adding
class Config:
orm_mode = True
inside your schema class. Since this isn't present pydantic expects a dict-alike (i.e. that the properties can be resolved through a dictionary lookup).
I have following models defined:
class Attribute(Base):
__tablename__ = "attributes"
id = Column(BigInteger, primary_key=True, index=True)
data_id = Column(BigInteger, ForeignKey("data.art_no"))
name = Column(VARCHAR(500), index=True)
data = relationship("Data", back_populates="attributes")
class Data(Base):
__tablename__ = "data"
art_no = Column(BigInteger, primary_key=True, index=True)
multiplier = Column(Float)
attributes = relationship("Attribute", back_populates="data", cascade="all, delete, delete-orphan")
If I query for a Data object, I get this for attributes:
[<app.db.models.Attribute object at 0x10d755d30>]
But I want to get:
['attribute name X']
What I want to get is, that the attributes field should be an array of the Attribute.name fields of the join'ed attributes.
My current query is:
db.query(models.Data).all()
How do I need to modify my query so the attributes field of Data contains not Attribute objects but just the strings name of `Attributes?
I hope you understand the question well ;)
db.query(models.Data).all() returns an array of Data objects. So you can define a custom property on the Data class to extract names from attributes relationship:
class Attribute(Base):
__tablename__ = "attributes"
id = Column(BigInteger, primary_key=True, index=True)
data_id = Column(BigInteger, ForeignKey("data.art_no"))
name = Column(VARCHAR(500), index=True)
data = relationship("Data", back_populates="attributes_rel")
class Data(Base):
__tablename__ = "data"
art_no = Column(BigInteger, primary_key=True, index=True)
multiplier = Column(Float)
attributes_rel = relationship("Attribute", back_populates="data", cascade="all, delete, delete-orphan")
#property
def attributes(self):
return [attribute.name for attribute in self.attributes_rel]
Note that by default sqlalchemy will fetch attributes_rel for each Data object separately upon access. This might result in N+1 selects problem. To avoid that you should specify relationship loading technique
Also take a look at with_entities and hybrid attributes
I've created the schema and 2 tables models with the relation between them.
class SubscriberBase(BaseModel):
subscriber_no: int
is_active: bool = False
class SubscriberCreate(BaseModel):
pass
class Subscriber(SubscriberBase):
owner: int
class Config:
orm_mode = True
class CustomerCreate(BaseModel):
customer_no: int
subscriber: Optional[List[SubscriberBase]] = None
class Customer(CustomerCreate):
id: int
class Config:
orm_mode = True
Models:
class CustomerModel(Base):
__tablename__ = 'customer'
id = Column(Integer, primary_key=True, index=True)
customer_no= Column(Integer,index=True)
subscriber= relationship("SubscriberModel", back_populates="owner")
class SubscriberModel(Base):
__tablename__ = 'subscriber'
id = Column(Integer, ForeignKey("customer.id"))
subscriber_no= Column(Integer, primary_key=True, index=True)
owner = relationship("CustomerModel", back_populates="subscriber")
for the following dictionary input:
test = {'customer_no': 2, 'subscriber': [{'subscriber_no': 2, 'is_active': False}, {'subscriber_no': 1, 'is_active': False}]}
I expect , it will insert one row to customer table ,
and 2 rows in subscriber table.
tried:
db_customer = models.CustomerModel(**test)
db.add(db_customer)
db.commit()
db.refresh(db_customer)
Getting error :
File "/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/attributes.py", line 1675, in emit_backref_from_collection_append_event
child_state, child_dict = instance_state(child), instance_dict(child)
AttributeError: 'dict' object has no attribute '_sa_instance_state'
How does sqlalchemy orm insert to multiple tables?
Is it possible to insert both customer and subscriber tables in one insert ?
#van
Updating the post with more Info:
Im Working with fastapi where the "customer" (the "test" dict i mention above)
is is actually the pydantic , and i did tried the following:
#customer_router.post("/customer/")
def overloaded_create_customer(customer: CustomerCreate, db: Session = Depends(get_db)):
db_customer = CustomerModel(**dict(customer))
db.add(db_customer)
db.commit()
db.refresh(db_customer)
It return similar error:
File "/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/attributes.py", line 1675, in emit_backref_from_collection_append_event
child_state, child_dict = instance_state(child), instance_dict(child)
AttributeError: 'SubscriberBase' object has no attribute '_sa_instance_state'
Creating a ORM model instance (like you try with the CustomerModel) is not the way to do it. Instead, with the assumption that those are pydantic schemas, you should try something along these lines:
# create pydantic model from the dictionary
cust_schema = schemas.Customer.parse_obj(test)
# convert to ORM model
db_customer = models.CustomerModel(**dict(cust_schema))
# ... (rest of your code)
I need to set the entity_type_id as a column value when I persist a row to a generic table of various entity_types. I should be able to load the entity_type_id for every specific instance at instantiation time because it is accessible via a simple select statement. I'd like to have that id automatically retrieved/set at the class (or instance) level without executing a query and/or manually setting to every time I persist a row of an "entity_type".
I tried an entity_type_id #property on the mixin that returns the id of the entity_type using the object_session but for reasons I don't fully understand the orm still inserts null as the entity_type_id value when I commit/flush the session. (my guess is having the "property" itself isn't the same thing as setting the attribute value on the instance and/or causing an issue because the column name from the base class has the same name)
Here's a slimmed down version of the relevant models in my schema:
class EntityType(Base):
__tablename__ = 'entity_type'
id = Column(UUID(as_uuid=True), primary_key=True, server_default=FetchedValue())
table_name = Column(String, nullable=False)
ui_label = Column(Text, unique=True, nullable=False)
entry_key = Column(Text, unique=True, nullable=False)
Base class model:
class TrackedEntity(Base):
#declared_attr
def __tablename__(cls):
return convert(cls.__name__)
__table_args__ = (
UniqueConstraint('entity_type_id', 'label'),
)
id = Column(UUID(as_uuid=True), primary_key=True, server_default=FetchedValue())
entity_type_id = Column('entity_type_id', ForeignKey('entity_type.id'))
label = Column('label', String, nullable=False)
entity_type = relationship('EntityType')
polymorphic_discriminator = column_property(select([EntityType.table_name]).where(EntityType.id == entity_type_id).as_scalar())
#declared_attr
def entity_type_label(cls):
return association_proxy('entity_type', 'label')
#declared_attr
def __mapper_args__(cls):
if cls.__name__ == 'TrackedEntity':
return {
"polymorphic_on": cls.polymorphic_discriminator,
"polymorphic_identity": cls.__tablename__
}
else:
return {"polymorphic_identity": cls.__tablename__}
Children class mixin:
class TrackedEntityMixin(object):
# noinspection PyMethodParameters
#declared_attr
def id(cls) -> Column:
return Column(ForeignKey('tracked_entity.id'), primary_key=True)
#gets me the id but isn't very helpful like this, still needs to be manually set like child.entity_type_id = child._entity_type_id
#property
def _entity_type_id(self):
return object_session(self). \
scalar(
select([EntityType.id]).
where(EntityType.table_name == self.__tablename__)
)
A child class model:
class DesignedMolecule(TrackedEntityMixin, TrackedEntity):
extra = Column('extra', String)
parents = relationship('TrackedEntity', secondary='mix_dm_parent_entity')