# ActiveModel: ORM Wrapper for SQLModel
No, this isn't *really* [ActiveModel](https://guides.rubyonrails.org/active_model_basics.html). It's just a wrapper around SQLModel that provides a more ActiveRecord-like interface.
SQLModel is *not* an ORM. It's a SQL query builder and a schema definition tool.
This package provides a thin wrapper around SQLModel that provides a more ActiveRecord-like interface with things like:
* Timestamp column mixins
* Lifecycle hooks
## Getting Started
First, setup your DB:
```python
import activemodel
activemodel.init("sqlite:///database.db")
```
Create models:
```python
from activemodel import BaseModel
from activemodel.mixins import TimestampsMixin, TypeIDMixin
class User(
BaseModel,
# optionally, obviously
TimestampsMixin,
# you can use a different pk type, but why would you?
# put this mixin last otherwise `id` will not be the first column in the DB
TypeIDMixin("user"),
# wire this model into the DB, without this alembic will not generate a migration
table=True
):
a_field: str
```
You'll need to create the models in the DB. Alembic is the best way to do it, but you can cheat as well:
```python
from sqlmodel import SQLModel
SQLModel.metadata.create_all(get_engine())
# now you can create a user! without managing sessions!
User(a_field="a").save()
```
Maybe you like JSON:
```python
from sqlalchemy.dialects.postgresql import JSONB
from pydantic import BaseModel as PydanticBaseModel
from activemodel import BaseModel
from activemodel.mixins import PydanticJSONMixin, TypeIDMixin, TimestampsMixin
class SubObject(PydanticBaseModel):
name: str
value: int
class User(
BaseModel,
TimestampsMixin,
PydanticJSONMixin,
TypeIDMixin("user"),
table=True
):
list_field: list[SubObject] = Field(sa_type=JSONB)
```
You'll probably want to query the model. Look ma, no sessions!
```python
User.where(id="user_123").all()
# or, even better, for this case
User.one("user_123")
```
Magically creating sessions for DB operations is one of the main problems this project tackles. Even better, you can set
a single session object to be used for all DB operations. This is helpful for DB transactions, [specifically rolling back
DB operations on each test.](#pytest)
## Usage
### Pytest
TODO detail out truncation and transactions
### Integrating Alembic
`alembic init` will not work out of the box. You need to mutate a handful of files:
* To import all of your models you want in your DB. [Here's my recommended way to do this.](https://github.com/iloveitaly/python-starter-template/blob/master/app/models/__init__.py)
* Use your DB URL from the ENV
* Target sqlalchemy metadata to the sqlmodel-generated metadata
* Most likely you'll want to add [alembic-postgresql-enum](https://pypi.org/project/alembic-postgresql-enum/) so migrations work properly
[Take a look at these scripts for an example of how to fully integrate Alembic into your development workflow.](https://github.com/iloveitaly/python-starter-template/blob/0af2c7e95217e34bde7357cc95be048900000e48/Justfile#L618-L712)
Here's a diff from the bare `alembic init` from version `1.14.1`.
```diff
diff --git i/test/migrations/alembic.ini w/test/migrations/alembic.ini
index 0d07420..a63631c 100644
--- i/test/migrations/alembic.ini
+++ w/test/migrations/alembic.ini
@@ -3,13 +3,14 @@
[alembic]
# path to migration scripts
# Use forward slashes (/) also on windows to provide an os agnostic path
-script_location = .
+script_location = migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
diff --git i/test/migrations/env.py w/test/migrations/env.py
index 36112a3..a1e15c2 100644
--- i/test/migrations/env.py
+++ w/test/migrations/env.py
@@ -1,3 +1,6 @@
+# fmt: off
+# isort: off
+
from logging.config import fileConfig
from sqlalchemy import engine_from_config
@@ -14,11 +17,17 @@ config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
+from sqlmodel import SQLModel
+from test.models import *
+from test.utils import database_url
+
+config.set_main_option("sqlalchemy.url", database_url())
+
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
-target_metadata = None
+target_metadata = SQLModel.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
diff --git i/test/migrations/script.py.mako w/test/migrations/script.py.mako
index fbc4b07..9dc78bb 100644
--- i/test/migrations/script.py.mako
+++ w/test/migrations/script.py.mako
@@ -9,6 +9,8 @@ from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
+import sqlmodel
+import activemodel
${imports if imports else ""}
# revision identifiers, used by Alembic.
```
Here are some useful resources around Alembic + SQLModel:
* https://github.com/fastapi/sqlmodel/issues/85
* https://testdriven.io/blog/fastapi-sqlmodel/
### Query Wrapper
This tool is added to all `BaseModel`s and makes it easy to write SQL queries. Some examples:
### Easy Database Sessions
I hate the idea f
* Behavior should be intuitive and easy to understand. If you run `save()`, it should save, not stick the save in a transaction.
* Don't worry about dead sessions. This makes it easy to lazy-load computed properties and largely eliminates the need to think about database sessions.
There are a couple of thorny problems we need to solve for here:
* In-memory fastapi servers are not the same as a uvicorn server, which is threaded *and* uses some sort of threadpool model for handling async requests. I don't claim to understand the entire implementation. For global DB session state (a) we can't use global variables (b) we can't use thread-local variables.
*
https://github.com/tomwojcik/starlette-context
### Example Queries
* Conditional: `Scrape.select().where(Scrape.id < last_scraped.id).all()`
* Equality: `MenuItem.select().where(MenuItem.menu_id == menu.id).all()`
* `IN` example: `CanonicalMenuItem.select().where(col(CanonicalMenuItem.id).in_(canonized_ids)).all()`
* Compound where query: `User.where((User.last_active_at != None) & (User.last_active_at > last_24_hours)).count()`
### SQLModel Internals
SQLModel & SQLAlchemy are tricky. Here are some useful internal tricks:
* `__sqlmodel_relationships__` is where any `RelationshipInfo` objects are stored. This is used to generate relationship fields on the object.
* `ModelClass.relationship_name.property.local_columns`
* Get cached fields from a model `object_state(instance).dict.get(field_name)`
* Set the value on a field, without marking it as dirty `attributes.set_committed_value(instance, field_name, val)`
* Is a model dirty `instance_state(instance).modified`
* `select(Table).outerjoin??` won't work in a ipython session, but `Table.__table__.outerjoin??` will. `__table__` is a reference to the underlying SQLAlchemy table record.
* `get_engine().pool.stats()` is helpful for inspecting connection pools and limits\
### TypeID
I'm a massive fan of Stripe-style prefixed UUIDs. [There's an excellent project](https://github.com/jetify-com/typeid)
that defined a clear spec for these IDs. I've used the python implementation of this spec and developed a clean integration
with SQLModel that plays well with fastapi as well.
Here's an example of defining a relationship:
```python
import uuid
from activemodel import BaseModel
from activemodel.mixins import TimestampsMixin, TypeIDMixin
from activemodel.types import TypeIDType
from sqlmodel import Field, Relationship
from .patient import Patient
class Appointment(
BaseModel,
# this adds an `id` field to the model with the correct type
TypeIDMixin("appointment"),
table=True
):
# `foreign_key` is a activemodel method to generate the right `Field` for the relationship
# TypeIDType is really important here for fastapi serialization
doctor_id: TypeIDType = Doctor.foreign_key()
doctor: Doctor = Relationship()
```
## Limitations
### Validation
SQLModel does not currently support pydantic validations (when `table=True`). This is very surprising, but is actually the intended functionality:
* https://github.com/fastapi/sqlmodel/discussions/897
* https://github.com/fastapi/sqlmodel/pull/1041
* https://github.com/fastapi/sqlmodel/issues/453
* https://github.com/fastapi/sqlmodel/issues/52#issuecomment-1311987732
For validation:
* When consuming API data, use a separate shadow model to validate the data with `table=False` and then inherit from that model in a model with `table=True`.
* When validating ORM data, use SQL Alchemy hooks.
<!--
This looks neat
https://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d2a4/src/my_model/model.py#L155
schema_extra={
'pattern': r'^[a-z0-9_\-\.]+\@[a-z0-9_\-\.]+\.[a-z\.]+$'
},
extra constraints
https://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d2a4/src/my_model/model.py#L424C1-L426C6
-->
## Related Projects
* https://github.com/woofz/sqlmodel-basecrud
* https://github.com/0xthiagomartins/sqlmodel-controller
* https://github.com/litestar-org/advanced-alchemy?tab=readme-ov-file
## Inspiration
* https://github.com/peterdresslar/fastapi-sqlmodel-alembic-pg
* [Albemic instructions](https://github.com/fastapi/sqlmodel/pull/899/files)
* https://github.com/fastapiutils/fastapi-utils/
* https://github.com/fastapi/full-stack-fastapi-template
* https://github.com/DarylStark/my_data/
* https://github.com/petrgazarov/FastAPI-app/tree/main/fastapi_app
## Upstream Changes
- [ ] https://github.com/fastapi/sqlmodel/pull/1293
Raw data
{
"_id": null,
"home_page": null,
"name": "activemodel",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": null,
"keywords": "activemodel, activerecord, orm, sqlalchemy, sqlmodel",
"author": null,
"author_email": "Michael Bianco <iloveitaly@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/f1/c4/e884e218eeff8f76457deaf3ab2da236278060045b6cc66c82afe8f3552e/activemodel-0.13.0.tar.gz",
"platform": null,
"description": "# ActiveModel: ORM Wrapper for SQLModel\n\nNo, this isn't *really* [ActiveModel](https://guides.rubyonrails.org/active_model_basics.html). It's just a wrapper around SQLModel that provides a more ActiveRecord-like interface.\n\nSQLModel is *not* an ORM. It's a SQL query builder and a schema definition tool.\n\nThis package provides a thin wrapper around SQLModel that provides a more ActiveRecord-like interface with things like:\n\n* Timestamp column mixins\n* Lifecycle hooks\n\n## Getting Started\n\nFirst, setup your DB:\n\n```python\nimport activemodel\nactivemodel.init(\"sqlite:///database.db\")\n```\n\nCreate models:\n\n```python\nfrom activemodel import BaseModel\nfrom activemodel.mixins import TimestampsMixin, TypeIDMixin\n\nclass User(\n BaseModel,\n # optionally, obviously\n TimestampsMixin,\n # you can use a different pk type, but why would you?\n # put this mixin last otherwise `id` will not be the first column in the DB\n TypeIDMixin(\"user\"),\n # wire this model into the DB, without this alembic will not generate a migration\n table=True\n):\n a_field: str\n```\n\nYou'll need to create the models in the DB. Alembic is the best way to do it, but you can cheat as well:\n\n```python\nfrom sqlmodel import SQLModel\n\nSQLModel.metadata.create_all(get_engine())\n\n# now you can create a user! without managing sessions!\nUser(a_field=\"a\").save()\n```\n\nMaybe you like JSON:\n\n```python\nfrom sqlalchemy.dialects.postgresql import JSONB\nfrom pydantic import BaseModel as PydanticBaseModel\n\nfrom activemodel import BaseModel\nfrom activemodel.mixins import PydanticJSONMixin, TypeIDMixin, TimestampsMixin\n\nclass SubObject(PydanticBaseModel):\n name: str\n value: int\n\nclass User(\n BaseModel,\n TimestampsMixin,\n PydanticJSONMixin,\n TypeIDMixin(\"user\"),\n table=True\n):\n list_field: list[SubObject] = Field(sa_type=JSONB)\n```\n\nYou'll probably want to query the model. Look ma, no sessions!\n\n```python\nUser.where(id=\"user_123\").all()\n\n# or, even better, for this case\nUser.one(\"user_123\")\n```\n\nMagically creating sessions for DB operations is one of the main problems this project tackles. Even better, you can set\na single session object to be used for all DB operations. This is helpful for DB transactions, [specifically rolling back\nDB operations on each test.](#pytest)\n\n## Usage\n\n### Pytest\n\nTODO detail out truncation and transactions\n\n### Integrating Alembic\n\n`alembic init` will not work out of the box. You need to mutate a handful of files:\n\n* To import all of your models you want in your DB. [Here's my recommended way to do this.](https://github.com/iloveitaly/python-starter-template/blob/master/app/models/__init__.py)\n* Use your DB URL from the ENV\n* Target sqlalchemy metadata to the sqlmodel-generated metadata\n* Most likely you'll want to add [alembic-postgresql-enum](https://pypi.org/project/alembic-postgresql-enum/) so migrations work properly\n\n[Take a look at these scripts for an example of how to fully integrate Alembic into your development workflow.](https://github.com/iloveitaly/python-starter-template/blob/0af2c7e95217e34bde7357cc95be048900000e48/Justfile#L618-L712)\n\nHere's a diff from the bare `alembic init` from version `1.14.1`.\n\n```diff\ndiff --git i/test/migrations/alembic.ini w/test/migrations/alembic.ini\nindex 0d07420..a63631c 100644\n--- i/test/migrations/alembic.ini\n+++ w/test/migrations/alembic.ini\n@@ -3,13 +3,14 @@\n [alembic]\n # path to migration scripts\n # Use forward slashes (/) also on windows to provide an os agnostic path\n-script_location = .\n+script_location = migrations\n\n # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s\n # Uncomment the line below if you want the files to be prepended with date and time\n # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file\n # for all available tokens\n # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s\n+file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s\n\n # sys.path path, will be prepended to sys.path if present.\n # defaults to the current working directory.\ndiff --git i/test/migrations/env.py w/test/migrations/env.py\nindex 36112a3..a1e15c2 100644\n--- i/test/migrations/env.py\n+++ w/test/migrations/env.py\n@@ -1,3 +1,6 @@\n+# fmt: off\n+# isort: off\n+\n from logging.config import fileConfig\n\n from sqlalchemy import engine_from_config\n@@ -14,11 +17,17 @@ config = context.config\n if config.config_file_name is not None:\n fileConfig(config.config_file_name)\n\n+from sqlmodel import SQLModel\n+from test.models import *\n+from test.utils import database_url\n+\n+config.set_main_option(\"sqlalchemy.url\", database_url())\n+\n # add your model's MetaData object here\n # for 'autogenerate' support\n # from myapp import mymodel\n # target_metadata = mymodel.Base.metadata\n-target_metadata = None\n+target_metadata = SQLModel.metadata\n\n # other values from the config, defined by the needs of env.py,\n # can be acquired:\ndiff --git i/test/migrations/script.py.mako w/test/migrations/script.py.mako\nindex fbc4b07..9dc78bb 100644\n--- i/test/migrations/script.py.mako\n+++ w/test/migrations/script.py.mako\n@@ -9,6 +9,8 @@ from typing import Sequence, Union\n\n from alembic import op\n import sqlalchemy as sa\n+import sqlmodel\n+import activemodel\n ${imports if imports else \"\"}\n\n # revision identifiers, used by Alembic.\n```\n\nHere are some useful resources around Alembic + SQLModel:\n\n* https://github.com/fastapi/sqlmodel/issues/85\n* https://testdriven.io/blog/fastapi-sqlmodel/\n\n### Query Wrapper\n\nThis tool is added to all `BaseModel`s and makes it easy to write SQL queries. Some examples:\n\n\n\n### Easy Database Sessions\n\nI hate the idea f\n\n* Behavior should be intuitive and easy to understand. If you run `save()`, it should save, not stick the save in a transaction.\n* Don't worry about dead sessions. This makes it easy to lazy-load computed properties and largely eliminates the need to think about database sessions.\n\nThere are a couple of thorny problems we need to solve for here:\n\n* In-memory fastapi servers are not the same as a uvicorn server, which is threaded *and* uses some sort of threadpool model for handling async requests. I don't claim to understand the entire implementation. For global DB session state (a) we can't use global variables (b) we can't use thread-local variables.\n*\n\nhttps://github.com/tomwojcik/starlette-context\n\n### Example Queries\n\n* Conditional: `Scrape.select().where(Scrape.id < last_scraped.id).all()`\n* Equality: `MenuItem.select().where(MenuItem.menu_id == menu.id).all()`\n* `IN` example: `CanonicalMenuItem.select().where(col(CanonicalMenuItem.id).in_(canonized_ids)).all()`\n* Compound where query: `User.where((User.last_active_at != None) & (User.last_active_at > last_24_hours)).count()`\n\n### SQLModel Internals\n\nSQLModel & SQLAlchemy are tricky. Here are some useful internal tricks:\n\n* `__sqlmodel_relationships__` is where any `RelationshipInfo` objects are stored. This is used to generate relationship fields on the object.\n* `ModelClass.relationship_name.property.local_columns`\n* Get cached fields from a model `object_state(instance).dict.get(field_name)`\n* Set the value on a field, without marking it as dirty `attributes.set_committed_value(instance, field_name, val)`\n* Is a model dirty `instance_state(instance).modified`\n* `select(Table).outerjoin??` won't work in a ipython session, but `Table.__table__.outerjoin??` will. `__table__` is a reference to the underlying SQLAlchemy table record.\n* `get_engine().pool.stats()` is helpful for inspecting connection pools and limits\\\n\n### TypeID\n\nI'm a massive fan of Stripe-style prefixed UUIDs. [There's an excellent project](https://github.com/jetify-com/typeid)\nthat defined a clear spec for these IDs. I've used the python implementation of this spec and developed a clean integration\nwith SQLModel that plays well with fastapi as well.\n\nHere's an example of defining a relationship:\n\n```python\nimport uuid\n\nfrom activemodel import BaseModel\nfrom activemodel.mixins import TimestampsMixin, TypeIDMixin\nfrom activemodel.types import TypeIDType\nfrom sqlmodel import Field, Relationship\n\nfrom .patient import Patient\n\nclass Appointment(\n BaseModel,\n # this adds an `id` field to the model with the correct type\n TypeIDMixin(\"appointment\"),\n table=True\n):\n # `foreign_key` is a activemodel method to generate the right `Field` for the relationship\n # TypeIDType is really important here for fastapi serialization\n doctor_id: TypeIDType = Doctor.foreign_key()\n doctor: Doctor = Relationship()\n```\n\n## Limitations\n\n### Validation\n\nSQLModel does not currently support pydantic validations (when `table=True`). This is very surprising, but is actually the intended functionality:\n\n* https://github.com/fastapi/sqlmodel/discussions/897\n* https://github.com/fastapi/sqlmodel/pull/1041\n* https://github.com/fastapi/sqlmodel/issues/453\n* https://github.com/fastapi/sqlmodel/issues/52#issuecomment-1311987732\n\nFor validation:\n\n* When consuming API data, use a separate shadow model to validate the data with `table=False` and then inherit from that model in a model with `table=True`.\n* When validating ORM data, use SQL Alchemy hooks.\n\n<!--\n\nThis looks neat\nhttps://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d2a4/src/my_model/model.py#L155\n schema_extra={\n 'pattern': r'^[a-z0-9_\\-\\.]+\\@[a-z0-9_\\-\\.]+\\.[a-z\\.]+$'\n },\n\nextra constraints\n\nhttps://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d2a4/src/my_model/model.py#L424C1-L426C6\n-->\n## Related Projects\n\n* https://github.com/woofz/sqlmodel-basecrud\n* https://github.com/0xthiagomartins/sqlmodel-controller\n* https://github.com/litestar-org/advanced-alchemy?tab=readme-ov-file\n\n## Inspiration\n\n* https://github.com/peterdresslar/fastapi-sqlmodel-alembic-pg\n* [Albemic instructions](https://github.com/fastapi/sqlmodel/pull/899/files)\n* https://github.com/fastapiutils/fastapi-utils/\n* https://github.com/fastapi/full-stack-fastapi-template\n* https://github.com/DarylStark/my_data/\n* https://github.com/petrgazarov/FastAPI-app/tree/main/fastapi_app\n\n## Upstream Changes\n\n- [ ] https://github.com/fastapi/sqlmodel/pull/1293\n",
"bugtrack_url": null,
"license": null,
"summary": "Make SQLModel more like an a real ORM",
"version": "0.13.0",
"project_urls": {
"Repository": "https://github.com/iloveitaly/activemodel"
},
"split_keywords": [
"activemodel",
" activerecord",
" orm",
" sqlalchemy",
" sqlmodel"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "4471d6577db7288bf38d52b6455336f7044ca3bddb768134eacbe632e3f0c69a",
"md5": "facc685ef33c2701a65ce26b8ba411d5",
"sha256": "e032fc0d7383d04f8eeec3fbe54c7bce9cd10ddf94667b016bc4e76be2c31f8a"
},
"downloads": -1,
"filename": "activemodel-0.13.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "facc685ef33c2701a65ce26b8ba411d5",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 37923,
"upload_time": "2025-09-05T14:33:53",
"upload_time_iso_8601": "2025-09-05T14:33:53.541766Z",
"url": "https://files.pythonhosted.org/packages/44/71/d6577db7288bf38d52b6455336f7044ca3bddb768134eacbe632e3f0c69a/activemodel-0.13.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "f1c4e884e218eeff8f76457deaf3ab2da236278060045b6cc66c82afe8f3552e",
"md5": "567d4e91a66ef4550794f7b1564e46de",
"sha256": "9cb2cd762cbfc338981c24eadb9f79a756ce3f83de271ef37b5adb30796b9de5"
},
"downloads": -1,
"filename": "activemodel-0.13.0.tar.gz",
"has_sig": false,
"md5_digest": "567d4e91a66ef4550794f7b1564e46de",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 137123,
"upload_time": "2025-09-05T14:33:54",
"upload_time_iso_8601": "2025-09-05T14:33:54.663536Z",
"url": "https://files.pythonhosted.org/packages/f1/c4/e884e218eeff8f76457deaf3ab2da236278060045b6cc66c82afe8f3552e/activemodel-0.13.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-09-05 14:33:54",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "iloveitaly",
"github_project": "activemodel",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "activemodel"
}