One to one relation sqlalchemy - python

I am creating a TODO list. Each user has to have only one TODO list. ( I assume one to one model ).
If I set default value of todolist is None for each user, then create a todolist - it won't link it to the User. User.todolist will still be null. What should I do to have only one instance of todolist for each user?
My code:
class ToDoListBase(BaseModel):
items: List[ToDoItemCreate] = []
owner_id: int
class Config:
orm_mode = True
class UserBase(BaseModel):
todo: Optional[ToDoListCreate]
class Config:
orm_mode = True
Sqlalchemy models:
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
todolist = relationship('ToDoList', back_populates='owner', uselist=False)
class ToDoList(Base):
__tablename__ = 'todolists'
id = Column(Integer, primary_key=True, index=True)
owner_id = Column(Integer, ForeignKey('users.id'))
owner = relationship('User', back_populates='todolist')
What I expect to get is User where User.todolist = {items: [], owner_id: users.id}
what I get is User.todolist = null
and this is how I create todolist:
def get_or_create(self, owner_id: int, db: Session):
todolist = db.query(ToDoList).filter(ToDoList.owner_id==owner_id).first()
if not todolist:
todolist = ToDoList(
owner_id=owner_id,
)
db.add(todolist)
db.commit()
db.refresh(todolist)
return todolist

Related

How to get sqlalchemy relationship list in pydantic model?

when I try to get organization by id (inn) in devices_list field is null (device is exist):
{
inn: 8481406044,
organization_name: "slava bebrow",
devices_list: null
}
models.py:
class Organization(Base):
__tablename__ = "organizations_table"
inn = Column(BigInteger, primary_key=True, index=False)
organization_name = Column(String, nullable=False, unique=True)
devices = relationship("Device", backref="organizations_table")
class Device(Base):
__tablename__ = "devices_table"
uuid = Column(String, primary_key=True, index=False)
device_name = Column(String, nullable=False, unique=True)
organization_id = Column(BigInteger, ForeignKey("organizations_table.inn"), nullable=True)
pydantic_models.py:
class OrganizationBase(BaseModel):
inn: int
organization_name: str
class Config:
orm_mode = True
class Organization(OrganizationBase):
devices_list: list['DeviceBase'] = None
class DeviceBase(BaseModel):
uuid: str
device_name: str
organization_id: int | None
class Config:
orm_mode = True
functions to get organization:
def get_organization(db: Session, organization_id: int):
db_organization = db.query(models.Organization).filter(models.Organization.inn == organization_id).first()
if db_organization is None:
raise HTTPException(status_code=404, detail="Organization not found")
return db_organization
when i try to print(db_organization.devices[0].uuid) i get a list with objects
#app.get("/organizations/{organization_id}", response_model=pydantic_models.Organization)
def get_organization(organization_id, db: Session = Depends(get_db)):
return al.get_organization(db=db, organization_id=organization_id)
I think problem is in pydantic model, but i don't know how to fix it.
I expect a list of devices in field, not null

Trying to add a record to tables with many-to-many relationship using POST - FastAPI + SQLalchemy

I have some pydantic and SQLalchemy models
game_users = Table('game_users', Base.metadata,
Column('game_id', ForeignKey('games.id'), primary_key=True),
Column('user_id', ForeignKey('users.id'), primary_key=True)
)
class Game(Base):
__tablename__ = 'games'
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
users = relationship("User", secondary="game_users", back_populates='games')
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
age = Column(Integer)
email = Column(String, nullable=False, unique=True)
games = relationship("Game", secondary="game_users", back_populates='users')
class UserBase(BaseModel):
id: int
name: str
age: int = Query(ge=0, le=100)
email: str
class Config:
orm_mode = True
class GameBase(BaseModel):
id: int
name: str
class Config:
orm_mode = True
class UsersOut(UserBase):
games: List[GameBase]
class GamesOut(GameBase):
users: List[UserBase]
And I need to add entries by POST method, but I don't know how to do it exactly. I tried something like this:
#app.post('/connect/{uid}/{gid}')
def connect_to_game(uid: int, gid: int, db: Session = Depends(get_db)):
game = db.query(Game).filter(Game.id == gid).first()
user = db.query(User).filter(User.id == uid).first()
user_games = user(games=[game.id])
game_users = game(users=[user.id])
db.add_all([user_games, game_users])
db.commit()
return f'{game.name} successfully connected to {user.name}'
But it, certainly, doesn't work.
I tried to find information in pydantic and FastAPI documentations, but I couldn't. So I'll be really appreciated for any help or ideas.
I define models as:
game_users = Table('game_users', DBBase.metadata,
Column('game_id', ForeignKey('games.id'), primary_key=True),
Column('user_id', ForeignKey('users.id'), primary_key=True)
)
class Game(DBBase):
__tablename__ = 'games'
id = Column(Integer, primary_key=True)
name = Column(String(10), nullable=False)
users = relationship("User", secondary=game_users, backref='games')
class User(DBBase):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(10), nullable=False)
simplize back_populates by backref.
then assosicate objects:
>>> g1 = Game(name='g1')
>>> g2 = Game(name='g2')
>>> u1 = User(name='u1')
>>> u2 = User(name='u2')
>>> g1.users.extend([u1,u2])
>>> g2.users.extend([u1,u2])
>>> g1.users
[<models.User object at 0x105166b50>, <models.User object at 0x10519cfa0>]
>>> u1.games
[<models.Game object at 0x10512a280>, <models.Game object at 0x105166130>]
>>> s.add_all([g1,g2,u1,u2]) # s is db session
>>> s.commit()
So you just need to use the same type of adding relationships as above in your post method:
#app.post('/connect/{uid}/{gid}')
def connect_to_game(uid: int, gid: int, db: Session = Depends(get_db)):
game = db.query(Game).filter(Game.id == gid).first()
user = db.query(User).filter(User.id == uid).first()
game.users.append(user)
# no new instance, no need `add_all`
# db.add_all([user_games, game_users])
db.commit()
return f'{game.name} successfully connected to {user.name}'
Reference sqlalchemy many to many

