A migration que transformou as colunas bilíngues em JSONB
Todo título, descrição e dica bilíngue que os cursos anteriores citaram
Catalog::Course#title e #description, Catalog::Lesson#title e #content_markdown, Catalog::Challenge#instruction, #solution e #hints: todas essas colunas são jsonb. A maioria não começou assim. courses.title, lessons.title, lessons.content_markdown e challenges.instruction nasceram como colunas string e text comuns e foram convertidas um dia depois; hints e solution vieram mais tarde e já foram criadas como jsonb. db/migrate/20260227224007_convert_course_fields_to_jsonb.rb é a migration em que courses.title deixou de ser uma string comum:
# db/migrate/20260227224007_convert_course_fields_to_jsonb.rb
def up
add_column :courses, :title_jsonb, :jsonb, default: {}
add_column :courses, :description_jsonb, :jsonb, default: {}
execute <<-SQL
UPDATE courses
SET title_jsonb = jsonb_build_object('en', title, 'pt-BR', title),
description_jsonb = jsonb_build_object('en', description, 'pt-BR', description)
SQL
change_column_null :courses, :title_jsonb, false
remove_column :courses, :title
remove_column :courses, :description
rename_column :courses, :title_jsonb, :title
rename_column :courses, :description_jsonb, :description
end
Lendo a migration
Ela faz cinco movimentos, em ordem. Primeiro acrescenta uma coluna jsonb temporária ao lado de cada coluna antiga. Depois preenche todas as linhas com jsonb_build_object, uma função do Postgres que monta um objeto JSON no próprio SQL. Ela embrulha a única string existente sob duas chaves, 'en' e 'pt-BR', as duas iguais no começo porque ainda não havia tradução. Em terceiro lugar, torna a nova coluna de título NOT NULL, já que toda linha tem valor. Em quarto, apaga as colunas de string antigas e, por fim, renomeia as novas colunas para os nomes antigos.
A tabela termina com os mesmos nomes de coluna, title e description, guardando JSON em vez de texto puro.
jsonb_build_object, e por que a migration não faz só um cast
title::jsonb sozinho falharia, porque um texto como Some Title não é JSON válido. Mesmo uma string com aspas viraria uma string JSON, e não o objeto {"en": ..., "pt-BR": ...} que o código do app espera. jsonb_build_object('en', title, 'pt-BR', title) monta o objeto chave por chave dentro do Postgres, num único UPDATE sobre todas as linhas, sem loop em Ruby pelos cursos.
A migration sabe se desfazer
# mesmo arquivo
def down
add_column :courses, :title_string, :string
add_column :courses, :description_text, :text
execute <<-SQL
UPDATE courses
SET title_string = title->>'en',
description_text = description->>'en'
SQL
change_column_null :courses, :title_string, false
remove_column :courses, :title
remove_column :courses, :description
rename_column :courses, :title_string, :title
rename_column :courses, :description_text, :description
end
Os mesmos cinco movimentos rodam ao contrário, com uma peça nova: title->>'en', o operador de seta do Postgres, que tira o valor de 'en' do objeto JSON como texto puro. A aula 4 trata desse operador. Por enquanto, repare que ele espelha o jsonb_build_object: um monta um objeto JSON a partir de valores simples, o outro extrai um valor simples de um objeto JSON.
O que este curso cobre
Sete aulas, todas sobre código deste app. A aula 2 trata do ActiveRecord::Store, a ferramenta do próprio Rails para esse tipo de coluna, numa tabela descartável, já que este app não a usa. A aula 3 mostra por que o app escreveu o próprio concern Translatable no lugar dela. A aula 4 consulta JSONB direto em SQL com o mesmo operador ->>, em queries de produção. A aula 5 trata de índices GIN: o que os índices do app aceleram e o que não aceleram. A aula 6 trata do principal motivo de existir do JSONB, dados de formato não fixo, numa parte do app que está rodando agora.
Continue com a aula 2
store_accessor: a ferramenta do próprio Rails para esse tipo de coluna
O store_accessor do Rails numa coluna jsonb, testado numa tabela descartável, incluindo chaves symbol que retornam nil após recarregar.
Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.