Select from table defined by SQLAlchemy declarative_base - python

I am working on FastAPI tutorial and I am trying to create tables using SQLAlchemy+Alembic+databases.
In my main.py I have:
from typing import List
import databases
import sqlalchemy
from fastapi import FastAPI
from pydantic import BaseModel
from sqlalchemy import Table
DATABASE_URL = "sqlite:///./test.db"
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
notes = sqlalchemy.Table(
"note",
metadata,
sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
sqlalchemy.Column("text", sqlalchemy.String),
sqlalchemy.Column("completed", sqlalchemy.Boolean),
)
class Note2(BaseModel):
id: int
text: str
completed: bool
app = FastAPI()
#app.on_event("startup")
async def startup():
await database.connect()
#app.on_event("shutdown")
async def shutdown():
await database.disconnect()
#app.get("/notes/", response_model=List[Note2])
async def read_notes():
query = notes.select()
return await database.fetch_all(query)
And this works - I can GET /notes/ endpoint. But it looks newbie to create a datatabase table in the same module with endpoints, so I decided to make models.py file and create a normal model there, like this:
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Note(Base):
__tablename__ = "note"
id = sa.Column(sa.Integer, primary_key=True)
text = sa.Column(sa.String)
completed = sa.Column(sa.Boolean)
And here comes a problem - when I change the endpoint like this:
from app_second.models import Note
#app.get("/notes/", response_model=List[Note2])
async def read_notes():
query = Note().select()
return await database.fetch_all(query)
I recieve an error:
AttributeError: 'Note' object has no attribute 'select'
As it is mentioned here - declarative_base() is just a syntactic shugar for Table + mapper. But what is the right way to select/filter/update tables declared that way?

You can use the table property to access the table methods and then use database.fetch_all() or similar. E.g:
from sqlalchemy import select
...
skip = 0
limit = 100
query = (
Note.__table__.select()
.offset(skip)
.limit(limit)
)
return await database.fetch_all(query)

Related

Is there a way to combine a pydantic and SQLite model into one class?

I'm using fastapi and SQLite to make a simple API that can write to a database.
The pydantic model (Component class in main.py) is almost identical to the SQLite one (Component class in models.py). These two models could end up having many more properties. Each added property adds a line of duplicated code to the create and update methods that exists only to copy data from one model to the other. It seems like there must be a more elegant way to do this (sharing a model, multiple inheritance?) rather than defining two nearly identical classes and copying individual properties line by line multiple times.
models.py
A simple SQLite model:
from sqlalchemy import Column, Integer, String
from database import Base
class Component(Base):
__tablename__ = "components"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
author = Column(String)
description = Column(String)
main.py
I use fastapi and pydantic to define a basic CRUD API in a main.py. I've included only two of the operations:
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from database import engine, SessionLocal
import models
app = FastAPI()
models.Base.metadata.create_all(bind=engine)
def get_db():
try:
db = SessionLocal()
yield db
finally:
db.close()
class Component(BaseModel):
name: str = Field(min_length=1)
author: str = Field(min_length=1, max_length=100)
description: str = Field(min_length=1, max_length=100)
#app.post("/")
def create_component(component: Component, db: Session = Depends(get_db)):
c = models.Component()
c.name = component.name
c.author = component.author
c.description = component.description
db.add(c)
db.commit()
return component
#app.put("/{component_id}")
def update_component(component_id: int, component: Component, db: Session = Depends(get_db)):
c.name = component.name
c.author = component.author
c.description = component.description
db.add(c)
db.commit()
return component

Put method does not update the database but it shows 200 as a response

