02. Como guardar os recados?
Perguntas para o “Pense antes”
- “Se a gente fechar o navegador, onde o recado fica?” Deixe a pessoa chegar por conta própria na ideia de um lugar que guarda informações.
- “Como você anotaria vários recados numa planilha?” A planilha é a ponte para a ideia de tabela, linha e coluna.
- “A autora e a mensagem são do mesmo tipo?” Leva à diferença entre
stringetext(vejastringoutext?).
Confusões comuns
- Model e migration parecem a mesma coisa. A migration é a instrução para criar a tabela, e roda uma vez. O model é quem usa a tabela, o tempo todo. Uma analogia que costuma ajudar: a migration é a planta da reforma; o model é quem mora na casa. E quem faz a obra? O comando
bin/rails db:migrate: ele lê a planta e constrói a tabela no banco de dados. Sem rodá-lo, a planta existe, mas a casa não foi construída, e o model não tem onde morar (é o errono such table: messages). - Esquecer o
bin/rails db:migrate. O console respondeno such table: messages, e o navegador mostraMigrations are pending. - Ficar “preso” no console. Quem tenta rodar
bin/rails ...dentro do console recebe erro de Ruby. Oexitvolta para o terminal. Messagecom m minúsculo no console dáNameError.- Nomes em inglês. Se alguém estranhar
Message,authorecontent, lembre que o código segue o costume do inglês, e o guia traz a tradução de cada nome. - O recado vazio do “Quebre de propósito”. É de propósito: o Rails aceita, porque ninguém disse que é proibido. Não adiante a solução; ela é o capítulo 07.
Onde os recados ficam guardados no Codespaces
O banco de dados é o SQLite, que fica num único arquivo: storage/development.sqlite3, dentro da pasta do projeto no codespace.
- Os recados continuam lá quando o codespace é desligado e religado, quando a participante fecha a aba ou quando o codespace dorme por inatividade.
- Os recados se perdem quando o codespace é apagado (inclusive quando o GitHub apaga sozinho um codespace parado por muito tempo, por padrão depois de 30 dias), quando a participante cria um codespace novo para o mesmo repositório ou quando passa a trabalhar no próprio computador.
- O banco não vai para o GitHub. O
.gitignorecriado pelo Rails deixa a pastastoragefora dos commits. O código viaja com o repositório; os dados, não.
Se alguém perguntar por que os recados sumiram num codespace novo, é isso: o código veio do GitHub, mas o banco começa vazio. Basta rodar bin/rails db:migrate para criar a tabela de novo e postar novos recados.
Isso também vale para o capítulo 08: no plano gratuito do Render, o disco também não é permanente, e o SQLite perde os dados a cada deploy. Por isso, o capítulo 08 usa um PostgreSQL no Render.
string ou text?
O que dizer para as participantes
Uma explicação curta, para quem perguntar ou errar o palpite do passo 1:
stringé um texto curto, de uma linha, como um nome. É aauthor(autora).texté um texto longo, que pode ter várias linhas, como uma mensagem. É ocontent(conteúdo) do recado.- Dá para ver a diferença na tela: no capítulo 04, a autora vai ser uma caixa de uma linha, e a mensagem, uma caixa maior.
- Se alguém perguntar “então tem limite de tamanho?”: não é o tipo que limita. A regra dos 280 caracteres do mural de recados vai ser escrita no capítulo 07.
Se perguntarem “por que existem os dois tipos?”: pense numa ficha de papel. O campo “Nome” tem uma linha curta, e o campo “Observações” tem um quadro grande, porque cada um foi pensado para um tamanho de texto. No app é igual: string para o que é curto (um nome, um título) e text para o que pode ser longo (uma mensagem). E o desempenho, por baixo dos panos? No SQLite e no PostgreSQL, que o guia usa, os dois tipos são guardados e lidos do mesmo jeito, então a escolha não deixa o app mais rápido nem mais lento. Em outros bancos de dados, existem pequenas diferenças, mas elas só aparecem em apps muito grandes, e não no mural de recados.
O resto desta seção é para a mentoria.
Com mais detalhe (só para a mentoria)
Contexto só para a mentoria. Não precisa levar para as participantes: o capítulo só diz que string é texto curto e text, texto longo.
Os dois guardam texto. A diferença está no tamanho esperado e em como cada banco de dados guarda:
string |
text |
|
|---|---|---|
| Para quê | Texto curto, de uma linha: nome, e-mail, título | Texto longo, que pode ter várias linhas: mensagem, descrição, comentário |
| No SQLite (codespace) | varchar, mas o SQLite guarda os dois do mesmo jeito e não limita o tamanho |
text |
| No PostgreSQL (Render, capítulo 08) | character varying, sem limite de tamanho |
text, também sem limite (por dentro, os dois funcionam igual) |
| No MySQL | varchar(255): no máximo 255 caracteres |
text: até 64 KB (uns 65 mil caracteres sem acento) |
| No formulário | Combina com text_field (uma linha) |
Combina com text_area (caixa maior) |
No mural de recados, author é string (um nome) e content é text (a mensagem). Nos bancos que o guia usa, a escolha quase não muda nada na prática: ela diz a intenção de quem planejou, e essa intenção reaparece no capítulo 04, com o form.text_field :author e o form.text_area :content. O scaffold do Rails também usa essa dica para escolher o campo do formulário.
Se alguém perguntar “não dava para usar só text, ou só string?”: no SQLite e no PostgreSQL, funcionaria. Mesmo assim, vale ter os dois:
- A intenção fica no código. Quem lê
stringsabe que é um texto curto; quem lêtext, que pode ser longo. É como escolher bem o nome de uma variável. - O Rails e outras ferramentas usam essa dica, como o scaffold, que escolhe a caixa de uma linha ou a caixa maior no formulário.
- O app pode mudar de banco de dados. No MySQL, tudo
stringcortaria mensagens acima de 255 caracteres. E tudotextatrapalha os índices (usados para buscar rápido), que no MySQL não funcionam direto numa colunatext. - É a convenção. Quem chega num projeto Rails espera nomes e títulos como
stringe textos longos comotext.
E o espaço e o desempenho? Ao contrário do que muita gente pensa, string não economiza espaço. Nos três bancos, o que ocupa espaço é o texto guardado, e não o tipo: um nome de 3 letras ocupa o mesmo numa coluna string ou text, e o varchar(255) do MySQL é só um limite, e não reserva 255 caracteres. Quem reserva espaço fixo é outro tipo, o char(n), que o Rails quase não usa. Por banco de dados:
- SQLite: nenhuma diferença. O
varchare otexttêm a mesma afinidade (TEXT), e o SQLite ignora o número entre parênteses:varchar(255)não limita nada. - PostgreSQL: nenhuma diferença. A documentação diz que “não há diferença de desempenho” entre
character varyingetext, a não ser uma pequena checagem do tamanho quando há limite. Textos longos, nos dois tipos, são comprimidos sozinhos, e os muito longos ficam em tabelas de apoio (o TOAST) para não atrapalhar a leitura das outras colunas. Tudo isso é transparente para o app. - MySQL (InnoDB): é onde existem diferenças, e elas são práticas:
- Índices: numa coluna
text, o índice exige um tamanho de prefixo (index(content(100))), e numavarchar, não. - Ordenação: ao ordenar por uma coluna
text, só os primeiros 1024 bytes são usados, por padrão (max_sort_length). - Tabelas temporárias: uma consulta que devolve colunas
texte usa uma tabela temporária vai para o disco, e não para a memória, e a documentação recomenda evitar oSELECT *por isso. - Onde o texto fica: valores longos, de
varcharou detext, podem ir para fora da linha da tabela quando a linha fica grande; sobra na linha só um ponteiro de 20 bytes. O tipo não decide isso, e o tamanho do valor, sim.
- Índices: numa coluna
Se alguém perguntar “então a mensagem pode ter qualquer tamanho?”: no banco de dados, sim. O limite de 280 caracteres é uma regra do mural de recados, e não do tipo da coluna: ela entra no model, com uma validação, no capítulo 07.
Fontes: os tipos de cada banco estão no código do Active Record 8.1 (NATIVE_DATABASE_TYPES dos adaptadores do SQLite, PostgreSQL e MySQL). O resto vem da documentação oficial (em inglês): SQLite, tipos de dados, PostgreSQL, tipos de caracteres, MySQL, BLOB e TEXT e MySQL, formato de linha do InnoDB.
SQLite, MySQL e PostgreSQL
Isto é contexto para a mentoria. Não precisa explicar para as participantes agora: só se alguém perguntar.
O app usa o SQLite, que é o padrão do Rails. Ele funciona diferente de bancos como o MySQL e o PostgreSQL:
| SQLite | MySQL e PostgreSQL | |
|---|---|---|
| Onde fica | Num único arquivo dentro do projeto (storage/development.sqlite3) |
Num programa separado, o servidor do banco de dados, que o app acessa pela rede |
| Instalação | Nenhuma: já vem com o Rails | Precisa instalar e configurar o servidor do banco, usuário e senha |
| Bom para | Aprender, desenvolver e apps pequenos ou médios | Apps com muitos acessos ao mesmo tempo, vários servidores usando o mesmo banco e recursos avançados |
| Leitura e escrita de dados ao mesmo tempo | Ler (mostrar os recados) pode ser feito por várias pessoas ao mesmo tempo. Escrever (guardar, mudar ou apagar um recado) é uma de cada vez: as outras esperam na fila | Várias leituras e várias escritas ao mesmo tempo, desde que em linhas diferentes da tabela |
| Vários servidores | Não: o arquivo fica num computador só, e só o app que está nele acessa | Sim: o banco fica num servidor próprio, e vários apps conectam a ele pela rede |
| Recursos avançados | Menos: poucos tipos de dados, sem usuários e permissões, sem réplicas | Mais: usuários e permissões, réplicas (cópias para leitura e para backup) e, no PostgreSQL, tipos como JSON e busca em texto |
Limitações do SQLite, em resumo:
- Escrita: uma por vez. Enquanto um app guarda, muda ou apaga um dado, os outros que querem escrever esperam na fila. Cada escrita leva milissegundos, então a fila quase não existe com poucos acessos, e vira problema só quando muita gente escreve no mesmo instante.
- Leitura: não é limite. Ler dados em paralelo funciona bem, inclusive durante uma escrita.
- Um servidor só. O arquivo fica num computador, então o app não consegue rodar em vários servidores usando o mesmo banco.
- Velocidade, número de tabelas e tamanho não costumam ser problema. Sem rede no caminho, as leituras são rapidíssimas. O tamanho máximo de um banco é de 281 TB, e o número de tabelas, na prática, não tem limite.
Para um app com poucos acessos por segundo, como o mural de recados, o SQLite dá conta com folga. Quando o app passa a ter muita escrita ao mesmo tempo, ou precisa rodar em vários servidores, aí vale migrar para o PostgreSQL ou o MySQL.
Um mini exemplo: duas pessoas clicam em Postar recado no mesmo instante, e cada clique vira um INSERT na tabela messages.
| SQLite | PostgreSQL e MySQL | |
|---|---|---|
| O que acontece | O primeiro INSERT trava o banco inteiro para escrita. O segundo espera, por alguns milissegundos, e só então entra |
Os dois INSERT entram ao mesmo tempo, cada um na sua linha |
| E se forem em tabelas diferentes? | Espera do mesmo jeito: a trava vale para o arquivo todo | Entram ao mesmo tempo |
| E se as duas pessoas editarem o mesmo recado? | Uma espera a outra | Uma espera a outra, só aquela linha fica travada |
| Se a espera passar do limite | O segundo pedido falha com SQLite3::BusyException: database is locked. O Rails espera até 5 segundos antes de desistir (timeout: 5000 no config/database.yml) |
Não costuma acontecer com esse volume |
O SQLite trava o arquivo inteiro; o PostgreSQL e o MySQL travam só a linha que está sendo alterada. Com dezenas de escritas por segundo, essa diferença começa a aparecer. O Rails 8 já deixa o SQLite bem ajustado para isso (usa o modo WAL, em que leitura e escrita andam juntas).
Fontes: a documentação do SQLite sobre o modo WAL (“there can only be one writer at a time”), os limites do SQLite e a documentação do PostgreSQL sobre MVCC (“reading never blocks writing and writing never blocks reading”). Os padrões do Rails (WAL e timeout) estão no adaptador do SQLite do Active Record 8.1 e no database.yml de um app novo.
No dia a dia do Rails, a diferença quase não aparece: o Active Record (a parte do Rails por trás dos models) gera os comandos certos para cada banco, e o código do app praticamente não muda. Quem escolhe o banco é o arquivo config/database.yml.
O SQLite não é só “banco de brinquedo”: desde o Rails 8, ele também é uma opção recomendada para colocar apps em produção. Mesmo assim, PostgreSQL e MySQL continuam muito comuns em empresas.
Esse assunto volta no capítulo 08: em alguns serviços de hospedagem, como o plano gratuito do Render, o disco não é permanente, e por isso o capítulo 08 usa um PostgreSQL em produção.
Onde o Rails anota as migrations que já rodaram
Curiosidade só para a mentoria. Não precisa mostrar para as participantes.
A pergunta “Por que o nome da migration começa com data e hora?” diz que o Rails anota no banco de dados as migrations que já rodou. Essa anotação fica numa tabela que o próprio Rails cria, a schema_migrations. Ela tem uma coluna só, version, com o número (a data e a hora) de cada migration aplicada. Quando você roda bin/rails db:migrate, o Rails compara os arquivos de db/migrate com essa tabela e aplica só os que faltam.
Para ver, no terminal:
bin/rails db:migrate:status
A saída lista cada migration com o número, o nome e o status: up (já aplicada) ou down (ainda não):
database: storage/development.sqlite3
Status Migration ID Migration Name
--------------------------------------------------
up 20261003120000 Create messages
Ou direto na tabela, pelo console:
ActiveRecord::Base.connection.select_values("SELECT version FROM schema_migrations")
O número da última migration também aparece no começo do db/schema.rb, em ActiveRecord::Schema[8.1].define(version: 2026_10_03_120000).
As convenções do Rails são flexíveis
Curiosidade só para a mentoria. Não precisa falar para as participantes: para elas, o importante agora é seguir a convenção.
O capítulo diz que o Rails cria sozinho as colunas id, created_at e updated_at. É o comportamento padrão, mas dá para mudar. O Rails tem convenções, mas não obriga ninguém a segui-las:
- Sem as datas: basta tirar o
t.timestampsda migration. A tabela fica semcreated_ateupdated_at. - Sem o
id:create_table :messages, id: false do |t|cria a tabela sem a colunaid. Muita gente acredita que toda tabela precisa deid, mas não precisa: oidé só o jeito padrão de identificar cada linha. Uma tabela pode ser identificada por outra coluna (primary_key: :code) ou por uma combinação de colunas, a chave primária composta (primary_key: [:product_id, :client_id]). No banco de dados e na migration, isso funciona desde o Rails 5.0. Já os models só passaram a entender a chave composta no Rails 7.1: antes, o Active Record tratava uma tabela assim como se não tivesse chave primária, e quem precisava usava uma gem, acomposite_primary_keys. Tabelas que só ligam outras duas tabelas costumam ser assim, e ocreate_join_tablejá cria semid. Esse comando não acrescenta as datas, mas na prática é comum colocá-las (t.timestampsno bloco), principalmente quando a ligação vira um model e vale saber quando ela foi criada. - Outro tipo de
id:id: :uuid(no PostgreSQL) troca o número por um código único (veja UUID ouidnumérico?), eprimary_key: :codeusa outra coluna como identificador. - Outros nomes: no model,
self.table_name = "recados"liga oMessagea uma tabela com outro nome, por exemplo num banco de dados que já existia antes do app.
Seguir a convenção é o caminho mais curto: o Rails liga tudo sozinho e o código fica parecido com o de qualquer outro app Rails. Sair dela é possível, mas cada exceção precisa ser configurada à mão.
UUID ou id numérico?
Contexto só para a mentoria. Não precisa levar para as participantes: o capítulo só diz que o id é um número que o Rails cria sozinho. Use só se alguém perguntar.
Se alguém perguntar por que o Rails usa 1, 2, 3… e quando faz sentido trocar por UUID (um código aleatório como 3f2b8c1e-…), um overview:
id numérico (padrão) |
UUID | |
|---|---|---|
| Segurança | Previsível: se o recado 12 existe, o 13 também. Quem vê /messages/12 tenta /messages/13, e o total de registros do app fica à mostra |
Impossível de adivinhar e não revela quantos registros existem |
| Tamanho | 8 bytes (bigint) |
16 bytes (128 bits). Índices e chaves estrangeiras ficam maiores |
| Desempenho | Ótimo: cresce em ordem, e cada novo registro entra no fim do índice. Listar os mais recentes (ORDER BY id DESC) lê só o fim do índice |
Um UUID aleatório (v4) tem dois custos. Na escrita: cada registro novo entra numa posição qualquer do índice, o que mexe em partes espalhadas dele e usa pior a memória (só pesa em tabelas grandes). Na listagem: ordenar por id deixa de fazer sentido, porque a ordem é aleatória, e é preciso ordenar por created_at, com um índice só para isso. Buscar um registro pelo id custa quase o mesmo. O UUIDv7, que começa com a data e a hora, resolve os dois: entra em ordem no índice e ordena por criação (o PostgreSQL 18 gera com uuidv7()) |
| Outras diferenças | Simples de ler e de falar (“recado 12”). Message.first e .last são o mais antigo e o mais novo |
Pode ser gerado fora do banco e juntar bancos diferentes sem colisão. Com UUID aleatório, .first e .last deixam de ser o mais antigo e o mais novo, porque o Rails os ordena pela chave primária quando ninguém pede outra ordem. Dá para mudar: self.implicit_order_column = "created_at" no model (ou no ApplicationRecord, para todos) faz o .first e o .last ordenarem por data, e a chave primária só desempata |
Dois cuidados:
- UUID não substitui autorização. Esconder o endereço não impede ninguém de ver o que não deveria. A regra que protege é conferir, em cada pedido, se a pessoa pode ver aquele recado (a falha de não conferir se chama IDOR). O UUID só dificulta adivinhar, uma camada extra.
- Dá para ter os dois. Muitos apps guardam o
idnumérico no banco, por desempenho, e mostram na URL um código público. O Active Record tem osigned_id, que gera um código assinado, impossível de forjar, a partir doid.
Para o mural de recados, o id numérico é a escolha certa: os recados são públicos para quem tem a palavra-chave, a tabela é pequena e a convenção deixa tudo mais simples.
Para ir além:
- Normalização de dados, na Wikipédia: por que dividir os dados em várias tabelas, e as formas normais (1FN, 2FN, 3FN…). É daí que vêm as tabelas de ligação.
- Database normalization basics, da Microsoft (em inglês): uma introdução curta, com exemplos.
has_many :throughouhas_and_belongs_to_many?, no guia de associações do Rails (em inglês): quando a tabela de ligação precisa deide de datas (vira um model) e quando não.- Chaves primárias compostas, no guia do Rails (em inglês).
- Tipo
uuide funções de UUID, na documentação do PostgreSQL (em inglês). - Insecure Direct Object Reference Prevention, da OWASP (em inglês): o IDOR, e por que identificadores difíceis de adivinhar são uma camada extra, e não a proteção principal.
Active Record é um padrão, não só do Rails
Isto é só curiosidade para quem não conhece. Não precisa falar para as participantes, nem se alguém perguntar por que o model se chama Message e a tabela, messages: a resposta do guia (convenção do Rails) basta.
A parte do Rails que cuida dos models se chama Active Record, e esse nome vem de um padrão de projeto (design pattern) descrito por Martin Fowler no livro Patterns of Enterprise Application Architecture (2002). Veja o resumo do padrão no catálogo do Fowler: “um objeto que envolve uma linha de uma tabela do banco de dados, encapsula o acesso ao banco e acrescenta regras sobre esses dados” (tradução livre).
Isso não explica os nomes Message e messages. A pergunta “Por que o model se chama Message e a tabela, messages?” é sobre a convenção de nomes do Rails, e o padrão não diz nada sobre nomes: ele só descreve que um objeto representa uma linha. Singular para o model e plural para a tabela é uma escolha do Rails, parte do “convenção em vez de configuração”, e quem faz a conversão é o inflector do Active Support, que segue as regras do inglês (por isso Mensagem viraria mensagems). Quando o plural é irregular, dá para ensinar o Rails em config/initializers/inflections.rb.
A ideia do padrão, em Rails:
| No padrão | No Mural de recados |
|---|---|
| um objeto para cada linha | Message.first é um recado, uma linha |
| um atributo para cada coluna | message.author, message.content |
| o próprio objeto se guarda e se busca | Message.create, Message.find, message.destroy |
Já a ligação entre a classe Message e a tabela messages é a convenção de nomes do Rails, e não do padrão.
O padrão não é exclusivo do Rails. O Eloquent, do Laravel (PHP), segue a mesma ideia, e o ORM do Django (Python) é bem parecido. Outras bibliotecas preferem separar o objeto do acesso ao banco, como no padrão Data Mapper (por exemplo, Doctrine, em PHP, e SQLAlchemy, em Python) ou num repositório à parte, como o Repo do Ecto, em Elixir.
A analogia da planilha e da assistente
Contexto só para a mentoria. Não precisa levar para as participantes: a analogia aparece no capítulo, mas esta seção trata do limite dela (classe e objeto), que o capítulo não explica.
O capítulo compara a migration com montar uma planilha e o model com uma assistente especialista que só cuida dessa planilha. É só uma comparação didática, para separar estrutura (migration) de dados (model). Não é uma descrição exata de como o Rails funciona.
O ponto em que ela não funciona é a diferença entre classe e objeto: Message, com M maiúsculo, é a classe (a “assistente”), mas o que Message.first devolve é um objeto, um recado específico, uma linha da tabela. Para quem nunca programou, essa diferença dificilmente fica clara de primeira, e não é objetivo deste capítulo.
- Não introduza os termos “classe” e “objeto” se ninguém perguntar.
- No capítulo 03, a analogia ganha um complemento: a assistente “te entrega aquele recado, com todas as informações dele”.
- Se alguém perceber a diferença sem ajuda, ótimo: confirme e diga que esses nomes vão aparecer com mais calma depois.
Por que generate model
Contexto só para a mentoria. Não precisa levar para as participantes: é a justificativa de como o guia foi escrito. A palavra scaffold só aparece para elas nas caixas sobre IA dos capítulos 03 a 05.
O capítulo usa bin/rails generate model, e não scaffold, para a participante ver só o model e a migration, sem telas. O projeto não usa scaffold em nenhum capítulo (veja Sem scaffold): rotas, controller e views são escritos à mão nos capítulos 03 a 05.