Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

fastapi + sqlalchemy + pydantic → how to process many-to-many relations

I have editors and articles. Many editors may be related to many articles and many articles may have many editors at same time.

My DB tables are

  • Article
id subject text
1 New Year Holidays In this year... etc etc etc
  • Editor
id name email
1 John Smith some@email
  • EditorArticleRelation
editor_id article_id
1 1

My models are

from sqlalchemy import Boolean, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship

from database import Base

class Editor(Base):
    __tablename__ = "editor"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(32), unique=False, index=False, nullable=True)
    email = Column(String(115), unique=True, index=True)
    articles = relationship("Article",
                    secondary=EditorArticleRelation,
                    back_populates="articles",
                    cascade="all, delete")

class Article(Base):
    __tablename__ = "article"

    id = Column(Integer, primary_key=True, index=True)
    subject = Column(String(32), unique=True, index=False)
    text = Column(String(256), unique=True, index=True, nullable=True)
    editors = relationship("Editor",
                    secondary=EditorArticleRelation,
                    back_populates="editors",
                    cascade="all, delete")

EditorArticleRelation = Table('editorarticlerelation', Base.metadata,
    Column('editor_id', Integer, ForeignKey('editor.id')),
    Column('article_id', Integer, ForeignKey('article.id'))
)

My schemas are

from typing import Optional, List
from pydantic import BaseModel

class EditorBase(BaseModel):
    name: Optional[str]
    email: str

class EditorCreate(EditorBase):
    pass

class Editor(EditorBase):
    id: int

    class Config:
        orm_mode = True

class ArticleBase(BaseModel):
    subject: str
    text: str

class ArticleCreate(ArticleBase):
    # WHAT I NEED TO SET HERE???
    editor_ids: List[int] = []

class Article(ArticleBase):
    id: int
    editors: List[Editor] = []

    class Config:
        orm_mode = True

My crud

def create_article(db: Session, article_data: schema.ArticleCreate):
    db_article = model.Article(subject=article_data.subject, text=article_data.text, ??? HOW TO SET EDITORS HERE ???)
    db.add(db_article)
    db.commit()
    db.refresh(db_article)
    return db_article

My route

@app.post("/articles/", response_model=schema.Article)
def create_article(article_data: schema.ArticleCreate, db: Session = Depends(get_db)):
    db_article = crud.get_article_by_name(db, name=article_data.name)
    if db_article:
        raise HTTPException(status_code=400, detail="article already registered")
    if len(getattr(article_data, 'editor_ids', [])) > 0:
        ??? WHAT I NEED TO SET HERE???
    return crud.create_article(db=db, article_data=article_data)

What I want →

I want to post data for article creation API and automatically resolve and add editor relations, or raise error if some of editors doesn't exist:

{
  "subject": "Fresh news"
  "text": "Today is ..."
  "editor_ids": [1, 2, ...]
}

Questions are:

  1. How to correctly set crud operations (HOW TO SET EDITORS HERE place)?
  2. How to correctly set create/read schemas and relation fields (especially WHAT I NEED TO SET HERE place)?
  3. How to correctly set route code (especially WHAT I NEED TO SET HERE place)?
  4. If here is no possible to resolve relations automatically, what place will be better to resolve relations (check if editor exists, etc)? route or crud?
  5. Maybe my way is bad at all? If you know any examples how to handle many-to-many relations with pydantic and sqlalchemy, any information will be welcome
like image 493
rzlvmp Avatar asked Jul 15 '21 12:07

rzlvmp


People also ask

How do you create a many to many relationship in SQLAlchemy?

You add a tags class variable to the Post model. You use the db. relationship() method, passing it the name of the tags model ( Tag in this case). You pass the post_tag association table to the secondary parameter to establish a many-to-many relationship between posts and tags.

How can you perform many to many relationship in Python?

Many to Many relationship between two tables is achieved by adding an association table such that it has two foreign keys - one from each table's primary key.

What is Orm_mode?

orm_mode = True. It doesn't use : as for the type declarations before. This is setting a config value, not declaring a type. Pydantic's orm_mode will tell the Pydantic model to read the data even if it is not a dict , but an ORM model (or any other arbitrary object with attributes).

What is Backref in SQLAlchemy?

In Flask-SQLAlchemy, the backref parameter in relationship method allows you to declare a new property under a specified class as seen in the example in their docs: class Person(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50)) addresses = db.relationship('Address', backref='person ...


2 Answers

Not sure if my solution is most effective, but I did it by this way:

  • route (same as in question):
...
@app.post("/articles/", response_model=schema.Article)
def create_article(article_data: schema.ArticleCreate, db: Session = Depends(get_db)):
    db_article = crud.get_article_by_name(db, name=article_data.name)
    if db_article:
        raise HTTPException(status_code=400, detail="article already registered")
    return crud.create_article(db=db, article_data=article_data)
...
  • schema (same as in question):
...
class ArticleCreate(ArticleBase):
    editor_ids: List[int] = []
...
  • crud (solution is here):
def create_article(db: Session, article_data: schema.ArticleCreate):
    db_article = model.Article(subject=article_data.subject, text=article_data.text)
    if (editors := db.query(model.Editor).filter(model.Editor.id.in_(article_data.editor_ids))).count() == len(endpoint_data.topic_ids):
        db_article.topics.extend(editors)
    else:
        # even if at least one editor is not found, an error is raised
        # if existence is not matter you can skip this check and add relations only for existing data
        raise HTTPException(status_code=404, detail="editor not found")
    db.add(db_article)
    db.commit()
    db.refresh(db_article)
    return db_article

Any better ideas are welcome

like image 168
rzlvmp Avatar answered Nov 02 '22 14:11

rzlvmp


A solution I found.

def create_user_groups(db: Session, user_groups: schemas.UserGroupsBase):
    db_user = db.query(models.User).filter(models.User.id == user_groups.id_user).first()
    db_group = db.query(models.Group).filter(models.Group.id == user_groups.id_group).first()

    if not db_user and db_group:
        raise HTTPException(status_code=409, detail="User or Group not found in system.")

    db_user.groups.append(db_group)

    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user
like image 42
Brayan Avatar answered Nov 02 '22 15:11

Brayan