Best practice for unique constraint validation - python

Coming from Django and DRF unique key validation is a piece of cake.
I'm trying to understand what is the best practice to this kind of validation.
I have some keys that need to be unique within the db table, but I can't figure out how to do so.
I looked for answer in pydantic's repo and they are say it is a bad practice to have a validator on the schema which queries the database and checks for duplicate.
so, what is the best practice to validate a unique key value?
# crud create
def create(self, db: Session, obj: CreateSchemaType) -> ModelType:
obj_data = jsonable_encoder(obj)
db_obj = self.model(**obj_data)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
# model
class Company(Base):
__tablename__ = "companies"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
name = Column(String, nullable=False, index=True)
business_number = Column(String, nullable=False, index=True)
carrier_license_exp_date = Column(Date)
professional_manager = Column(JSON, nullable=False)
address = Column(JSON)
contact = Column(JSON)
email = Column(String)
fax_number = Column(String)
phone_number = Column(String)
# schema
class CompanyBase(BaseModel):
name: str
business_number: str
professional_manager: ProfessionalManager
carrier_license_exp_date: date | None
contact: Contact | None = None
address: Address | None = None
phone_number: str | None = None
fax_number: str | None = None
email: EmailStr | None = None
# endpoint
#router.post(
"/",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Company,
response_model_exclude_none=True,
)
async def create_company(
company_obj: schemas.CompanyCreate,
db: Session = Depends(deps.get_db),
) -> Any:
company = crud.company.create(db=db, obj=company_obj)
return company
example case:
The values of the keys name and business_number should be unique.

As Gord says, the best practice is to let the database handle those kinds of constraints. That way you can be sure your data is consistent, no matter how it ends up in the database (even with pydantic validation, someone might e.g. want to import a list of companies later and forget to use that specific validation).
Also, since you don't have the entire database in memory, you're gonna have to run a database query either way, so first checking if the data is OK with a select query and then inserting would result in two database calls instead of one, and more code to maintain.
See this part of the SQLAlchemy documentation for examples. In your example, it would be:
class Company(Base):
__tablename__ = "companies"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
name = Column(String, nullable=False, unique=True)
business_number = Column(String, nullable=False, unique=True)
...
Or, if the combination of name and business_number should be unique:
from sqlalchemy import UniqueConstraint
class Company(Base):
__tablename__ = "companies"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
name = Column(String, nullable=False)
business_number = Column(String, nullable=False)
...
__table_args__ = (
UniqueConstraint('name', 'business_number', name='_name_business_number_uc'),
)

Related

SQLAlchemy - How to correctly connect two sets of data?

