sqlalchemy check foreign key refer data exist before insert? - python

I'm new to the sqlalchemy and fastAPI. I wonder there is any way to check refer data automatically before inserting it. For example, I want to make sure that profile.user_id exists before adding a new profile, but I don't want to do it by myself. Is that possible? Below are my table settings.
class User(Base):
__tablename__ = "user"
id = Column("id", Integer, primary_key=True, autoincrement=True)
email = Column(String, unique=True, nullable=False)
hashed_password = Column(String, nullable=False)
create_time = Column(DateTime, nullable=False, default=func.now())
login_time = Column(DateTime, nullable=False, default=func.now())
class Profile(Base):
__tablename__ = "user_profile"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
name = Column(String, )
age = Column(Integer)
country = Column(Integer)
photo = Column(String, )

What is missing in your mapped classes is a relationship.
In ORMs, these relationships handle ensuring the foreign key exits and makes creating relationships much easier.
Also, if you want to set a default value on the SQL side (since you use default=func.now()), you should use the server_default=func.now() keyword argument. Otherwise, use the python side equivalent, default=datetime.utcnow().
Finally, if your relationship is a one-to-one relationship, use the uselist=False keyword argument in the User.profiles relationship (also best to rename it User.profile).
from datetime import datetime
from sqlalchemy import (
Column,
DateTime,
ForeignKey,
Integer,
String,
create_engine,
)
from sqlalchemy.orm import Session, declarative_base, relationship
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column("id", Integer, primary_key=True, autoincrement=True)
email = Column(String, unique=True, nullable=False)
hashed_password = Column(String, nullable=False)
create_time = Column(DateTime, nullable=False, default=datetime.utcnow())
login_time = Column(DateTime, nullable=False, default=datetime.utcnow())
profiles = relationship(
"Profile", back_populates="user"
) # add uselist=False if one-to-one
class Profile(Base):
__tablename__ = "user_profile"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
name = Column(String)
age = Column(Integer)
country = Column(Integer)
photo = Column(String)
user = relationship("User", back_populates="profiles")
engine = create_engine("sqlite://", echo=True, future=True)
Base.metadata.create_all(engine)
with Session(engine) as session:
ljmc = User(email="ljmc#stack.overflow", hashed_password="00ab")
ljmc_profile = Profile(name="ljmc")
ljmc_profile.user = ljmc
session.add(ljmc)
session.flush()
This emits:
CREATE TABLE user (
id INTEGER NOT NULL,
email VARCHAR NOT NULL,
hashed_password VARCHAR NOT NULL,
create_time DATETIME NOT NULL,
login_time DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE (email)
)
CREATE TABLE user_profile (
id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
name VARCHAR,
age INTEGER,
country INTEGER,
photo VARCHAR,
PRIMARY KEY (id),
FOREIGN KEY(user_id) REFERENCES user (id)
)
INSERT INTO user (email, hashed_password, create_time, login_time) VALUES ('ljmc#stack.overflow', '00ab', '2023-01-17 10:11:48.250845', '2023-01-17 10:11:48.250959')
INSERT INTO user_profile (user_id, name, age, country, photo) VALUES (1, 'ljmc', None, None, None)

Related

Adding multiple foreign keys from same model (FastAPI and SqlAlechemy)

