
DoveORM – Small, security-hardened Python ORM with built-in migrations
A lightweight, expressive, security-hardened Python ORM.
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
A lightweight, expressive, security-hardened Python ORM.
Python 3.9+ · Zero dependencies for SQLite · mysqlclient for MySQL · psycopg for PostgreSQL · Works with Flask, FastAPI, or standalone
Quick Start • Configuration • Models • Fields • Querying • Instances • Relationships • Transactions • Migrations • CLI • Drivers • Security
DoveORM is a lightweight yet fully-featured Python ORM with validated models, chainable queries, relationships and migrations behind one small, consistent API.
Feature
@table() decorator and class Meta model declarations ✅
Dynamic finders — findByEmail, findAllByActive, countBy…, existsBy… ✅
Chainable, lazy QuerySet with Q objects, result caching and slicing ✅
30+ field types with validation ✅
SQLite, MySQL/MariaDB and PostgreSQL behind a pluggable driver interface ✅
Thread-safe & fork-safe connections, nested transactions (SAVEPOINTs) ✅
Deadlock-retrying transactions — db.run_in_transaction(fn) ✅
Migrations — autodetected, optimized, reversible, squashable ✅
doveorm command — generate, apply, revert, status, doctor, console… ✅
pip-installable, standalone — no web framework required ✅
Optimistic locking, mass-assignment protection, sensitive fields ✅
Injection-hardened — bound parameters, validated identifiers, strict SQL modes ✅
Explicit configuration — the ORM never guesses where your data lives ✅
🚀 Quick Start
Installation
DoveORM is a normal pip package (Python 3.9+):
pip install doveorm
with MySQL / MariaDB support (adds mysqlclient)
pip install "doveorm[mysql]"
with PostgreSQL support (adds psycopg 3)
pip install "doveorm[postgres]"
In a requirements.txt:
doveorm[postgres]==0.8.0
Installing gives you the doveorm package and the doveorm command. SQLite needs nothing else. PostgreSQL uses psycopg (version 3) and falls back to psycopg2 if that is what you already have installed. On Python 3.9/3.10 pip also installs tomli, which the command uses to read its config file.
from doveorm import Model, configure, CharField, IntegerField
configure(DATABASE_PATH="app.db") # or ":memory:" for a throw-away database
class User(Model): name = CharField(max_length=100, nullable=False) age = IntegerField(default=0)
User.create_table()
user = User.create(name="Titus", age=25) print(user.id) # 1 (auto-generated primary key)
user.age = 26 user.save() # UPDATE
adults = User.where(age__gte=18).order_by("name").get() user.delete()
Hello World — MySQL
import os from doveorm import Model, configure, CharField, FloatField
configure( DATABASE_ENGINE="mysql", DATABASE_NAME="myapp", DATABASE_HOST="127.0.0.1", DATABASE_USER="app", DATABASE_PASSWORD=os.environ["DB_PASSWORD"], # never hard-code secrets )
class Product(Model): name = CharField(max_length=200) price = FloatField(default=0.0)
Product.create_table() Product.create(name="Widget", price=9.99)
Hello World — PostgreSQL
import os from doveorm import Model, configure, CharField, DecimalField
configure( DATABASE_ENGINE="postgresql", DATABASE_NAME="myapp", DATABASE_HOST="127.0.0.1", # or a socket dir: "/var/run/postgresql" DATABASE_USER="app", DATABASE_PASSWORD=os.environ["DB_PASSWORD"], )
class Product(Model): name = CharField(max_length=200) price = DecimalField(max_digits=10, decimal_places=2, default=0)
Product.create_table() Product.create(name="Widget", price="9.99")
⚙️ Configuration
Your application owns the configuration; the ORM only consumes it. The ORM ships defaults for its own behaviour (logging, timeouts, strictness) but never for where your data lives:
import os from doveorm import configure
SQLite
db = configure( DATABASE_ENGINE="sqlite", # "sqlite" (default), "mysql" or "postgresql" DATABASE_PATH="app.db", # file path or ":memory:" LOG_QUERIES=True, # log every SQL statement (values redacted) )
MySQL
db = configure( DATABASE_ENGINE="mysql", DATABASE_NAME="myapp", DATABASE_HOST="127.0.0.1", DATABASE_PORT=3306, DATABASE_USER="app", DATABASE_PASSWORD=os.environ["DB_PASSWORD"], DATABASE_OPTIONS={"ssl_mode": "REQUIRED", "isolation_level": "READ COMMITTED"}, )
PostgreSQL
db = configure( DATABASE_ENGINE="postgresql", # "postgres" and "django.db.backends.postgresql" work too DATABASE_NAME="myapp", DATABASE_HOST="127.0.0.1", DATABASE_PORT=5432, # optional: 5432 is the default DATABASE_USER="app", DATABASE_PASSWORD=os.environ["DB_PASSWORD"], DATABASE_OPTIONS={"sslmode": "require", "schema": "app", "statement_timeout": 30}, )
- DATABASES dict
import os from doveorm import configure
configure(DATABASES={ "default": { "ENGINE": "mysql", "NAME": "myapp", "HOST": "127.0.0.1", "PORT": 3306, "USER": "app", "PASSWORD": os.environ["DB_PASSWORD"], } })
PostgreSQL works the same way:
"ENGINE": "postgresql" (or "django.db.backends.postgresql"), "PORT": 5432
- Environment variables
export DOVEORM_DATABASE_ENGINE=mysql export DOVEORM_DATABASE_NAME=myapp export DOVEORM_DATABASE_HOST=127.0.0.1 export DOVEORM_DATABASE_USER=app export DOVEORM_DATABASE_PASSWORD='...' # from your secret store export DOVEORM_DATABASE_OPTIONS='{"ssl_mode": "REQUIRED"}' # dict settings are JSON
PostgreSQL: DOVEORM_DATABASE_ENGINE=postgresql, DOVEORM_DATABASE_OPTIONS='{"sslmode": "require"}'
from doveorm import configure_from_env db = configure_from_env() # reads DOVEORM_* variables
- From your own settings module
settings.py (part of your application)
import os ORM_SETTINGS = { "DATABASE_ENGINE": "sqlite", "DATABASE_PATH": os.environ.get("APP_DB_PATH", "production.db"), "LOG_QUERIES": False, }
from doveorm import configure from settings import ORM_SETTINGS db = configure(**ORM_SETTINGS)
configure_from_file() still works but is deprecated: it executes the file it is given.
For tests or several databases in one process, skip configure() and bind models directly:
from doveorm import Database
db = Database(":memory:") # or Database("app.db"), or Database(driver=...) User.use(db) User.create_table()
All configuration options
Setting Default Description
DATABASE_ENGINE "sqlite" "sqlite", "mysql" or "postgresql"
DATABASE_PATH required (SQLite) SQLite file path, or ":memory:"
DATABASE_NAME required (MySQL / PostgreSQL) Database name / alias for the path (SQLite)
DATABASE_HOST required (MySQL / PostgreSQL) Server host. MySQL: or DATABASE_OPTIONS["unix_socket"]; PostgreSQL: or a socket directory
DATABASE_PORT engine default 3306 for MySQL, 5432 for PostgreSQL
DATABASE_USER required (MySQL / PostgreSQL) Database user
DATABASE_PASSWORD required (MySQL / PostgreSQL) Password ("" must be explicit)
DATABASE_CHARSET "utf8mb4" MySQL charset (PostgreSQL always uses UTF8)
DATABASE_OPTIONS {} Extra driver options — see Drivers
LOG_QUERIES False Log every SQL statement to the doveorm logger
LOG_QUERY_PARAMS False Also log bound values (may expose secrets / PII)
AUTO_WIRE_MODELS True Bind every model to the configured database automatically
USE_TZ False DateTimeField returns aware UTC datetimes
📐 Defining Models
Style 1: @table() decorator
from doveorm import Model, table, CharField, BooleanField, HasMany
@table( name="users", # explicit table name indexes=[("username",), ("email",)], # (composite) indexes unique_together=[("email",)], # unique constraints ordering=["-id"], # default ORDER BY for every query engine="InnoDB", # MySQL engine charset="utf8mb4", # MySQL charset ) class User(Model): username = CharField(max_length=80, nullable=False, unique=True) email = CharField(max_length=120, nullable=False, unique=True) active = BooleanField(default=True)
posts = HasMany("Post", foreign_key="author_id")
Style 2: class Meta
from doveorm import Model, CharField, TextField, BooleanField, ForeignKey, DateTimeField
class Post(Model): title = CharField(max_length=200, nullable=False) body = TextField(nullable=False) published = BooleanField(default=False) author_id = ForeignKey("User", on_delete="CASCADE", nullable=False) created_at = DateTimeField(auto_now_add=True, nullable=True)
class Meta: table_name = "posts" indexes = [("author_id",), ("published",)] ordering = ["-created_at"]
If both are present, @table() values win. Supported options: table_name / name, indexes, unique_together, ordering, engine, charset, version_field, migrate, and abstract (Meta only).
class Tag(Model): name = CharField(max_length=50, unique=True)
Table name: "tags" (pluralized snake_case)
Primary key: id = AutoField() is injected automatically
Abstract base models
class TimeStamped(Model): created_at = DateTimeField(auto_now_add=True, nullable=True) updated_at = DateTimeField(auto_now=True, nullable=True)
class Meta: abstract = True # no table, cannot be instantiated or queried
class Article(TimeStamped): title = CharField(max_length=200)
Models that migrations should not touch (migrate = False)
Use migrate = False for a table that DoveORM must not create, alter or drop: a database view, or a table owned by another service or a legacy system. The model works normally for queries, saves and relationships, but doveorm generate, verify and the schema checks ignore it, and doveorm models lists it as external.
@table(name="legacy_customers", migrate=False) # or: class Meta: migrate = False class LegacyCustomer(Model): name = CharField(max_length=100)
You create the table yourself, so it must already exist before you query it. A model that is migrated cannot have a ForeignKey to a migrate = False model: use a plain integer or UUID field for the key instead. Unlike abstract, the option is not inherited by subclasses.
import uuid from doveorm import Model, UUIDField, CharField
class Ticket(Model): id = UUIDField(primary_key=True, default=uuid.uuid4) # pass the callable title = CharField(max_length=200)
Ticket.create_table() ticket = Ticket.create(title="Bug report") print(ticket.id) # UUID('a1b2c3d4-...')
Table name rules
Class name Auto table name
User users
Category categories
Address addresses
BlogPost blog_posts
Naming rules (enforced)
Table, column, index and join-table names must be plain identifiers ([A-Za-z_][A-Za-z0-9_]*, ≤ 64 chars).
Reserved words (order, user, group, limit, …) and mixed-case names (firstName) work
as table and column names on every backend: DoveORM quotes a name only when it has to
("order", or order on MySQL), so ordinary names produce plain, unquoted SQL.
On PostgreSQL a mixed-case name is case-sensitive. Keep names to 63
characters there; PostgreSQL truncates longer ones.
Field names may not start with _ and may not shadow the Model API (save, delete, where, to_dict, …).
A model may declare at most one primary key.
📋 Fields Reference
Auto-increment primary keys
Field SQLite MySQL PostgreSQL
AutoField() INTEGER PRIMARY KEY AUTOINCREMENT INT PRIMARY KEY AUTO_INCREMENT INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
BigAutoField() INTEGER PRIMARY KEY AUTOINCREMENT BIGINT PRIMARY KEY AUTO_INCREMENT BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
SmallAutoField() INTEGER PRIMARY KEY AUTOINCREMENT SMALLINT PRIMARY KEY AUTO_INCREMENT SMALLINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
An explicit value (User(id=99)) is inserted as given, and the next generated key continues after it on every backend: PostgreSQL's identity sequence is moved forward automatically (never backwards). Rows inserted with raw SQL bypass this; after a raw bulk import on PostgreSQL, run SELECT setval(pg_get_serial_sequence('users', 'id'), (SELECT MAX(id) FROM users));
Field SQLite MySQL PostgreSQL Validation
IntegerField() INTEGER INT INTEGER 32-bit range; bool rejected
BigIntegerField() INTEGER BIGINT BIGINT 64-bit range
SmallIntegerField() INTEGER SMALLINT SMALLINT 16-bit range
PositiveIntegerField() / PositiveSmallIntegerField() / PositiveBigIntegerField() INTEGER INT / SMALLINT / BIGINT INTEGER / SMALLINT / BIGINT ≥ 0
FloatField() REAL DOUBLE DOUBLE PRECISION finite numbers only
DecimalField(max_digits=10, decimal_places=2) TEXT DECIMAL(m,d) NUMERIC(m,d) dig
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。