I am hoping for some guidance about what I believe is going to be a common pattern in SQLAlchemy for Python. However, I have so far failed to find a simple explanation for someone new to SQLAlchemy.
I have the follow objects:
Customers
Orders
Products
I am building a Python FastAPI application and I want to be able to create customers, and products individually. And subsequently, I want to then be able to create an order for a customer that can contain 1 or more products. A customer will be able to have multiple orders also.
Here are my SQLAlchemy models:
order_products = Table('order_products', Base.metadata,
Column('order_id', ForeignKey('orders.id'), primary_key=True),
Column('product_id', ForeignKey('products.id'), primary_key=True)
)
class Customer(Base):
__tablename__ = "customers"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
address = Column(String)
phonenumber = Column(String)
email = Column(String, unique=True, index=True)
is_active = Column(Boolean, default=True)
orders = relationship("Order", back_populates="customers")
class Order(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True, index=True)
ordernumber = Column(String, index=True)
customer_id = Column(Integer, ForeignKey("customers.id"))
customers = relationship("Customer", back_populates="orders")
products = relationship("Product", secondary="order_products", back_populates="orders")
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
size = Column(Integer)
order_id = Column(Integer, ForeignKey("orders.id"))
orders = relationship("Order", secondary="order_products", back_populates="products")
And here are my CRUD operations:
def create_customer(db: Session, customer: customer.CustomerCreate):
db_customer = models.Customer(name = customer.name, address = customer.address, email=customer.email, phonenumber=customer.phonenumber)
db.add(db_customer)
db.commit()
db.refresh(db_customer)
return db_customer
def create_product(db: Session, product: product.Productreate):
db_product = models.Product(name = product.name, size = product.size)
db.add(db_product)
db.commit()
db.refresh(db_product)
return db_product
def create_order(db: Session, order: order.OrderCreate, cust_id: int):
db_order = models.Order(**order.dict(), customer_id=cust_id)
db.add(db_order)
db.commit()
db.refresh(db_order)
return db_order
def update_order_with_product(db: Session, order: order.Order):
db_order = db.query(models.Order).filter(models.Order.id==1).first()
if db_order is None:
return None
db_product = db.query(models.Order).filter(models.Product.id==1).first()
if db_order is None:
return None
db_order.products.append(db_product)
db.add(db_order)
db.commit()
db.refresh(db_order)
return db_order
All of the CRUD operations work apart from update_order_with_product which gives me this error:
child_impl = child_state.manager[key].impl
KeyError: 'orders'
I'm not sure if I am taking the correct approach to the pattern needed to define the relationships between my models. If not, can someone point me in the right direction of some good examples for a beginner?
If my pattern is valid then there must be an issue with my CRUD operation trying to create the relationships? Can anyone help with that?
This query could be a problem:
db_product = db.query(models.Order).filter(models.Product.id==1).first()
Should probably be:
db_product = db.query(models.Product).filter(models.Product.id==1).first()
because you want to get a Product instance, not Order.
When you update a record you should not add it to the session (because it has been registered to the session when you queried the record).
def update_order_with_product(db: Session, order: order.Order):
db_order = db.query(models.Order).filter(models.Order.id==1).first()
if db_order is None:
return None
db_product = db.query(models.Product).filter(models.Product.id==1).first()
if db_product is None:
return None
db_order.products.append(db_product)
db.commit()
db.refresh(db_order)
return db_order

Flask SQLAlchemy: adding third column to joining table

Context: I'm making an auctioning website for which I am using Flask-SQLAlchemy. My tables will need to have a many-to-many relationship (as one artpiece can have many user bids and a user can bid on many artpieces)
My question is: it is possible to add another column to my joining table to contain the id of the user bidding, the id of artpiece that they are bidding on and also how much they bid? Also if yes, how would I include this bid in the table when I add a record to said table?
bid_table = db.Table("bid_table",
db.Column("user_id", db.Integer, db.ForeignKey("user.user_id")),
db.Column("item_id", db.Integer, db.ForeignKey("artpiece.item_id"))
)
class User(db.Model):
user_id = db.Column(db.Integer, unique=True, primary_key=True, nullable=False)
username = db.Column(db.Integer, unique=True, nullable=False)
email = db.Column(db.String(50), unique =True, nullable=False)
password = db.Column(db.String(60), nullable=False)
creation_date = db.Column(db.DateTime, default=str(datetime.datetime.now()))
bids = db.relationship("Artpiece", secondary=bid_table, backref=db.backref("bids", lazy="dynamic"))
class Artpiece(db.Model):
item_id = db.Column(db.Integer, unique=True, primary_key=True, nullable=False)
artist = db.Column(db.String(40), nullable=False)
buyer = db.Column(db.String(40), nullable=False)
end_date = db.Column(db.String(40))
highest_bid = db.Column(db.String(40))
It is possible to do this with SQL Alchemy, but it's very cumbersome in my opinion.
SQLAlchemy uses a concept called an Association Proxy to turn a normal table into an association table. This table can have whatever data fields you want on it, but you have to manually tell SQLAlchemy which columns are foreign keys to the other two tables in question.
This is a good example from the documentation.
In your case, the UserKeyword table is the association proxy table that you want to build for your user/bid scenario.
The special_key column is the arbitrary data you would store like the bid amount.
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import backref, declarative_base, relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(64))
# association proxy of "user_keywords" collection
# to "keyword" attribute
keywords = association_proxy('user_keywords', 'keyword')
def __init__(self, name):
self.name = name
class UserKeyword(Base):
__tablename__ = 'user_keyword'
user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
keyword_id = Column(Integer, ForeignKey('keyword.id'), primary_key=True)
special_key = Column(String(50))
# bidirectional attribute/collection of "user"/"user_keywords"
user = relationship(User,
backref=backref("user_keywords",
cascade="all, delete-orphan")
)
# reference to the "Keyword" object
keyword = relationship("Keyword")
def __init__(self, keyword=None, user=None, special_key=None):
self.user = user
self.keyword = keyword
self.special_key = special_key
class Keyword(Base):
__tablename__ = 'keyword'
id = Column(Integer, primary_key=True)
keyword = Column('keyword', String(64))
def __init__(self, keyword):
self.keyword = keyword
def __repr__(self):
return 'Keyword(%s)' % repr(self.keyword)
Check out the full documentation for instructions on how to access and create this kind of model.
Having used this in a real project, it's not particularly fun and if you can avoid it, I would recommend it.
https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html