I am Facing Issue With put Method it shows me 200 Successfully response but it does not updated on database.
database.py:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
sql_database_url="mysql://root:zeehan#localhost:3306/mybookstore"
engine=create_engine(sql_database_url)
sessionLocal=sessionmaker(autocommit=False,bind=engine)
base=declarative_base()
This is my database file
main.py:
from typing import Optional
from fastapi import FastAPI,Query,Depends,Path,HTTPException
from sqlalchemy import Column,String,Integer,Float
from pydantic import BaseModel, errors
from sqlalchemy.orm import Session, query, session
from sqlalchemy import create_engine
from sqlalchemy.sql.schema import MetaData
from sqlalchemy import databases
from book_Database import engine,sessionLocal,base,sessionmaker
app=FastAPI()
class BookStore(base):
__tablename__="mybookstore"
id=Column(Integer,primary_key=True,index=True,autoincrement=True)
name=Column(String(255),index=True)
author=Column(String(225))
rating=Column(Float)
description=Column(String(225))
class BookStoreSchema(BaseModel):
name:str
author:str
rating:float
description:str
class BookStoreUpdateSchema(BaseModel):
id=int
name:str
author:str
rating:float
description:str
class Config:
orm_model=True
def get_bookdb():
db=sessionLocal()
try:
yield db
finally:
db.close()
base.metadata.create_all(bind=engine)
#app.get("/books")
def getstudent(db:Session=Depends(get_bookdb)):
return db.query(BookStore).all()
#app.get("/books/{id}")
def getstudent_by_name(id:int,db:Session=Depends(get_bookdb)):
bookstore = db.query(BookStore).get(id)
return bookstore
#app.post("/create-book")
def creatBook(*,BookStoreSchema:BookStoreSchema,rating :float=Query( None,gt=0,lt=6)):
bookstore = BookStore( name=BookStoreSchema.name,author=BookStoreSchema.author,rating=BookStoreSchema.rating ,description=BookStoreSchema.description)
with Session(bind=engine) as session:
session.add(bookstore)
session.commit()
return bookstore
#app.put("/update-book/{id}")
def update_student(id:int,bookstore:BookStoreUpdateSchema,db:Session=Depends(get_bookdb)):
bookstore = db.query(BookStore).get(id)
BookStore.name = bookstore.name
BookStore.author=bookstore.author
BookStore.rating=bookstore.rating
BookStore.description=bookstore.description
db.commit()
return bookstore
When I try to run this API using UVICorn I was running successfully but there is some issue with the put method. It shows successful response but it does not update the content of database.
In your database.py file, declarative_base() returns a class so instead of base make it Base
Base = declarative_base()
class BookStore(Base):
pass
In your main.py file for createBook function try something simple first:
#app.post("/create-book")
def creat_a_book(new_book: BookStoreSchema, db: Session = Depends(get_bookdb)):
book = BookStore(**new_book())
db.add(book)
db.commit()
db.refresh(book)
return book
change the variable name of the updated book in your update function arguments, and in your schema definition change orm_model to orm_mode let's call it updated_book
Try grabbing your book from bookstore like this:
bookstore = db.query(BookStore).filter(BookStore.id == id)
and update the book with the update function
bookstore.update(updated_book.dict(), synchronize_session=False)
db.commit()

SQLAlchemy ORM Relationship stopped working

I built an API using FastAPI, sqlalchemy, and pydantic. The ORM modeling was working great for some time, but I updated my code and broke the relational mapping somehow. Please help me figure out what I am doing wrong in my ORM mapping.
When I go to http://localhost:8000/test-rel I expect to see this:
[{"name":"P One","pk_id":1,"fk_id":1, "test":{"test_id":1, "test_name":"One"}},{"name":"P Two","pk_id":2,"fk_id":2, "test":{"test_id":2, "test_name":"Two"}},{"name":"P Three","pk_id":3,"fk_id":null, "test": null}]
but instead I see this:
[{"name":"P One","pk_id":1,"fk_id":1},{"name":"P Two","pk_id":2,"fk_id":2},{"name":"P Three","pk_id":3,"fk_id":null}]
Here is the test code portion of my project that I was using to troubleshoot:
schemas.py
from typing import Optional
from pydantic import BaseModel
class Test(BaseModel):
test_id: int
test_name: str
class TestP(BaseModel):
pk_id: int
fk_id: Optional[int]
name: Optional[str]
test: Test
class Config:
orm_mode = True
models.py
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DATE, DECIMAL
from sqlalchemy.orm import relationship
from sql_app.database import Base
class Test(Base):
__tablename__ = "test"
test_id = Column(Integer, primary_key=True)
test_name = Column(String)
class TestP(Base):
__tablename__ = "test_p"
pk_id = Column(Integer, primary_key=True)
fk_id = Column(Integer, ForeignKey("test.test_id"), nullable=True)
name = Column(String, nullable=True)
test = relationship("Test")
database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import urllib
params = urllib.parse.quote_plus("DRIVER={SQL Server Native Client 11.0};"
"SERVER=SQLEXPRESS;"
"DATABASE=Test;"
"Trusted_Connection=yes")
engine = create_engine(
"mssql+pyodbc:///?odbc_connect={}".format(params)
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
crud.py
from sqlalchemy.orm import Session
from sqlalchemy import and_, update, delete
from fastapi.encoders import jsonable_encoder
from typing import List
from sql_app import models
from sql_app import schemas
def test_rel(db: Session) -> List[schemas.TestP]:
return db.query(models.TestP).all()
main.py
import babel.numbers as bn
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from sql_app import crud, models, schemas
from sql_app.database import SessionLocal, engine
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
#app.get("/test-rel/")
def test_rel(db: Session = Depends(get_db)):
return crud.test_rel(db=db)
Answered on github discussions by CaseIIT:
A couple of things:
def test_rel(db: Session) -> List[schemas.TestP]:
return db.query(models.TestP).all()
This returns a List[model.TestP]
Also the relationships are lazy loaded by default. Look at https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html for the eager loading options
My Response:
This was exactly what I was looking for. I was unaware that relationships were lazy loaded by default. My code is using pydantic to validate the response and my queries were failing validation because the pydantic validation was not seeing the joined tables. I updated my code to add eager loading and it is now working exactly as I expect it to.
test = relationship("Test", lazy='joined')

Custom engine creation SQLAlchemy in module

I have a project where I want to isolate DB initialization (SQLAlchemy) from the other module so I've create a module
db_initializer.py:
engine = create_engine('sqlite:///db') # TODO from config file
Session = sessionmaker(bind=engine)
Base = declarative_base(bind=engine)
def create_tables():
Base.metadata.create_all(engine)
First of all I need to put create_all in a function because my model is in another package.
model/foo.py:
from core.db_initializer import Base
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String)
def __init__(self, name: str = None):
self.name = name
def __repr__(self):
return "<User(id=%d, name=%s)>" % (int(self.id), str(self.name))
And my main call create_tables.
Is there any other to do that? And now I need to create the engine with custom config (IP,User, ...) and only the main script know the config it's possible?
Something like
main.py:
import db_initializer as db
import model.foo
db.init(config) # which create the engine and create table
When I do something like that I got problem with the Base object which have not bind to the engine which is not created yet. Any solutions?
You don't need to create engine or session before declaring your models. You can declare models in model/foo.py:
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
Let's assume you have some application in myapp.py. Declare it so it can be initialized with engine:
from sqlalchemy.orm import Session
import model.foo
class MyApp:
def __init__(self, engine):
self.engine = engine
def get_users(self):
session = Session(self.engine)
users = session.query(model.foo.User).all()
session.close()
return users
Create engine in main.py and use it to initialize models.foo.Base.metadata and other applications where you need it:
from sqlalchemy import create_engine
import model.foo
import myapp
engine = create_engine('sqlite:///db')
model.foo.Base.metadata.bind = engine
model.foo.Base.metadata.create_all()
app = myapp.MyApp(engine)
UPD: For scoped_session approach myapp.py can be look like this:
import model.foo
class MyApp:
def __init__(self, session):
self.session = session
def get_users(self):
return self.session.query(model.foo.User).all()
And main.py:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session
import model.foo
import myapp
engine = create_engine('sqlite:///db')
session = scoped_session(sessionmaker(engine))
model.foo.Base.metadata.bind = engine
model.foo.Base.metadata.create_all()
app = myapp.MyApp(session)

