Loading a SQL Alchemy Model with relationships without triggering an insert - python

The problem I am having is When I create my SQLAlchemy model without loading the relationships and immediately commit after, no action is taken as expected (Great). But when I create it with the relationships loaded (based on some ids passed in) and then immediately commit, it tries to insert the data into the database.
Example:
Hero(id=event.stream_id,
name=event.event_payload["name"])
db.commit()
^ In the above nothing happens, which makes sense because I haven't added the model to the session or tried to insert into a table.
Hero(id=event.stream_id,
name=event.get("name"),
teams=[teams.get(p) for p in event.get("teams")])
db.commit()
^ This fails with a duplicate key exception as for some reason it tries to now insert the model into the DB. Is this expected behaviour? Can it be turned off?
Note:teams.get returns a list of Team()
Models for reference:
class HeroTeamLink(SQLModel, table=True):
__tablename__ = "hero_cr_person"
hero_id: Optional[str] = Field(
default=None, foreign_key="hero.id", primary_key=True
)
team_id: Optional[str] = Field(
default=None, foreign_key="team.id", primary_key=True
)
class Hero(SQLModel, table=True):
__tablename__ = "hero"
id: str
name: str
teams: List["Team"] = Relationship(
back_populates="heroes", link_model=HeroTeamLink
)
class Team(SQLModel, table=True):
__tablename__ = "team"
team_name: str
heroes: List[Hero] = Relationship(
back_populates="teams", link_model=HeroTeamLink
)

Related

How to make a double foreign key as a primary key in SQLModel resp. SQLAlchemy

Currently, I have two tables, user and groups and I want to associate them in table group2user, where I specify who has which rights to a group table.
Hence, I need two foreign keys in group2user, which should be able to do cascading delete (if we delete the user or group item).
For this, I wrote down the following code with SQLModel and SQLAlchemy
import enum
from typing import Optional
from sqlmodel import SQLModel, Field, Relationship
class User(SQLModel, table=True):
user_id: str = Field(primary_key=True, nullable=False)
user_group: Optional["Group"] = Relationship(
sa_relationship_kwargs={"uselist": False, "cascade": "save-update,merge,expunge,delete,delete-orphan"})
class Group(SQLModel, table=True):
group_id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True, nullable=False)
user_id: Optional[str] = Field(sa_column=Column(String, ForeignKey("user.user_id", ondelete="CASCADE")))
user_list: List["Group2User"] = Relationship(
sa_relationship_kwargs={"cascade": "save-update,merge,expunge,delete,delete-orphan"},
)
class GroupRights(enum.Enum):
READ = "read"
WRITE = "write"
ADMIN = "admin"
class Group2User(SQLModel):
user_id: str = Field(sa_column=Column(String, ForeignKey("user.user_id", ondelete="CASCADE"), nullable=False, primary_key=True))
group_id: uuid.UUID = Field(sa_column=Column(UUID, ForeignKey("group.group_id", ondelete="CASCADE"),
primary_key=True, nullable=False))
rights: GroupRights = Field(default="READ")
When I have a look at the tables (see below), I see the cascading delete for group via foreign key user_id.
However, the same does not apply for user_id and group_id in the table group2user, where it is a primary key, but not a foreign key with ON DELETE CASCADE.
CREATE TABLE "user" (
user_id VARCHAR NOT NULL,
PRIMARY KEY (user_id)
)
CREATE TABLE "group" (
user_id VARCHAR,
group_id UUID NOT NULL,
PRIMARY KEY (group_id),
FOREIGN KEY(user_id) REFERENCES "user" (user_id) ON DELETE CASCADE
)
CREATE TABLE group2user (
user_id VARCHAR NOT NULL,
group_id UUID NOT NULL,
rights grouprights NOT NULL,
PRIMARY KEY (user_id, group_id)
)
Do you know how to fix that?
If you want a many to many relationship, you should use the link_model option on Relationship members, like the following.
class Group2User(SQLModel, table=True):
...
class User(SQLModel, table=True):
...
groups: List['Group'] = Relationship(back_populates='users', link_model=Group2User)
class Group(SQLModel, table=True):
...
users: List[User] = Relationship(back_populates='groups', link_model=Group2User)
See the official tutorial for a detail.
If you want an association object relationship, you should define bidirectional Relationship members like the following.(I renamed Group2User to Acl for better readability. ACL means access control list.)
class Acl(SQLModel, table=True):
...
user: 'User' = Relationship(back_populates='acls')
group: 'Group' = Relationship(back_populates='acls')
class User(SQLModel, table=True):
...
acls: List[Acl] = Relationship(back_populates='user')
class Group(SQLModel, table=True):
...
acls: List[Acl] = Relationship(back_populates='group')
See the official tutorial for a detail.
If you want cascade deletions on the DBMS level, you should do like this.(I changed the name and type of the primary key columns for better readability.)
class Acl(SQLModel, table=True):
user_id: int = Field(sa_column=
Column(Integer, ForeignKey('user.id', ondelete='CASCADE'),
primary_key=True))
group_id: int = Field(sa_column=
Column(Integer, ForeignKey('group.id', ondelete='CASCADE'),
primary_key=True))
...
user: 'User' = Relationship(back_populates='acls')
group: 'Group' = Relationship(back_populates='acls')
class User(SQLModel, table=True):
...
acls: List[Acl] = Relationship(back_populates='user',
sa_relationship_kwargs = dict(cascade='all', passive_deletes=True))
class Group(SQLModel, table=True):
...
acls: List[Acl] = Relationship(back_populates='group',
sa_relationship_kwargs = dict(cascade='all', passive_deletes=True))
See the SQLAlchemy documentation for a detail.
As a side note, SQLModel project is in its pretty early stage at this time.(Even it has no API reference.) I don't recommend it if you are not ready to hack the source code.