Update property in all child objects if parent is updated in one to many relationship

I have a one to many relationship between a Project and a Note (i.e. a project will have many notes but a note belongs to a single project) in my Flask app:
class BaseDocument(db.Model):
__abstract__ = True
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, onupdate=datetime.now)
archived = db.Column(db.Boolean, default=False)
def __repr__(self):
return str(self.__dict__)
class Project(BaseDocument):
__tablename__ = "project"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
parent_id = db.Column(db.Integer, db.ForeignKey("project.id"))
notes = db.relationship(
"Note",
backref="project",
lazy=True,
order_by="Note.updated_at",
cascade="all, delete, delete-orphan",
)
class Note(BaseDocument):
__tablename__ = "note"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
content = db.relationship(
"Bullet", backref="note", lazy=True, order_by="Bullet.order"
)
project_id = db.Column(db.Integer, db.ForeignKey("project.id"))
I would like to do something that seems to be very simple but I can't figure out how: I want to update the archived property of all the child notes in a project to True if the parent project archived property is also set to True.
I can only find answers here in StackOverfow about how to update the parent object if a child is updated (the oposite of what I am trying to do), so I am assuming that what I want to do is trivial and I am just bad at sqlalchemy. How can I set this up? Do I need to use a after_update event on Project? If yes, how can I access all the child Notes and set archived=True for all of them?
I have tried to setup the following event listener with no success, I get the following error AttributeError: 'InstrumentedList' object has no attribute 'update':
#db.event.listens_for(Project, "after_update")
def archive_notes(mapper, connection, target):
obj = target.object
connection.execute(target.notes.update(archived=True))
Any help will be very appreciated!
You're on the right track by using after_update. Here's a working example:
import sqlalchemy as sa
from sqlalchemy.orm import declarative_base, relationship
connection_uri = (
"mssql+pyodbc://#localhost:49242/myDb?driver=ODBC+Driver+17+for+SQL+Server"
)
engine = sa.create_engine(
connection_uri,
future=True,
echo=False,
)
Base = declarative_base()
class Project(Base):
__tablename__ = "project"
id = sa.Column(sa.Integer, primary_key=True)
title = sa.Column(sa.Unicode(100), nullable=False)
archived = sa.Column(sa.Boolean, nullable=False, default=False)
class ProjectNote(Base):
__tablename__ = "project_note"
id = sa.Column(sa.Integer, primary_key=True)
project_id = sa.Column(sa.Integer, sa.ForeignKey("project.id"))
project = relationship(Project)
note_text = sa.Column(sa.Unicode(255), nullable=False)
archived = sa.Column(sa.Boolean, nullable=False, default=False)
#sa.event.listens_for(Project, "after_update")
def archive_remaining_project_notes(mapper, connection, target):
if target.archived:
sql = """\
UPDATE project_note SET archived = :yes
WHERE project_id = :proj_id
AND archived = :no
"""
connection.execute(
sa.text(sql),
{"yes": True, "no": False, "proj_id": target.id},
)
# <just for testing>
Base.metadata.drop_all(engine, checkfirst=True)
Base.metadata.create_all(engine)
# </just for testing>
p1 = Project(title="project 1")
p1n1 = ProjectNote(
project=p1, note_text="project 1, note 1, archived", archived=True
)
p1n2 = ProjectNote(project=p1, note_text="project 1, note 2, not archived")
with sa.orm.Session(engine, future=True) as session:
session.add_all([p1, p1n1, p1n2])
session.commit()
print(f"p1n2.archived is: {p1n2.archived}") # p1n2.archived is: False
p1.archived = True
session.commit()
print(f"p1.archived is: {p1.archived}") # p1.archived is: True
print(f"p1n2.archived is: {p1n2.archived}") # p1n2.archived is: True

Flask-Admin: Change sort order of inline_models?