Instantiating object automatically adds to SQLAlchemy Session. Why?

From my understanding of SQLAlchemy, in order to add a model to a session, I need to call session.add(obj). However, for some reason, in my code, SQLAlchemy seems to do this automatically.
Why is it doing this, and how can I stop it? Am I approaching session in the correct way?
example
>>> from database import Session as db
>>> import clients
>>> from instances import Instance
>>> from uuid import uuid4
>>> len(db.query(Instance).all())
>>> 0 # Note, no instances in database/session
>>> i = Instance(str(uuid4()), clients.get_by_code('AAA001'), [str(uuid4())])
>>> len(db.query(Instance).all())
>>> 1 # Why?? I never called db.add(i)!
database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base
import config
Base = declarative_base()
class Database():
def __init__(self):
db_url = 'postgresql://{:s}:{:s}#{:s}:{}/{:s}'.format(
config.database['user'],
config.database['password'],
config.database['host'],
config.database['port'],
config.database['dbname']
)
self.engine = create_engine(db_url)
session_factory = sessionmaker(bind=self.engine)
self.session = scoped_session(session_factory)
Database = Database()
Session = Database.session
instance.py
from sqlalchemy import Column, Text, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID, ARRAY
import database
Base = database.Base
class Instance(Base):
__tablename__ = 'instances'
uuid = Column(UUID, primary_key=True)
client_code = Column(
Text, ForeignKey('clients.code', ondelete='CASCADE'), nullable=False)
mac_addresses = Column(ARRAY(Text, as_tuple=True),
primary_key=True)
client = relationship("Client", back_populates="instances")
def __init__(self, uuid, client, mac_addresses):
self.uuid = uuid
self.client = client
self.mac_addresses = tuple(mac_addresses)
client.py
from sqlalchemy import Column, Text
from sqlalchemy.orm import relationship
import database
from database import Session as db
Base = database.Base
class Client(Base):
__tablename__ = 'clients'
code = Column(Text, primary_key=True)
name = Column(Text)
instances = relationship("Instance", back_populates='client')
def __init__(self, code, name=None):
self.code = code
self.name = name
def get_by_code(code):
client = db.query(Client).filter(Client.code == code).first()
return client
When you create a SQLAlchemy object and link it directly to another SQLAlchemy object, both objects end up in the session.
The reason is that SQLAlchemy needs to make sure you can query these objects.
Take, for example, a user with addresses.
If you create a user in code, with an address, both the user and the address end up in the session, because the address is linked to the user and SQLAlchemy needs to make sure you can query all addresses of a user using user.addresses.all().
In that case all (possibly) existing addresses need to be fetched, as well as the new address you just added. For that purpose the newly added address needs to be saved in the database.
To prevent this from happening (for example if you only need objects to just calculate with), you can link the objects with their IDs/Foreign Keys:
address.user_id = user.user_id
However, if you do this, you won't be able to access the SQLAlchemy properties anymore. So user.addresses or address.user will no longer yield results.
The reverse is also true; I asked a question myself a while back why linking two objects by ID will not result in SQLAlchemy linking these objects in the ORM:
relevant stackoverflow question
another description of this behavior

Categories