Saltar a contenido

Tu primer modelo

Un modelo es una clase con descriptores tipados. Sin metaclase, sin campo mágico.

from snakeorm import SnakeColumn, SnakeModel, snake_auto, snake_column, snake_model, snake_str

@snake_model(table="users")
class User(SnakeModel):
    id: SnakeColumn[int] = snake_auto()
    email: SnakeColumn[str] = snake_str(unique=True)
    nickname: SnakeColumn[str | None] = snake_str()
    active: SnakeColumn[bool] = snake_column(default=True)

Genera:

CREATE TABLE "public"."users" (
  "id" BIGSERIAL,
  "email" TEXT NOT NULL,
  "nickname" TEXT,
  "active" BOOLEAN NOT NULL DEFAULT TRUE,
  PRIMARY KEY ("id"),
  CONSTRAINT "uq_users_email" UNIQUE ("email")
)

Tres cosas que entender

1. El tipo lo dice la anotación, y solo la anotación

nickname: SnakeColumn[str | None] es nulable porque el | None está ahí. No existe nullable=True: dos fuentes para el mismo dato significan que una puede mentir.

El resto sale igual. En PostgreSQL: intBIGINT, DecimalNUMERIC, dictJSONB, UUIDUUID; y datetimeTIMESTAMP mientras que SnakeUtcTIMESTAMPTZ, porque una hora de pared y un instante son dos tipos de columna distintos y es la anotación la que dice cuál. La metadata solo añade info de SQL (unicidad, defaults), nunca el tipo.

La tabla completa — cada tipo, contra los tres motores — está en columnas y tipos, y solo ahí. Una tabla copiada en una segunda página es una tabla que se desincroniza, y ésta ya lo hizo.

2. snake_auto() no es un argumento del constructor

user = User(email="ana@x.com", nickname=None)  # no `id`

La PK autoincremental la pone la base, así que se excluye del __init__. Tras el INSERT, el valor aparece (vino en el RETURNING):

session.add(user)
user.id  # there it is

3. Se imprime y se compara como esperas

>>> user
User(id=7, email='ana@x.com', nickname=None, active=True)

La igualdad va por clave primaria, no por valor. Las PK compuestas usan el mismo código, sin caso especial.

Hashear un objeto sin PK lanza TypeError

A propósito. Si el hash saliera de una PK vacía, el INSERT la rellenaría después y mutaría el hash de un objeto ya metido en un set. Insértalo primero.

Nombres de tabla

@snake_model                                   # table: "users" (class name + s)
@snake_model(table="users_legacy")          # table: "users_legacy"
@snake_model(prefix="app", table="users")   # table: "app_users"
@snake_model(schema="analytics")               # table: "analytics"."users"

La pluralización por defecto es ingenua

f"{ClassName.lower()}s". Userusers, pero Countrycountrys. Para plurales reales usa table="...".

Bases abstractas

Columnas compartidas (auditoría, típicamente) sin que la base sea una tabla:

from snakeorm import SnakeUtc, snake_abstract, snake_datetimetz

@snake_abstract
class WithAudit(SnakeModel):
    created_at: SnakeColumn[SnakeUtc] = snake_datetimetz(default_factory=SnakeUtc.now)

@snake_model(table="orders")
class Order(WithAudit):
    id: SnakeColumn[int] = snake_auto()

WithAudit aporta sus columnas a cada tabla hija y no genera ninguna propia. Consultarla lanza un error explícito.

Para las otras formas de herencia —duplicar columnas en tablas hermanas, o compartir tabla con un discriminador— mira herencia.


Siguiente: consultar.