How to remove 1 record from a db.Table using Flask_SQLAlchemy?

I have a very simple many-to-many table structure and I'm having problems removing records from the table that makes the association between the other two:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
user_book = db.Table('user_book',
db.Column('uid', db.Integer, db.ForeignKey('user.uid'), primary_key=True),
db.Column('bid', db.Text, db.ForeignKey('book.bid'), primary_key=True),
db.Column('date_added', db.DateTime(timezone=True), server_default=db.func.now())
)
class User(db.Model):
__tablename__ = 'user'
uid = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(25), nullable=False)
hash = db.Column(db.String(), nullable=False)
first_name = db.Column(db.String(30), nullable=True)
last_name = db.Column(db.String(80), nullable=True)
books = db.relationship('Book', secondary=user_book)
class Book(db.Model):
__tablename__ = 'book'
bid = db.Column(db.Text, primary_key=True)
title = db.Column(db.Text, nullable=False)
authors = db.Column(db.Text, nullable=False)
thumbnail = db.Column(db.Text, nullable=True)
users = db.relationship('User', secondary=user_book)
To make it even clearer, here is an excerpt from the table with some records:
In the function that removes a record I did it this way:
def remove(book_id):
# get the user id (uid)
user_id = db.session.query(User).filter_by(email=session['email']).first().uid
# match the user id with the book id on table 'user_book'
book_rm = db.session.query(user_book).filter_by(uid=user_id, bid=book_id).one()
db.session.delete(book_rm)
db.session.commit()
When I call this function I get the following error on the console:
Class 'sqlalchemy.engine.row.Row' is not mapped
So after some research on Stack and documentation, I tried to do it like this:
db.session.execute(user_book.delete(user_id).where(bid=book_id))
db.session.commit()
And in this case I have received the following:
SQL expression for WHERE/HAVING role expected, got 2.
I really don't know how to go about solving this. I would like to delete only 1 record from the user_book table. Does anyone know how to do this?
Given the a User instance and the Book instance to be deleted from the User's books, the Book can be removed like this:
user_instance.books.remove(book_instance)
db.session.commit()
So the remove function would look like this:
def remove(book_id):
# get the user
user = db.session.query(User).filter_by(email=session['email']).first()
# Find the book by its id
book_rm = Book.query.get(book_id)
user.books.remove(book_rm)
db.session.commit()
See the SQLAlchemy docs for more information.

Checks on foreign key of foreign key in SQLAlchemy

