DocsBancos de dados SQL
Use PostgreSQL com Nexy
Para usar PostgreSQL com Nexy, instale o driver psycopg, defina DATABASE_URL como uma URL postgresql+psycopg:// e consulte-o em manipuladores e páginas através de SQLModel ou SQLAlchemy.
Execute PostgreSQL localmente
docker run --name nexy-db -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=app -p 5432:5432 -d postgres:17
Instale o driver
uv add "psycopg[binary]"
psycopg é a versão 3 do driver, para código sincronizado e assíncrono. Para código somente assíncrono, asyncpg é a outra opção:
uv add asyncpg
A cadeia de conexão
DATABASE_URL=postgresql+psycopg://postgres:secret@localhost:5432/app| URL | Motorista |
|---|---|
postgresql+psycopg://user:pass@host:5432/db |
psycopg 3, sincronizado e assíncrono |
postgresql+asyncpg://user:pass@host:5432/db |
assíncrono, somente assíncrono |
postgresql://user:pass@host:5432/db |
psycopg2, se estiver instalado |
postgres://... |
rejeitado por SQLAlchemy: renomeie o esquema para postgresql+psycopg:// |
Uma senha com @, : ou / é codificada em porcentagem no URL: p@ss é p%40ss.
O motor
As plataformas de hospedagem geralmente fornecem uma URL postgres://. Converta-o onde o motor é feito:
import os from dotenv import load_dotenvfrom sqlmodel import create_engine load_dotenv() def database_url() -> str: url = os.environ["DATABASE_URL"] return url.replace("postgres://", "postgresql+psycopg://", 1) engine = create_engine(database_url(), pool_size=5, max_overflow=10, pool_pre_ping=True)| Opção de piscina | O que isso faz |
|---|---|
pool_size=5 |
conexões mantidas abertas por processo |
max_overflow=10 |
conexões extras permitidas sob carga, fechadas após |
pool_pre_ping=True |
verifica uma conexão antes de usar, portanto, um banco de dados reiniciado não é um erro |
Cada trabalhador nx start tem seu próprio pool: workers × (pool_size + max_overflow) deve permanecer sob o max_connections do servidor.
Um modelo com PostgreSQL tipos
uuid chaves, JSONB, matrizes e timestamptz são tipos de coluna nativos:
import uuidfrom datetime import datetime, timezone from sqlalchemy import Column, DateTime, Stringfrom sqlalchemy.dialects.postgresql import ARRAY, JSONBfrom sqlmodel import Field, SQLModel class Post(SQLModel, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) title: str = Field(max_length=200, index=True) tags: list[str] = Field(default_factory=list, sa_column=Column(ARRAY(String))) extra: dict = Field(default_factory=dict, sa_column=Column(JSONB)) published_at: datetime = Field( default_factory=lambda: datetime.now(timezone.utc), sa_column=Column(DateTime(timezone=True)), )Uma sessão por solicitação
from collections.abc import Iterator from sqlmodel import Session from src.configs.db import engine def get_session() -> Iterator[Session]: with Session(engine) as session: yield sessionLer e escrever de um manipulador
from pydantic import BaseModelfrom sqlmodel import Session, select from nexy.http import Depends, JSONResponsefrom src.configs.session import get_sessionfrom src.models.post_model import Post class NewPost(BaseModel): title: str tags: list[str] = [] async def GET(tag: str | None = None, session: Session = Depends(get_session)): query = select(Post).order_by(Post.published_at.desc()) if tag: query = query.where(Post.tags.contains([tag])) return session.exec(query).all() async def POST(body: NewPost, session: Session = Depends(get_session)): post = Post(title=body.title, tags=body.tags) session.add(post) session.commit() session.refresh(post) return JSONResponse(post.model_dump(mode="json"), status_code=201)from pydantic import BaseModelfrom sqlmodel import Session, select from nexy.decorators import Controllerfrom nexy.http import Depends, JSONResponsefrom src.configs.session import get_sessionfrom src.models.post_model import Post class NewPost(BaseModel): title: str tags: list[str] = [] @Controller("/api/posts")class PostsApiController: async def get(self, tag: str | None = None, session: Session = Depends(get_session)): query = select(Post).order_by(Post.published_at.desc()) if tag: query = query.where(Post.tags.contains([tag])) return session.exec(query).all() async def post(self, body: NewPost, session: Session = Depends(get_session)): post = Post(title=body.title, tags=body.tags) session.add(post) session.commit() session.refresh(post) return JSONResponse(post.model_dump(mode="json"), status_code=201)Liste o controlador em controllers do módulo raiz.
Post.tags.contains([tag]) é PostgreSQL de @>: as postagens cujas tags contêm tag.
Um uuid no URL
import uuid from sqlmodel import Session from nexy.http import Depends, HTTPExceptionfrom src.configs.session import get_sessionfrom src.models.post_model import Post async def GET(id: uuid.UUID, session: Session = Depends(get_session)): post = session.get(Post, id) if post is None: raise HTTPException(status_code=404, detail="No such post") return postimport uuid from sqlmodel import Session from nexy.decorators import Controllerfrom nexy.http import Depends, HTTPExceptionfrom src.configs.session import get_sessionfrom src.models.post_model import Post @Controller("/api/posts/{id}")class PostsByIdApiController: async def get(self, id: uuid.UUID, session: Session = Depends(get_session)): post = session.get(Post, id) if post is None: raise HTTPException(status_code=404, detail="No such post") return postListe o controlador em controllers do módulo raiz.
Um segmento que não é um UUID responde 404 antes da execução da consulta.
Consulta JSONB
from sqlmodel import Session, select from src.configs.db import enginefrom src.models.post_model import Post def featured_posts() -> list[Post]: with Session(engine) as session: query = select(Post).where(Post.extra["featured"].as_boolean()) return list(session.exec(query))from src.services.posts import featured_posts posts = featured_posts()<ul> {% for post in posts %} <li>{{ post.title | e }}</li> {% endfor %}</ul>from src.services.posts import featured_posts posts = featured_posts()<ul> {% for post in posts %} <li>{{ post.title | e }}</li> {% endfor %}</ul>from nexy.decorators import Controllerfrom nexy.hooks import useViews @Controller("/featured")class FeaturedController: async def get(self): return useViews("src/apps/featured/featured_view.nexy")Mantenha a consulta em uma função: apenas os nomes atribuídos no nível superior do frontmatter chegam ao template.
Pesquisa de texto completo
from sqlalchemy import funcfrom sqlmodel import Session, select from nexy.http import Dependsfrom src.configs.session import get_sessionfrom src.models.post_model import Post async def GET(q: str, session: Session = Depends(get_session)): document = func.to_tsvector("english", Post.title) query = func.websearch_to_tsquery("english", q) rank = func.ts_rank(document, query) rows = session.exec(select(Post).where(document.op("@@")(query)).order_by(rank.desc())) return rows.all()from sqlalchemy import funcfrom sqlmodel import Session, select from nexy.decorators import Controllerfrom nexy.http import Dependsfrom src.configs.session import get_sessionfrom src.models.post_model import Post @Controller("/api/search")class SearchApiController: async def get(self, q: str, session: Session = Depends(get_session)): document = func.to_tsvector("english", Post.title) query = func.websearch_to_tsquery("english", q) rank = func.ts_rank(document, query) rows = session.exec(select(Post).where(document.op("@@")(query)).order_by(rank.desc())) return rows.all()Liste o controlador em controllers do módulo raiz.
Um índice mantém a rapidez em uma tabela grande. Adicione-o em uma migração:
def upgrade() -> None: op.execute( "CREATE INDEX post_title_search ON post USING GIN (to_tsvector('english', title))" )Upserts
from sqlalchemy.dialects.postgresql import insertfrom sqlmodel import Field, Session, SQLModel from src.configs.db import engine class TagCount(SQLModel, table=True): tag: str = Field(primary_key=True) count: int = 0 def count_tag(tag: str) -> None: statement = insert(TagCount).values(tag=tag, count=1) statement = statement.on_conflict_do_update( index_elements=[TagCount.tag], set_={"count": TagCount.count + 1} ) with Session(engine) as session: session.exec(statement) session.commit()Migrações
nx migrate --initnx migrate -m "create posts"nx migrate --upgrade
nx migrate lê DATABASE_URL, verifica se o driver está instalado e se o servidor responde e então executa Alembic. Importe cada módulo do modelo em alembic/env.py para que a geração automática o veja.
Assíncrono com asyncpg
from sqlalchemy.ext.asyncio import create_async_enginefrom sqlmodel.ext.asyncio.session import AsyncSession from src.configs.db import database_url async_engine = create_async_engine( database_url().replace("+psycopg", "+asyncpg", 1), pool_pre_ping=True) async def get_async_session(): async with AsyncSession(async_engine) as session: yield sessionfrom sqlmodel import func, selectfrom sqlmodel.ext.asyncio.session import AsyncSession from nexy.http import Dependsfrom src.configs.async_db import get_async_sessionfrom src.models.post_model import Post async def GET(session: AsyncSession = Depends(get_async_session)): count = (await session.exec(select(func.count()).select_from(Post))).one() return {"count": count}from sqlmodel import func, selectfrom sqlmodel.ext.asyncio.session import AsyncSession from nexy.decorators import Controllerfrom nexy.http import Dependsfrom src.configs.async_db import get_async_sessionfrom src.models.post_model import Post @Controller("/api/posts/count")class PostsCountApiController: async def get(self, session: AsyncSession = Depends(get_async_session)): count = (await session.exec(select(func.count()).select_from(Post))).one() return {"count": count}Liste o controlador no controllers do módulo raiz, antes de PostsByIdApiController: o primeiro controlador cujo caminho corresponde às respostas, e listado depois dele, /api/posts/count/ responde 404.
psycopg 3 também roda assíncrono: postgresql+psycopg:// funciona com create_async_engine também.
Testes em uma transação
Cada teste é executado dentro de uma transação que é revertida, portanto o banco de dados de teste permanece vazio:
import pytestfrom sqlmodel import Session, SQLModel, create_engine from nexy.app import appfrom src.configs.session import get_session test_engine = create_engine("postgresql+psycopg://postgres:secret@localhost:5432/app_test")SQLModel.metadata.create_all(test_engine) @pytest.fixture(autouse=True)def session(): connection = test_engine.connect() transaction = connection.begin() session = Session(bind=connection, join_transaction_mode="create_savepoint") app.dependency_overrides[get_session] = lambda: session yield session session.close() transaction.rollback() connection.close()Gerenciado PostgreSQL
| Serviço | O que muda |
|---|---|
| Néon | adicione ?sslmode=require; use o host em pool para muitas conexões curtas |
| Supabase | o mesmo formulário de URL; use a porta do pooler 6543 para muitos trabalhadores |
| Amazon RDS, Cloud SQL, Azure | adicione ?sslmode=require e permita a rede do aplicativo |