Reputation: 59
I have just followed and completed the entire SQLAlchemy part of the FastAPI documentation here, with my local MariaDB as my chosen database.
The code I have looks identical to the final files listed at the end. I've tested everything and it all works.
An issue arose when I tried to make my own database table called DHT ( Digital Humidity and Temperature ). It's meant to hold information on the humidity and temperature of my room at certain times during the day. When I test the endpoint for writing into the table, I get an error back. But the data does get saved into the table as illustrated further below.
This is what the SQLAlchemy ORM model looks like:
class DHT(Base):
__tablename__ = "dht"
datetime = Column(DateTime, primary_key=True, default=datetime.now)
humidity = Column(Float(8), index=True)
temp = Column(Float(8), index=True)
Here are the Pydantic schemas for data validation:
class DHTBase(BaseModel):
temp: float
humidity: float
class DHTCreate(DHTBase):
pass
class DHT(DHTBase):
"""
In the Pydantic models for reading, we add an internal Config class.
"""
date: datetime
class Config:
orm_mode = True
Everything starts here at the endpoint, I followed the other POST endpoint examples as best as I can.
@app.post("/dht/", response_model=schemas.DHT)
def create_dht(dht: schemas.DHTCreate,
db: Session = Depends(get_db)):
return crud.create_dht(db=db, dht_data=dht)
This is the crud operation that the endpoint calls:
def create_dht(db: Session, dht_data: schemas.DHTCreate):
dht = models.DHT(humidity=dht_data.humidity,
temp=dht_data.temp)
db.add(dht)
db.commit()
db.refresh(dht)
return dht
When I test the endpoint with curl, example something like this:
curl -X 'POST' \
'http://127.0.0.1:8000/dht/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"temp": 23.34,
"humidity": 55.35
}'
...FastAPI logs this error:
File "/home/sydney/.cache/pypoetry/virtualenvs/app-sfi98_Li-py3.10/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2328, in refresh
raise sa_exc.InvalidRequestError(
sqlalchemy.exc.InvalidRequestError: Could not refresh instance '<DHT at 0x7f09f52c5420>'
But when I check the database, an entry is in fact written.
MariaDB [blogfolio]> select * from dht;
+---------------------+----------+-------+
| datetime | humidity | temp |
+---------------------+----------+-------+
| 2022-03-26 10:00:11 | 55.35 | 23.34 |
+---------------------+----------+-------+
The reply I expected from FastAPI would be a JSON of this form.
{
"temp": 23.34,
"humidity": 55.35,
"date": "2022-03-26T17:00:11.815Z"
}
Instead I get "Internal Server Error".
I made a dummy version of the DHT table where the datetime column is just replaced with a string column and is filled with string data, and everything works just fine. So at this point the only thing I'm certain of is that it's coming from SQLAlchemy not liking either the datetime field of my ORM model, or how I'm writing the datetime data into it.
What's wrong with my ORM model / CRUD operation?
Upvotes: 1
Views: 4224
Reputation: 9049
The documentation for mariadb's datetime states that 0 microseconds is the default datetime precision. This appears to be so in your db output, ie. 2022-03-26 10:00:11
. Also it appears from the same documentation that the time zone depends on the session.
I was able to recreate this exception as well as a half-fix by using DATETIME(fsp=6)
. This is what appears to happen:
datetime.now()
as the primary key in your applicationdatetime = 2022-03-26T17:00:11.815
(note there are microseconds here)datetime
in the db trimmed those down to 2022-03-26T17:00:11
, so refresh fails with an exceptionRight here I'd say the safest strategy would be to use an integer id for the primary key but ...
To make this work first you could bump the precision up on the column to 6 places for microseconds. You have to use the dialect specific DATETIME
, from sqlalchemy.dialect.mysql import DATETIME
, with fsp=6
.
Then timezones... you should make sure the db session time zone matches the time zone you use to make the datetime in your app because mariadb does not do conversion. UTC is probably the safest thing here but I'm not sure of the specifics of mariadb.
You might have to change the server's default itself. This might work for the session, session.execute(text("SET time_zone = 'UTC'"))
.
In python you could do default = lambda: datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
via import datetime
. This will get the current time in UTC but then remove the timezone to make the dt naive.
Upvotes: 1