Ir para o conteúdo

Exercícios da aula 04

Exercício 01

Fazer uma alteração no modelo (tabela User) e adicionar um campo chamado updated_at:

  • Esse campo deve ser mapeado para o tipo datetime
  • Esse campo não deve ser inicializado por padrão init=False
  • O valor padrão deve ser now
  • Toda vez que a tabela for atualizada esse campo deve ser atualizado:
    mapped_column(onupdate=func.now())
    

Solução

@table_registry.mapped_as_dataclass
class User:
    __tablename__ = 'users'

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    username: Mapped[str] = mapped_column(unique=True)
    password: Mapped[str]
    email: Mapped[str] = mapped_column(unique=True)
    created_at: Mapped[datetime] = mapped_column(
        init=False, server_default=func.now()
    )
    updated_at: Mapped[datetime] = mapped_column(  # Exercício
        init=False, server_default=func.now(), onupdate=func.now()
    )

Exercício 02

Altere o evento de testes (mock_db_time) para ser contemplado no mock o campo updated_at na validação do teste.

Solução

A ideia é adicionar mais um campo na verificação do modelo, para que o update também esteja um horário determinístico:

@contextmanager
def _mock_db_time(*, model, time=datetime(2024, 1, 1)):

    def fake_time_handler(mapper, connection, target):
        if hasattr(target, 'created_at'):
            target.created_at = time
        if hasattr(target, 'updated_at'):
            target.updated_at = time

    event.listen(model, 'before_insert', fake_time_handler)

    yield time

    event.remove(model, 'before_insert', fake_time_handler)

Com a alteração do modelo, o teste também passará a falhar. Isso pode ser modificado adicionando o campo updated_at no dicionário de validação:

def test_create_user(session, mock_db_time):
    with mock_db_time(model=User) as time:
        new_user = User(
            username='alice', password='secret', email='teste@test'
        )
        session.add(new_user)
        session.commit()

        user = session.scalar(select(User).where(User.username == 'alice'))

    assert asdict(user) == {
        'id': 1,
        'username': 'alice',
        'password': 'secret',
        'email': 'teste@test',
        'created_at': time,
        'updated_at': time,
    }

Exercício 03

Criar uma nova migração autogerada com alembic.

Solução

Comando explicado na aula para gerar uma migração automática:

$ Execução no terminal!
alembic revision --autogenerate -m "exercicio 02 aula 04"

O Comando deve retornar algo parecido com isso:

Resultado do comando
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added column 'users.updated_at'
  Generating /home/dunossauro/git/fastapi-do-
  zero/codigo_das_aulas/04/migrations/versions/bb77f9679811_exercicio_02_aula_04.py ...  done

O arquivo de migrações deve se parecer com esse:

/migrations/versions/bb77f9679811_exercicio_02_aula_04.py
"""exercicio 02 aula 04

Revision ID: bb77f9679811
Revises: 74f39286e2f6
Create Date: ...

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'bb77f9679811'
down_revision: Union[str, None] = '74f39286e2f6'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('users', sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False)) #(1)!
    # ### end Alembic commands ###


def downgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('users', 'updated_at') #(2)!
    # ### end Alembic commands ###
  1. Adiciona a coluna updated_at na tabela users
  2. Remove a coluna updated_at na tabela users

Exercício 04

Aplicar essa migração ao banco de dados

Solução

Para aplicar a ultima migração devemos nos mover até a head:

$ Execução no terminal!
alembic upgrade head
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade 74f39286e2f6 -> bb77f9679811, exercicio 02 aula 04

Checando o resultado no schema do banco de dados:

$ Execução no terminal!
sqlite3 database.db 
SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.
sqlite> .schema
CREATE TABLE alembic_version (
        version_num VARCHAR(32) NOT NULL, 
        CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)
);
CREATE TABLE users (
        id INTEGER NOT NULL, 
        username VARCHAR NOT NULL, 
        password VARCHAR NOT NULL, 
        email VARCHAR NOT NULL, 
        created_at DATETIME DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
        updated_at DATETIME DEFAULT (CURRENT_TIMESTAMP) NOT NULL, 
        PRIMARY KEY (id), 
        UNIQUE (email), 
        UNIQUE (username)
);

Podemos ver que o campo updated_at foi criado com o tipo DATETIME e com o valor padrão para CURRENT_TIMESTAMP, assim como no created_at.