Duration: uma representação de intervalo de tempo muito útil
Duration é coberto pelo padrão ISO 8601 que trata de data e tempo e define a quantidade de tempo em um dado intervalo.
Talvez o exemplo mais simples e clássico para explicar uma duração seria a diferença entre duas datas ou dois horários
- Quantos anos tem uma pessoa?
- Quantos dias faltam para o pagamento daquele empréstimo?
- O tempo de espera para o início do show é de n minutos
Dessa forma um tipo de dado representando a duração de um determinado intervalo de tempo se torna útil em muitos cenários
A representação legível para humanos de uma duração é feita, de acordo com o padrão ISO 8601, por uma letra representando a unidade de tempo e um número representando a quantidade dessa unidade. Assim temos que:
P é um marco utilizado para separar a porção de um Período de uma dada duração, representado da seguinte forma:
Y | representa o número de anos | (Years) |
M | representa o número de meses | (Months) |
W | representa o número de semanas | (Weeks) |
D | representa o número de dias | (Days) |
T é um marco utilizado para separar a porção de Tempo de uma dada duração, representado da seguinte forma:
H | representa o número de horas | (Hours) |
M | representa o número de minutos | (Minutes) |
S | representa o número de segundos | (Seconds) |
Logo P
3W2D
T
10H45M
representa uma duração de 3 semanas, 2 dias, 10 horas e 45 minutos
Ecto, Postgrex e Duration
A estrutura Duration
e suas funções no Elixir foram introduzidas na versão 1.17 e em versões anteriores, segundo a documentação, a estrutura utilizada para decodificar intervalos de tempo no PostgreSQL era o Postgrex.Interval
que continua sendo a estrutura padrão
A seguir temos a definição do schema e migration para um conjunto fictício de dados e sua materialização no banco de dados
Schema
schema "records" do
field :name, :string
field :description, :string
field :start, :utc_datetime
field :duration, :duration
end
Migration
def change do
create table(:records, primary_key: false) do
add :id, :uuid, primary_key: true
add :name, :string
add :description, :string
add :start, :utc_datetime
add :duration, :duration
timestamps(type: :utc_datetime)
end
end
Estrutura da tabela
Column | Type | Collation | Nullable | Default
-------------+--------------------------------+-----------+----------+---------+
id | uuid | | not null | |
name | character varying(255) | | | |
description | character varying(255) | | | |
start | timestamp(0) without time zone | | | |
duration | interval | | | |
Dados persistidos
id | name | description | start | duration |
--------------------------------------+------------+----------------------+-------+----------+
664e5fd6-17ab-4a27-8799-9ebca4c603e5 | Record AA | The Record AA descr | | 11 days |
O que deu errado e porque estou falando desse assunto?
Como a estrutura padrão mapeada para intervalos no PostgreSQL é o Postgrex.Interval
quando o tipo :duration
é definido no schema e nas migrations o valor é persistido corretamente porém na recuperação do dado e o consequente mapeamento para o struct %Duration{}
ocorre o seguinte erro
(ArgumentError) cannot load
%Postgrex.Interval{months: 0, days: 11, secs: 0, microsecs: 0}
as type :duration for field :duration
Quando se está trabalhando num produto é interessante manter o foco no trabalho, no domínio do negócio e por vezes misturar código de infraestrutrura pode gerar débitos técnicos, dificuldade de manutenção e talvez o principal seja tirar a clareza do código.
É interessante poder manter homogeneidade do código mantendo tanto quanto possível o uso das bibliotecas padrão sempre que suportadas, e nesse caso a estrutura Duration
era suportada e por um motivo que eu não sabia não estava funcionando. O conhecimento de ser Postgrex.Interval
a estrutura padrão para decodificar o tipo interval
do PostgreSQL veio através da pesquisa sobre como resolver o erro aí em cima.
Aprender é uma constante na indústria de tecnologia, mas nem sempre dispomos do tempo que gostaríamos para adquirir novos conhecimentos, explorar uma biblioteca ou framework novo, etc, portanto torna-se praticamente impossível não buscar caminhos mais curtos, que resolvam o problema e permitam seguir adiante, porque tempo é dinheiro e queria resolver o problema logo
Procurei bastante mas não encontrei nada no estilo stackoverflow, um simples copy & paste para commitar mais um código e encerrar o dia, então fui juntando informando aqui e acolá, parando de ser preguiçoso e lendo mais a documentação para economizar o tempo de alguém no futuro
Talvez eu não tenha encontrado muita coisa por não utilizar as keywords corretas ou por insistir em usar o DuckDuckGo
Configuração
Basicamente a configuração de uma Duration
como decodificadora de interval
se concentra em dois arquivos: config.exs
e postgrex_types.ex
O arquivo postgrex_types.ex
contém a configuração de uma opção ao tipo padrão Postgrex.Interval
, já o arquivo config.exs
associa os tipos customizados ao Ecto.Repo
Sem mais demora e preparando aquele pedaço de código para copiar, colar e encerrar o dia, assim ficaria a configuração que faz tudo funcionar de forma quase mágica (quase porque na verdade é design de código de caras muito bons que mantém isso tudo funcionando)
# config/config.exs
# configures postgrex types
import Config
config :my_app,
ecto_repos: [MyApp.Repo],
generators: [timestamp_type: :utc_datetime]
# configures postgrex types
config :my_app, MyApp.Repo, types: MyApp.PostgresTypes
# Configures the endpoint
(config continues...)
# lib/my_app/postgres_types.ex
# set the interval decode
Postgrex.Types.define(
ConstruaApi.PostgresTypes,
[] ++ Ecto.Adapters.Postgres.extensions(),
interval_decode_type: Duration
)
# --- that's all folks ---
Enfim funcionou
A busca por um exemplo simples e direto não funcionou para mim e na correria em busca de solução esqueci de perguntar para os mecanismos de IA disponíveis como fazer isso de forma rápida e fácil (Doh!)
Agora se alguém precisar de um código na mão pra resolver este problema ou outros similares aqui está, mas não esqueça que os GPTs, Geminis e outras IAs da vida estão ai pra ajudar
uma versão em inglês está disponível aqui