I have these 3 sql alchemy (sqla) models:
class Site(Base):
__tablename__ = "site"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow())
updated_at = Column(DateTime, nullable=True, default=datetime.utcnow(), onupdate=datetime.utcnow)
class Camera(Base):
__tablename__ = "camera"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
site_id = Column(UUID(as_uuid=True), ForeignKey("site.id"), nullable=False)
name = Column(String, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow())
updated_at = Column(DateTime, nullable=True, default=datetime.utcnow(), onupdate=datetime.utcnow)
site = relationship("Site", backref="cameras")
class RtspServerEndpoint(Base):
__tablename__ = "rtsp_server_endpoint"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
camera_id = Column(UUID(as_uuid=True), ForeignKey("camera.id"), nullable=False)
rtsp_url_endpoint = Column(String, nullable=False)
rtsp_username = Column(String, nullable=False)
rtsp_encrypted_password = Column(String, nullable=False)
name = Column(String, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow())
updated_at = Column(DateTime, nullable=True, default=datetime.utcnow(), onupdate=datetime.utcnow)
camera = relationship("Camera", backref="rtsp_server_endpoint", lazy="joined")
camera_id is the foreign key of rtspserverendpoint table and site_id is the foreign key for the Camera table.
When a user wants to add a new rtspserverendpoint record, he makes an HTTP request such as:
POST sites/<site_id>/camera/<camera_id>/rtspserverendpoint
Before adding the new rtspserverendpoint, I would like to make sure that the <site_id> and the <camera_id> are consistent, as a security. I can probably make a separate query just to check that, such as:
check_record_exist = db.session.query(Camera).filter(Camera.site_id == site_id).first()
if not check_record_exist:
raise ("No such camera with this site_id")
But what I would like to know, is if there is a more elegant way to check that: For example, adding a constraint in my Base models that would forbid adding such an inconsistent record in the database.
I am not aware of any straightforward way to implement this 2-level check on the database directly.
In fact, the only consistency that the database should know about is that your new RtspServerEndpoint instance will belong to the correct Camera instance. But this will be correct by default by the way you will be creating the RtspServerEndpoint instance.
Therefore, in my opinion, the check of the correctness of the site_id in the URL of the POST request should be implemented in the logic of your code. I would probably do it along these lines:
#handler(..., method='POST')
def rtspserverendpoint(site_id: int, camera_id: int):
# find camera, which will allow us to check the correctness of the site_id as well
camera = db.session.query(Camera).get(camera_id)
if camera is None:
raise Exception(f"No camera with this {camera_id=}.")
if camera.site_id != site_id:
raise Exception(f"Camera with this {camera_id=} does not belong to the site with {site_id=}.")
new_obj = RtspServerEndpoint(
...,
camera_id=camera_id,
...,
)
db.session.add(new_obj)
db.session.commit()

Stuck with Flask-Sqlalchemy Relationship system

I want to create database, that consist user info(sqlite db)
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
login = db.Column(db.String(10), unique = True)
email = db.Column(db.String(20), unique = True)
psw = db.Column(db.String(500), nullable=True)
def __repr__(self):
return f"<User {self.id}>"
This is what i already did.
I want to create fields upcoming_friends, incoming_friends, friends, i think that i need to create a new class that will extends user , but I did not find the documentation and don't understand how to do it.
The User table stores the information that you need about a particular user. If you want to find out what friends this user might have in your application, then you can create another table called Friends.
class Friend(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
upcoming_friends = db.Column(db.String(64), unique = True)
incoming_friends = db.Column(db.String(64), unique = True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def __repr__(self):
return f"<Friends: {self.id}>"
To create a relationship between these two database structures, we will do as follows:
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
login = db.Column(db.String(10), unique = True)
email = db.Column(db.String(20), unique = True)
friends = db.relationship('Friend', backref='<give-a-reference>', lazy='dynamic')
def __repr__(self):
return f"<User {self.id}>"
The user_id field was initialized as a foreign key to user.id, which means that it references an id value from the user's table. In this reference the user part is the name of the database table for the model.
There is a bit of inconsistency when it comes to referring to the user table in db.ForeignKey. Here, you can see that the user table starts with a lower case, whereas when it comes to referencing Friend table in db.relationship we begin with an upper case.

Categories

Resources