Pular para o conteúdo

ORM

Modelos com validação, tipos convertidos e relações que não fazem N+1.

Um modelo descreve uma tabela e o que vale nela. O que ele acrescenta ao construtor de consultas:

  • validação antes de ir ao banco, e com todos os erros de uma vez
  • tipos convertidos — o que sai do banco volta como o modelo declara
  • relações carregadas sem o problema de N+1
  • migrações a partir da diferença entre o modelo e a tabela

Declarando#

dataforge
adopt Forge

db := Forge.conectar(":memory:")

Usuario := Forge.modelo("Usuario", {
    "id":    {"tipo": "Serial"},
    "email": {"tipo": "Texto", "obrigatorio": yes, "unico": yes,
              "validacoes": ["email"]},
    "nome":  {"tipo": "Texto", "obrigatorio": yes,
              "validacoes": [["minimo", 2]]},
    "idade": {"tipo": "Inteiro", "padrao": 0,
              "validacoes": [["minimo", 0], ["maximo", 130]]},
    "ativo": {"tipo": "Booleano", "padrao": yes},
    "perfil": {"tipo": "Json"}
}, {"conexao": db, "marcas_de_tempo": yes})

Forge.migrar_tudo(db)

ana := Usuario.criar({"email": "ana@exemplo.com", "nome": "Ana", "idade": 30})
assert ana["id"] is 1
assert ana["ativo"] is yes

O nome da tabela sai do plural do modelo: Usuariousuarios, Pedidopedidos, Animalanimais. Passe {"tabela": "..."} para um irregular.

Os tipos#

ForgePostgreSQLMySQLSQLite
SerialSERIAL PRIMARY KEYINT AUTO_INCREMENT PKINTEGER PK AUTOINCREMENT
InteiroINTEGERINTINTEGER
GrandeBIGINTBIGINTINTEGER
DecimalNUMERIC(18,6)DECIMAL(18,6)NUMERIC
RealDOUBLE PRECISIONDOUBLEREAL
TextoTEXTVARCHAR(255)TEXT
TextoLongoTEXTLONGTEXTTEXT
BooleanoBOOLEANTINYINT(1)INTEGER
Data / DataHoraDATE / TIMESTAMPTZDATE / DATETIMETEXT
JsonJSONBJSONTEXT
UuidUUIDCHAR(36)TEXT
BinarioBYTEABLOBBLOB

Validação#

Todos os problemas de uma vez. Um formulário que aponta um erro por vez faz o usuário submeter cinco vezes para descobrir cinco problemas:

dataforge
adopt Forge

db := Forge.conectar(":memory:")
Usuario := Forge.modelo("Usuario", {
    "id": {"tipo": "Serial"},
    "email": {"tipo": "Texto", "obrigatorio": yes, "validacoes": ["email"]},
    "nome": {"tipo": "Texto", "obrigatorio": yes, "validacoes": [["minimo", 2]]},
    "idade": {"tipo": "Inteiro", "validacoes": [["maximo", 130]]}
}, {"conexao": db})
Forge.migrar_tudo(db)

monitor:
    Usuario.criar({"email": "não-é-email", "nome": "X", "idade": 999})
handle ValidationError as e:
    // e.campos traz um item por problema
    assert len(e.campos) is 3
RegraCobra
"obrigatorio"não vazio
"email"formato de e-mail
["minimo", n]número ≥ n, ou texto com n caracteres
["maximo", n]número ≤ n, ou texto até n caracteres
["formato", padrao]casa com a expressão regular
["um_de", [a, b]]está na lista
"positivo"maior que zero

Buscar e escrever#

dataforge
adopt Forge

db := Forge.conectar(":memory:")
U := Forge.modelo("Usuario", {
    "id": {"tipo": "Serial"},
    "nome": {"tipo": "Texto", "obrigatorio": yes},
    "idade": {"tipo": "Inteiro", "padrao": 0}
}, {"conexao": db})
Forge.migrar_tudo(db)

U.criar({"nome": "Ana", "idade": 30})
U.criar({"nome": "Bia", "idade": 25})

assert len(U.todos()) is 2
assert U.buscar(1)["nome"] is "Ana"
assert U.buscar(99) is void
assert U.primeiro(nome := "Bia")["idade"] is 25
assert U.contar() is 2
assert U.existe(nome := "Ana") is yes

U.atualizar(1, {"idade": 31})
assert U.buscar(1)["idade"] is 31

U.criar_ou_atualizar({"nome": "Ana"}, {"idade": 32})
assert U.contar() is 2          // não criou outra

U.remover(2)
assert U.contar() is 1

buscar_ou_erro levanta RecordNotFoundError quando não acha — útil numa rota onde a ausência é 404, e desnecessário onde ela é prevista.

Marcas de tempo e remoção suave#

dataforge
adopt Forge

db := Forge.conectar(":memory:")
U := Forge.modelo("Usuario", {
    "id": {"tipo": "Serial"},
    "nome": {"tipo": "Texto", "obrigatorio": yes}
}, {"conexao": db, "marcas_de_tempo": yes, "remocao_suave": yes})
Forge.migrar_tudo(db)

u := U.criar({"nome": "Ana"})
assert u["criado_em"] isnt void

U.remover(u["id"])
assert U.contar() is 0                                  // some das buscas
assert len(Forge.consultar(db, "select * from usuarios")) is 1   // mas está lá

Ganchos#

dataforge
adopt Forge

db := Forge.conectar(":memory:")
U := Forge.modelo("Usuario", {
    "id": {"tipo": "Serial"},
    "nome": {"tipo": "Texto", "obrigatorio": yes}
}, {"conexao": db})
Forge.migrar_tudo(db)

U.antes_de_salvar(lambda dados => {"nome": dados["nome"].to_upper()})

assert U.criar({"nome": "ana"})["nome"] is "ANA"