psycopg2 return missing "id" primary key column instead of generate itself

Hello I have the following model:
#dataclass
class User(Base, TimestampMixin):
__tablename__ = "users"
email: str
password: int
name: str
id: Optional[int] = None
id = Column(UUID(as_uuid=True), primary_key=True)
email = Column(String(50), unique=True, nullable=False)
password = Column(String(20), nullable=False)
name = Column(String(15), unique=True, nullable=False)
When I try to generate a new user with the following code:
def insert_user(session: Session, name: str, email: str, password: str) -> None:
try:
user = User(name=name, email=email, password=password)
session.add(user)
except:
session.rollback()
raise InsertUserError
else:
session.commit()
I get the following error:
ERROR in handlers: (psycopg2.errors.InvalidForeignKey) missing "id" primary key column
I wasn't put the id because I guessed that psycopg2 will generate it itself, but it didn't.
How can I create that field automatically?
Just because a column is marked as a primary key, doesn't mean it has a default value. SQLAlchemy will automatically produce the correct SQL to auto-generate primary keys only when using an integer-based primary key column. For UUID primary keys, you are responsible for defining a default.
You can use a Python default:
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
(where you imported the uuid module). This default is used only when you create SQLAlchemy model instances, not when you insert into the table using SQL generated elsewhere.
If you have installed the uuid-osp module (e.g. create extension uuid-ossp) and want to use the uuid_generate_v4 function to produce the default on the server, you'd use server_default parameter:
id = Column(
UUID(as_uuid=True),
primary_key=True,
server_default=func.uuid_generate_v4(),
)
where func is sqlalchemy.sql.expression.func (from sqlalchemy.sql import func should suffice).

Can I have two foreign keys refering to different records in the same table with SQLAlchemy?

