One of Python's most significant advantages is that we can make applications fast. Boilerplate is low, but we can focus on business logic, quickly deliver value, and make projects production-ready. But even if, with current powerful libraries, "time-to-market" is low, it can be lowered even more.
The idea of RedBeanPython ORM is to decrease the time to market for applications by providing an absolutely zero configuration ORM and, simultaneously, heaving the possibility of zero effort transition to advanced ORM when needed.
RedBeanPython idea is inspired by the matured RedBeanPHP ORM (since 2009 and still running).
RedBeanPython ORM's idea is not to replace the more advanced ORMs (we should not reinvent the wheel) but to cooperate with them. Initially, it can be used as only ORM. When projects evolve, both code bases (RedBeanPython and SQLAlchemy) can coexist and, finally, can be swiftly replaced by SQLAlchemy (if needed).
RedBeanPython is available on PyPI and can be installed with pip:
pip install redbeanpython
Documentation is available on redbeanpython.org.
RedBeanPython requires
- Python 3.11 or newer.
- SQLAlchemy 2.0 or newer.
- alembic 1.7 or newer.
For PostgreSQL support RedBeanPython requires:
- psycopg 3.0.18 or newer.
For MySQL/MariaDB support RedBeanPython requires:
- PyMySQL 1.0.2 or newer.
We are starting a fresh project. We have an Idea, and we want to deliver MVP to production as soon as possible.
We have an empty database (or no database at all at that moment).
Let's store some data:
from datetime import datetime
from decimal import Decimal
from redbeanpython import redbean, Bean
redbean.setup(dsn=False) #(1)
user = Bean('user')
user.name = 'Adam'
user.age = 42
user.subscription_price = Decimal("12.20")
user.subscription_end = datetime.fromisoformat('2024-10-08T16:20:00')
redbean.store(user)
- For a temporary SQLite database call
redbean.setup(dsn=False)
. For a production database, DSN consistent with SQLAlchemy should be provided. See connection for more details. For example, you may run a docker container with PostgreSQL:And use it:docker run -p 5432:5432 -e POSTGRES_PASSWORD=pass -d postgres
redbean.setup('postgresql+psycopg://postgres:pass@localhost/postgres')
And that's all.
No schema is need to be created, no migrations need to be run, no config files to update.
Just run the code, and RedBeanPython will create everything automatically.
If we take a look into db, we will see that table was created:
create table user
(
age BIGINT,
id VARCHAR(255) not null primary key,
name TEXT,
subscription_end DATETIME,
subscription_price NUMERIC(30, 10)
);
In the background, the SQLAlchemy model was created:
from sqlalchemy import Column, BigInteger, String, Text, DateTime, Numeric
from redbeanpython import Model
class User(Model):
__tablename__ = 'user'
__table_args__ = {'extend_existing': True}
age = Column(BigInteger, nullable=True)
id = Column(String(255), primary_key=True)
name = Column(Text, nullable=True)
subscription_end = Column(DateTime, nullable=True)
subscription_price = Column(Numeric(precision=30, scale=10), nullable=True)
def __json__(self):
return self.as_dict()
def as_dict(self):
return {
'age': self.age,
'id': self.id,
'name': self.name,
'subscription_end': self.subscription_end,
'subscription_price': self.subscription_price,
}
We do not need this information now, but it might be helpful later. (See section: hybrid mode)
Let's load it from the database, and as we see, the types are preserved:
>>> from redbeanpython import redbean
>>> redbean.setup(dsn=False)
>>> users = list(redbean.find('user'))
>>> user = users[0]
>>> print(dict(user))
{
'age': 42,
'id': '1f0e6e46-6e47-404f-9547-9dc3e9741ec6',
'name': 'Adam',
'subscription_end': datetime.datetime(2024, 10, 8, 16, 20),
'subscription_price': Decimal('12.2000000000')
}
>>> print(type(user.subscription_end), user.subscription_end)
<class 'datetime.datetime'> 2024-10-08 16:20:00
Let's go on; development continues.
In the early stage of the project, life changes are widespread. Often, we even rewrite the first ideas.
With RedBeanPython, this is no problem; we can change the code and run it again. RedBeanPython will adjust everything automatically.
For example, we decided that the user should have a birthday. We can add in code:
# ...
user.birthday = date.fromisoformat('1981-12-01')
# ...
And rerun it. RedBeanPython will adequately adjust the database schema.
The database schema has been adjusted.
create table user
(
age BIGINT,
id VARCHAR(255) not null primary key,
name TEXT,
subscription_end DATETIME,
subscription_price NUMERIC(30, 10),
birthday DATE -- (1)
);
- A new column was added.
Same as model:
class User(Model):
# (...)
name = Column(Text, nullable=True)
subscription_end = Column(DateTime, nullable=True) # (1)
subscription_price = Column(Numeric(precision=30, scale=10), nullable=True)
# (...)
def as_dict(self):
return {
# (...)
'name': self.name,
'subscription_end': self.subscription_end, # (2)
'subscription_price': self.subscription_price,
}
- A new property was added.
- A new property was added.
And alembic migration was created automatically:
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('birthday', sa.Date(), nullable=True))
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('birthday')
:information: Migrations will be useful later when the project is production-ready.
Now, when we have MVP ready, we can go to production.
When the project stays production-ready, we would like to freeze production database to avoid potential unexpected changes. No automatic changes will be made to production since that moment.
To freeze the database, we just need to add one parameter to redbean.setup()
call:
redbean.setup('...', frozen=True)
Or use REDBEAN_DB_FROZEN=1
environment variable.
And, from that time on production, we can use alembic migrations to manage production database schema changes safely. We fully control how and when the database schema will be changed.
Because alembic migrations are created automatically by RedBeanPython, we can just run:
cd redbean/migrations
DB_DSN="sqlite:////tmp/redbean.sqlite" alembic upgrade head
on production to apply all changes.
At the same time, everything will work as before in the development environment, and we still have all the benefits from automatic creation.
When the project grows, we may need more performance or more sophisticated database features. As RedBeanPython is not intended to be a replacement for SQLAlchemy, instead of extending RedBeanPython forever, we can switch to SQLAlchemy ORM and take full advantage of the giants.
Moreover, we can use both ORMs simultaneously and switch to more advanced ORMs with zero effort for specific cases only.
As SQLAlchemy Models has already been created automatically, we can use it.
Let's see how it works.
from redbean.models.customer import Customer
from redbean.models.invoice import Invoice
redbean.setup(dsn=False)
# redbeanpython code
customer = Bean('customer')
customer.name = 'John'
invoice = Bean('invoice')
invoice.amount = Decimal("12.20")
invoice.customer_id = customer.id
redbean.store_many([invoice, customer])
# sqlalchemy code
session = redbean.session_maker()
query = session.query(Customer, Invoice)
query = query.filter(Customer.id == Invoice.customer_id)
query = query.filter(Invoice.amount > 10)
result = query.all()
for customer, invoice in result:
print(customer.name, invoice.amount)
query = session.query(
Invoice.customer_id,
func.count('*').label('amount'),
func.sum(Invoice.amount).label('amount'),
)
query = query.group_by(Invoice.customer_id)
for customer_id, invoices_count, invoices_total, in query.all():
print(f"{customer_id} has: {invoices_count} invoices with a sum: {invoices_total}")
# redbeanpython code again
print(customer_id)
customer = redbean.load('customer', customer_id)
print(dict(customer))
We can freely mix both ORMs in the same code base. This is a hybrid mode.
Finally, when our project succeeds, it will be on the market for months or years. Development team grows. Codebase enlarges - we can want to switch to SQLAlchemy ORM entirely.
When the project matures and we decide to switch entirely to SQLAlchemy ORM, we can say goodbye to RedBeanPython methods and start to use SQLAlchemy only.
Starting with previously automatically generated Models, we will continue to extend them and work with them in a standard SQLAlchemy way.
And then you can let RedBeanPython go. RedBeanPython did its job. It's time for retirement.
It was a beautiful journey!
Check more on redbeanpython.org.
Licence: MIT
Issue tracker is available on GitHub.
Adam Puza: [email protected]