In Flask-Admin, is there any way to control the order of the list generated by inline_models? It seems to be coming out in database order, i.e. ordered by the ID primary key.
That is, if I have an Author that has_many Books, and my AuthorModelView class has inline_models = (Books,), the books are always ordered by book_id. Passing column_default_sort to the inline model, to try to sort by (say) title or date_purchased, has no effect. Is there any way to handle this?
Specify the order_by parameter when specifying the relationships, see docs. See note at the end if you want to sort by a specific field at runtime.
Example of model declarations for Author -> Books. Here we are ordering on the book title field ascending - order_by='Book.title.asc()' :
class Author(db.Model):
__tablename__ = 'authors'
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.Text(length=255), nullable=False)
last_name = db.Column(db.Text(length=255), nullable=False)
books = db.relationship("Book", order_by='Book.title.asc()', cascade="all,delete-orphan", backref=db.backref('author'))
def __str__(self):
return f"ID: {self.id}; First Name: {self.first_name}; Last Name: {self.last_name}"
class Book(db.Model):
__tablename__ = 'books'
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey('authors.id'), nullable=False, index=True)
title = db.Column(db.Text(length=255), nullable=False)
def __str__(self):
return f"ID: {self.id}; Title: {self.title}; Author ID: {self.author_id}"
Single file full example:
from faker import Faker
import click
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin
from flask_admin.contrib import sqla
db = SQLAlchemy()
class Author(db.Model):
__tablename__ = 'authors'
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.Text(length=255), nullable=False)
last_name = db.Column(db.Text(length=255), nullable=False)
books = db.relationship("Book", order_by='Book.title.asc()', cascade="all,delete-orphan", backref=db.backref('author'))
def __str__(self):
return f"ID: {self.id}; First Name: {self.first_name}; Last Name: {self.last_name}"
class Book(db.Model):
__tablename__ = 'books'
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey('authors.id'), nullable=False, index=True)
title = db.Column(db.Text(length=255), nullable=False)
def __str__(self):
return f"ID: {self.id}; Title: {self.title}; Author ID: {self.author_id}"
app = Flask(__name__)
app.config['SECRET_KEY'] = '123456790'
app.config['SQLALCHEMY_ECHO'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///sample.sqlite'
db.init_app(app)
#app.cli.command('create-database', short_help='Create Authors database')
#click.option('--count', default=100, help='Number of authors (default 100)')
def create_database(count):
"""
Create database with "count" authors
"""
db.drop_all()
db.create_all()
_faker = Faker()
for _ in range(0, count):
_author = Author(
first_name=_faker.first_name(),
last_name=_faker.last_name(),
)
db.session.add(_author)
for _ in range(0, _faker.pyint(1, 20)):
_book = Book(
title=_faker.sentence(),
author=_author
)
db.session.add(_book)
db.session.commit()
class AuthorView(sqla.ModelView):
# default sort: last_name ascending
column_default_sort = ('last_name', False)
inline_models = (Book,)
# Flask views
#app.route('/')
def index():
return 'Click me to get to Admin!'
admin = Admin(app, template_mode="bootstrap3")
admin.add_view(AuthorView(Author, db.session))
if __name__ == '__main__':
app.run()
Run the following command to initialize an SQLite DB.
flask create-database --count 100
If you want to change the sort field at runtime override the view's get_one() method and use Python to sort the instrumented list directly. For example, sorting by ISBN field instead of title:
class Author2View(sqla.ModelView):
def get_one(self, id):
_author = super().get_one(id)
_author.books = sorted(_author.books, key=lambda book: book.isbn)
return _author
# default sort: last_name ascending
column_default_sort = ('last_name', False)
inline_models = (Book,)
admin.add_view(Author2View(Author, db.session, name="Author 2", endpoint='author-2'))

How to get parrent of object in session SQLAlchemy

I have two tables:
class Project(DataBase):
__tablename__ = 'projects'
id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
name = Column(String, nullable=False, unique=True)
domain = Column(String, nullable=False)
phrases = relationship("Phrase", backref='proj')
def __init__(self, name, domain):
self.name = name
self.domain = domain
class Phrase(DataBase):
__tablename__ = 'phrases'
query_id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
query_text = Column(String, nullable=False)
project = Column(Integer, ForeignKey('projects.id'), nullable=False)
enable = Column(Boolean, nullable=False, default=True)
def __init__(self, query_text, project, city):
self.query_text = query_text
self.project = project
And I have a function:
def get_first_query():
session = Session(bind=engine)
q = session.query(Phrase).filter(Phrase.enable == True).first()
session.close()
return q
I want to get an object from table 2 and than get its parrent from first table:
session = Session(bind=engine)
q = get_first_query()
print(q.proj)
It doesn't work and print this error:
sqlalchemy.orm.exc.DetachedInstanceError: Parent instance is not bound to a Session; lazy load operation of
attribute 'proj' cannot proceed
I can do this:
session = Session(bind=engine)
q = get_first_query()
q_project = session.query(Project).filter(Project.id == q.project)
But it's a bad way.
You can assess related object via proj attribute.
session.query(Phrase).filter(
Phrase.enable == True
).first().proj
This way you'll hit database one additional time to get it so you'll need to open session again. To avoid additional queries you can use joined load:
session.query(Phrase).filter(
Phrase.enable == True
).options(
joinedload('proj')
).first().proj

Categories