So I have two tables:
# "parent" table, each user has multiple "reports_received" and "reports_made"
class User(Base):
__tablename__ = "users"
id: str = Column("id", String(16), primary_key=True)
reports_received = relationship("Report", back_populates="reported_user")
reports_made = relationship("Report", back_populates="reporting_user")
# "child" table, each report has one "reported_user" and "reporting_user"
class Report(Base):
__tablename__ = "reports"
report_id: int = Column("report_id", Integer(), primary_key=True)
reported_user_id = Column("reported_user_id", ForeignKey("users.id"))
reporting_user_id = Column("reporting_user_id", ForeignKey("users.id"))
reported_user = relationship("User",back_populates="reports_received", foreign_keys=[reported_user_id])
reporting_user = relationship("User", back_populates="reports_made", foreign_keys=[reporting_user_id])
Reports should have a reported_user and a reporting_user, both being present in the "users" table.
But this is what I get when I try to create some users and assign them some reports:
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship User.reports_received - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table.
I can't understand why it's giving me this error, since I've already specified the foreign keys. Is there something else I'm missing or does doing something like this not make much sense?
SQLAlchemy just needs a little more help to reconcile the relationships by specifying the foreign_keys= in the User class as well. Switching the order of the class declarations and adding those, as in
# "child" table, each report has one "reported_user" and "reporting_user"
class Report(Base):
__tablename__ = "reports"
report_id: int = Column("report_id", Integer(), primary_key=True)
reported_user_id = Column("reported_user_id", ForeignKey("users.id"))
reporting_user_id = Column("reporting_user_id", ForeignKey("users.id"))
reported_user = relationship(
"User",
back_populates="reports_received",
foreign_keys=[reported_user_id],
)
reporting_user = relationship(
"User", back_populates="reports_made", foreign_keys=[reporting_user_id]
)
# "parent" table, each user has multiple "reports_received" and "reports_made"
class User(Base):
__tablename__ = "users"
id: str = Column("id", String(16), primary_key=True)
reports_received = relationship(
"Report",
back_populates="reported_user",
foreign_keys=[Report.reported_user_id],
)
reports_made = relationship(
"Report",
back_populates="reporting_user",
foreign_keys=[Report.reporting_user_id],
)
lets us do this
engine.echo = True
with Session(engine) as sess:
gord = User(id="Gord")
alexander = User(id="Alexander")
sess.add(Report(reported_user=gord, reporting_user=alexander))
sess.commit()
"""SQL emitted
INSERT INTO users (id) VALUES (?)
[generated in 0.00036s] [('Gord',), ('Alexander',)]
INSERT INTO reports (reported_user_id, reporting_user_id) VALUES (?, ?)
[generated in 0.00031s] ('Gord', 'Alexander')
"""

FastAPI model test importing errors

I try to write tests for my FastAPI application but I get some import errors.
I'm trying to do very simple testing for my models, e.g.:
models/example.py:
class ExampleDbModel(ExampleBase, table=True):
__tablename__ = "example"
id: str
name: str
relation_id: str = Field(foreign_key="another_example.id")
...
relation: AnotherExampleDbModel = Relationship()
class AnotherExampleDbModel(AnotherExampleBase, table=True):
__tablename__ = "another_example"
id: str
some_field: str
relation_id: str = Field(foreign_key="third_example.id")
...
relation: ThirdExampleDbModel = Relationship()
tests/test_example.py:
def test_example():
example = ExampleDbModel(name="test")
fields = [
"id",
"name",
...
]
class_fields = example.dict().keys()
diff = set(fields) ^ set(list(class_fields))
assert not diff
This gives me an error: sqlalchemy.exc.InvalidRequestError: Table 'third_example' is already defined for this MetaData instance. Specify 'extend_existing=True' to redefine options and columns on an existing Table object.. Am I right when assuming it's because the model AnotherExampleDbModel has its own fk relation to another table? How could I test a model that has relations to another table (which has relations to another table)?

ManyToMany with SQLalchemy in Fastapi

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

Categories