What I am trying to do is have 2 foreign keys from User table inside Ban table here is how I did it:
class Ban(Base):
__tablename__ = "ban"
ban_id = Column(Integer, primary_key=True, index=True)
poll_owner_id = Column(Integer)
banned_by = Column(String , ForeignKey('user.username', ondelete='CASCADE', ), unique=True)
user_id = Column(Integer, ForeignKey('user.user_id', ondelete='CASCADE', ))
updated_at = Column(DateTime)
create_at = Column(DateTime)
ban_to_user = relationship("User", back_populates='user_to_ban', cascade='all, delete')
and User table:
class User(Base):
__tablename__ = "user"
user_id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True)
email = Column(String)
create_at = Column(DateTime)
updated_at = Column(DateTime)
user_to_ban = relationship("Ban", back_populates='ban_to_user', cascade='all, delete')
When I try to run a query to fetch all users like this:
#router.get('/all')
async def get_all_users(db:Session = Depends(get_db)):
return db.query(models.User).all()
I get this error:
sqlalchemy.exc.InvalidRequestError: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'mapped class User->user'. Origina
l exception was: Could not determine join condition between parent/child tables on relationship User.user_to_ban - 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 did the relationship between them as you can see but it states that there is problem between them. If needed I can show you how I did migration for my db using alembic if that is possible cause or is there a cleaner and better way to do this. Thanks in advance
You can have several foreign keys to a single table, like in your case for banned user and banned_by user.
You just need to disambiguate, which ForeignKey for which relationship (docs):
class Ban(Base):
__tablename__ = "ban"
id = Column(Integer, primary_key=True)
banned_user_id = Column(Integer, ForeignKey("user.id")) # for banned_user relationship
banned_by_user_id = Column(Integer, ForeignKey("user.id")) # for banned_by relationship
banned_user = relationship("User", foreign_keys=[banned_user_id], back_populates="bans")
banned_by = relationship("User", foreign_keys=[banned_by_user_id])
Full demo:
from sqlalchemy import (
Column,
ForeignKey,
Integer,
String,
create_engine,
select,
)
from sqlalchemy.orm import Session, declarative_base, relationship
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
username = Column(String, unique=True)
bans = relationship(
"Ban",
back_populates="banned_user",
foreign_keys="Ban.banned_user_id",
)
class Ban(Base):
__tablename__ = "ban"
id = Column(Integer, primary_key=True)
banned_user_id = Column(Integer, ForeignKey("user.id"))
banned_by_user_id = Column(Integer, ForeignKey("user.id"))
banned_user = relationship(
"User", foreign_keys=[banned_user_id], back_populates="bans"
)
banned_by = relationship("User", foreign_keys=[banned_by_user_id])
engine = create_engine("sqlite://", echo=True, future=True)
Base.metadata.create_all(engine)
spongebob = User(username="spongebob")
patrick = User(username="patrickstarr")
spongebob_bans_patrick = Ban(banned_by=spongebob, banned_user=patrick)
with Session(engine) as session:
session.add_all(
[
spongebob,
patrick,
spongebob_bans_patrick,
]
)
session.commit()
with Session(engine) as session:
result = session.scalars(select(Ban)).first()
print(
"User:",
result.banned_user.username,
"was banned by User:",
result.banned_by.username,
)
# User: patrickstarr was banned by User: spongebob

Conversion from sql to sqlalchemy

I'm trying to create classes for sqlalchemy in order to make sqlalchemy create database Sometables.
Here is SQL code:
CREATE table "Sometable" (
id INTEGER UNIQUE NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
database_id INTEGER REFERENCES databases (id) ON DELETE CASCADE NOT NULL,
CONSTRAINT a UNIQUE (name, database_id) ON CONFLICT IGNORE
);
I have tried so far:
class Sometable(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(140))
database_id = db.Column(db.Integer,
db.ForeignKeyConstraint("Sometable.database_id", "databases.id"))
class Databases(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(140))
and:
class Sometable(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
database_id = db.Column(db.Integer, db.ForeignKeyConstraint(["Sometable.database_id"], ["databases.id"]))
class Databases(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
and:
class Sometable(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
database_id = db.Column(db.Integer, db.ForeignKey("databases.id"))
db.ForeignKeyConstraint(["database_id"], ["databases.id"])
class Databases(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
and I cannot figure out how to do it. When I do flask db init; migrate; upgrade I receive schema of Sometable:
CREATE TABLE Sometable(
id INTEGER NOT NULL,
name VARCHAR,
database_id INTEGER,
PRIMARY KEY (id),
FOREIGN KEY(database_id) REFERENCES databases (id)
#Edit
and also tried:
class Sometable(db.Model):
id = db.Column(db.Integer, primary_key=True, unique=True)
name = db.Column(db.String)
database_id = db.Column(db.Integer, db.ForeignKey("databases.id"))
db.ForeignKeyConstraint(["database_id"], ["databases.id"])
db.UniqueConstraint("name", "database_id")
class Databases(db.Model):
id = db.Column(db.Integer, primary_key=True, unique=True)
name = db.Column(db.String)

SQLAlchemy can't reflect table with primary key

These are the classes:
class User(db.Model):
__tablename__ = 'Users'
UserID = db.Column(
db.Integer,
primary_key=True,
autoincrement=True,
nullable=False
)
FirstName = db.Column(db.String(255), nullable=False)
LastName = db.Column(db.String(255), nullable=False)
Username = db.Column(db.String(255), nullable=False)
Password = db.Column(db.String(255), nullable=False)
class UserType(db.Model):
__tablename__ = 'UserTypes'
TypeID = db.Column(
db.Integer,
primary_key=True,
autoincrement=True,
nullable=False
)
Type = db.Column(db.String(255), nullable=False)
__table_args__ = (
db.CheckConstraint(
"Type IN ('Role1', 'Role2', 'Role3')"
),
)
class UserPrivilege(db.Model):
__tablename__ = 'UserPrivileges'
UserID = db.Column(db.Integer, primary_key=True)
UserTypeID = db.Column(db.Integer, primary_key=True)
__table_args__ = (
db.ForeignKeyConstraint(
['UserID'],
['Users.UserID'],
),
db.ForeignKeyConstraint(
['UserTypeID'],
['UserTypes.TypeID'],
),
)
PrivilegeUserInfoBackref = db.relationship(
'User',
backref='PrivilegeUserInfoBackref',
lazy=True,
)
PrivilegeUserTypeInfoBackref = db.relationship(
'UserType',
backref='PrivilegeUserTypeInfoBackref',
lazy=True,
)
And here is the code for reflecting the tables:
Base = automap_base()
engine = sa.create_engine(
DATABASE_CONNECTION,
convert_unicode=True,
pool_size=10,
max_overflow=20
)
db_session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
bind=engine))
Base.prepare(engine, reflect=True)
The 'Users' and 'UserTypes' classes appear in Base.classes._data but for some reasson 'UserPrivileges' does not appear in Base.classes._data. All I managed to find is that tables with no primary key can't be reflected but as you can see that is not the case here. I also have some more tables that have composite primary key with backrefs but that are reflected with no problem.
So, can anyone give me any suggestions in order to reflect the last table as well, please ?
The table created for UserPrivilege ticks all the boxes of a many-to-many relationship's "secondary" table, and as such is not mapped directly when using the automap extension. This behaviour is also explained in the note of "Basic Use":
By viable, we mean that for a table to be mapped, it must specify a primary key. Additionally, if the table is detected as being a pure association table between two other tables, it will not be directly mapped and will instead be configured as a many-to-many table between the mappings for the two referring tables.
Your table should exist as Base.metadata.tables['UserPrivileges'].

Flask-SQLAlchemy how to do constraint foreign key with cascade in MySQL (InnoDB)?

I've been looking for ways to implement the CONSTRAINT FOREIGN KEY ON DELETE CASCADE in the below UsersAccessMapping model in SQLAlchemy with PyMySQL driver and MariaDB 10.0 with InnoDB in the database.
Python = 3.5.2
SQLAlchemy = 1.1.13
Flask-SQLAlchemy = 2.2
The SQL:
CREATE TABLE Users (
UserID int AUTO_INCREMENT,
Name varchar(200) NOT NULL,
Email varchar(200),
Username varchar(200) NOT NULL,
Password text NOT NULL,
Created datetime,
Updated datetime,
PRIMARY KEY (UserID)
);
CREATE TABLE UsersAccessLevels (
UsersAccessLevelID int AUTO_INCREMENT,
LevelName varchar(100) NOT NULL,
AccessDescription text,
PRIMARY KEY (UsersAccessLevelID)
);
CREATE TABLE UsersAccessMapping (
UsersAccessMappingID int AUTO_INCREMENT,
UserID int NOT NULL,
UsersAccessLevelID int NOT NULL,
PRIMARY KEY (UsersAccessMappingID),
CONSTRAINT fk_useraccess FOREIGN KEY (UserID)
REFERENCES Users(UserID) ON DELETE CASCADE,
CONSTRAINT fk_useraccess_level FOREIGN KEY (UsersAccessLevelID)
REFERENCES UsersAccessLevels(UsersAccessLevelID) ON DELETE CASCADE
);
What I have in my models.py now:
from app import db
class Users(db.Model):
"""All users' information is stored here"""
__tablename__ = "Users"
UserID = db.Column(db.Integer(), primary_key=True)
Name = db.Column(db.String(200), nullable=False)
Email = db.Column(db.String(200))
Username = db.Column(db.String(200), nullable=False)
Password = db.Column(db.Text, nullable=False)
Created = db.Column(db.DateTime)
Updated = db.Column(db.DateTime)
class UsersAccessLevels(db.Model):
"""This defines the various access levels users can have"""
__tablename__ = "UsersAccessLevels"
UsersAccessLevelID = db.Column(db.Integer, primary_key=True)
LevelName = db.Column(db.String(100), nullable=False)
AccessDescription = db.Column(db.Text)
class UsersAccessMapping(db.Model):
"""Each users' access level is defined here"""
__tablename__ = "UsersAccessMapping"
UsersAccessMappingID = db.Column(db.Integer, primary_key=True)
UserID = db.Column(db.Integer, nullable=False)
UsersAccessLevelID = db.Column(db.Integer, nullable=False)
__table_args__ = (
db.ForeignKeyConstraint(
["fk_useraccess", "fk_useraccess_level"],
["Users.UserID", "UsersAccessLevels.UsersAccessLevelID"],
ondelete="CASCADE"
)
)
There is something wrong with the table_args syntax, but I haven't been able to find any examples on how it should be. I found one that was very similar, but in that the third parameter was an empty dict. However, I want to use the ondelete="CASCADE". How would that be added?
When running the python3 manage.py db init, it throws this:
File "/srv/vortech-backend/venv/lib/python3.5/site-packages/sqlalchemy/ext/declarative/base.py", line 196, in _scan_attributes
"__table_args__ value must be a tuple, "
sqlalchemy.exc.ArgumentError: __table_args__ value must be a tuple, dict, or None
I tried changing ondelete="cascade" to a dict {"ondelete": "cascade"}, but that doesn't work either. It gives the same error as above.
Update:
The problem was that the ondelete is supposed to be outside of the tuple, like this:
__table_args__ = (
db.ForeignKeyConstraint(
["fk_useraccess", "fk_useraccess_level"],
["Users.UserID", "UsersAccessLevels.UsersAccessLevelID"]
),
ondelete="CASCADE"
)
However, with this change there is still a syntax error, as ondelete="CASCADE" is not defined. Changing it to a dict {"ondelete": "cascade"} throws this:
File "/srv/vortech-backend/venv/lib/python3.5/site-packages/sqlalchemy/sql/base.py", line 282, in _validate_dialect_kwargs
"named <dialectname>_<argument>, got '%s'" % k)
TypeError: Additional arguments should be named <dialectname>_<argument>, got 'ondelete'
Okay, after some testing and reading, the answer is that SQLAlchemy does some internal magic to achieve it. So, this will accomplish the same result as the SQL:
from app import db # The value is from: db = SQLAlchemy(app)
class Users(db.Model):
"""All users' information is stored here"""
__tablename__ = "Users"
UserID = db.Column(db.Integer(), primary_key=True)
Name = db.Column(db.String(200), nullable=False)
Email = db.Column(db.String(200))
Username = db.Column(db.String(200), nullable=False)
Password = db.Column(db.Text, nullable=False)
Created = db.Column(db.DateTime)
Updated = db.Column(db.DateTime)
class UsersAccessLevels(db.Model):
"""This defines the various access levels users can have"""
__tablename__ = "UsersAccessLevels"
UsersAccessLevelID = db.Column(db.Integer, primary_key=True)
LevelName = db.Column(db.String(100), nullable=False)
AccessDescription = db.Column(db.Text)
class UsersAccessMapping(db.Model):
"""Each users' access level is defined here"""
__tablename__ = "UsersAccessMapping"
UsersAccessMappingID = db.Column(db.Integer, primary_key=True)
UserID = db.Column(
db.Integer, db.ForeignKey("Users.UserID", ondelete="CASCADE"), nullable=False
)
UsersAccessLevelID = db.Column(
db.Integer,
db.ForeignKey("UsersAccessLevels.UsersAccessLevelID", ondelete="CASCADE"),
nullable=False
)
The Constraints and such are automagically handled with the db.ForeignKey() parameters in the column definition. It does not need to be done on the Table directly, like in SQL.
The names for the foreign keys appear to be automatically generated by SQLAlchemy also. Here's how it looks like in the database:

Many to many relationship with a composite key on SQLAlchemy

Let's say I have the following model:
class Molecule(Base):
db = Column(Integer, primary_key=True)
id = Column(Integer, primary_key=True)
data = Column(Integer)
class Atom(Base):
id = Column(Integer, primary_key=True)
weight = Column(Integer)
And I want to establish a many-to-many relationship between Molecule and Atom, what would be the best way to do it? Notice that the primary key of Molecule is composite.
Thanks
many-to-many association tables should be defined like this:
molecule2atom = Table(
'molecule2atom',
Base.metadata,
Column('molecule_db', Integer),
Column('molecule_id', Integer),
Column('atom_id', Integer, ForeignKey('atom.id')),
ForeignKeyConstraint(
('molecule_db', 'molecule_id'),
('molecule.db', 'molecule.id') ),
)
And add the relatiohship to one of the models as usual, for example, in Class Atom add:
molecules = relationship("Molecule", secondary=molecule2atom, backref="atoms")
I liked the solution given here better - composite key many to many
If you're using an association table or fully declared table metadata, you can use the primary_key=True in both columns, as suggested here.
Association table example:
employee_role = db.Table(
"employee_role",
db.Column("role_id", db.Integer, db.ForeignKey("role.id"), primary_key=True),
db.Column("employee_id", db.Integer, db.ForeignKey("agent.id"), primary_key=True),
)
Metadata example:
# this is using SQLAlchemy
class EmployeeRole(Base):
__tablename__ = "employee_role"
role_id = Column(Integer, primary_key=True)
employee_id = Column(Integer, primary_key=True)
# this is using Flask-SQLAlchemy with factory pattern, db gives you access to all SQLAlchemy stuff
class EmployeeRole(db.Model):
__tablename__ = "employee_role"
role_id = db.Column(db.Integer, primary_key=True)
employee_id = db.Column(db.Integer, primary_key=True)
Alembic migration for it:
op.create_table(
'employee_role',
sa.Column('role_id', sa.Integer(), nullable=False),
sa.Column('employee_id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('role_id', 'employee_id')
)
SQL:
CREATE TABLE agent_role (
role_id INTEGER NOT NULL,
employee_id INTEGER NOT NULL,
PRIMARY KEY (role_id, employee_id)
);
In terms of relationship, declare it on one side (this should give you role.employees or employee.roles which should return a list):
# this is using Flask-SQLAlchemy with factory pattern, db gives you access to all SQLAlchemy stuff
class Employee(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
roles = db.relationship("Role", secondary=employee_role, backref="employee")
Your Role class can be:
# this is using Flask-SQLAlchemy with factory pattern, db gives you access to all SQLAlchemy stuff
class Role(db.Model):
__tablename__ = "role"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(25), nullable=False, unique=True)

Categories

Resources