Como começou
Quando eu estava desenvolvendo uma aplicação web
em Django, criei um modelo para armazenar endereços. Eu estava usando esses
endereços com dois objetivos: pagamento e entrega. O modelo de endereço tinha
os campos normais (rua, CEP etc), e o modelo do perfil se parecia com isto:
class Profile(models.Model):
...
shipping_address = models.ForeignKey(Address, related_name='shipping')
billing_address = models.ForeignKey(Address, related_name='billing')
Tudo
estava certo até que a validação para os endereços tinha que ser diferente para
o endereço do pagamento e de entrega. (Caixas de correio eram permitidas para o
endereço de pagamento, mas não para o de entrega).
Decidi criar dois modelos diferentes (Adress e
BillingAddress – “Endereço e Endereço de Pagamento”), ambos herdando de uma classe
de base abstrata
(BaseAddress). O modelo do perfil mudou para usar estes modelos:
class Profile(models.Model):
...
shipping_address = models.ForeignKey(Address, related_name='shipping')
billing_address = models.ForeignKey(BillingAddress, related_name='billing')
Migração
Infelizmente,
essa mudança foi feita depois que o site estava no ar, então eu precisava
migrar o banco de dados. Eu já estava usando o South para lidar com as migrações do banco, então
eu não estava muito preocupado. Apesar de a maioria dos casos de migração
criados pelo South funcionarem bem, esta precisava de intervenção manual.
A
migração consistia em 4 passos:
- Criar uma tabela para o
modelo BillingAddress. - Copiar todos os endereços de
pagamento para a tabela modelo BillingAddress. - Mudar a tabela modelo do
perfil para ter referências para o modelo BillingAddress. - Remover os endereços de pagamento
da tabela modelo de endereços. .
Para
facilitar a minha vida, decidi que no segundo passo eu iria apenas copiar o ID
dos endereços antigos para o novo BillingAddress. Dessa maneira, eu não teria
que atualizar cada perfil para apontar para o novo BillingAddress. Um pouco
bruto, talvez, mas efetivo.
Inicialmente, tudo pareceu funcionar. A migração
funcionou bem, e a aplicação ainda funcionava. Todos estavam felizes e eu segui
para uma tarefa diferente. Até eu receber um erro dois dias depois:
IntegrityError: duplicate key value violates unique constraint "profile_billingaddress_pkey"
Como
mostra o erro, a aplicação estava tentando criar um BillingAddress com um ID
que já existia.
O que aconteceu?
O Django tem
um bom ORM que abstrai todos os tipos de
coisas com as quais você não quer se preocupar. Por exemplo: a não ser que você
especifique diferente disso, um modelo tem um campo de preenchimento de ID automático
que é uma uma chave primária autoincrementável. E, como
especificado na documentação, “não tem jeito de saber qual será o valor de um
ID antes de você salvar (), porque esse valor é calculado pelo seu banco de dados, não pelo Django”.
Por algum
motivo, o PostgreSQL estava calculando o ID errado ao armazenar o BillingAddress.
Ele tinha que estar relacionado à nossa recente migração. Mas por que eu não vi
isso durante os testes? E por que isso somente começou a ocorrer depois de dois
dias?
Depois de procurar um pouco, descobri a
existência de algumas sequências do PostgreSQL. Olhando para trás, uma primeira
dica poderia ter sido encontrada ao olhar pelo SQL gerado pelo Django:
$ manage sql profile
...
CREATE TABLE "profile_billingaddress" (
"id" sequence NOT NULL PRIMARY KEY,
...
Quando
inspecionei o banco de dados, eu tive mais informações sobre a sequência:
$ manage dbshell
db=> ds
List of relations
Schema | Name | Type | Owner
--------+-------------------------------------------------+----------+-------
... | ... | ... | ...
public | profile_billingaddress_id_seq | sequence | mark
db=> d profile_billingaddress_id_seq
Sequence "public.profile_billingaddress_id_seq"
Column | Type | Value
---------------+---------+-------------------------------
sequence_name | name | profile_billingaddress_id_seq
last_value | bigint | 6
start_value | bigint | 1
increment_by | bigint | 1
max_value | bigint | 9223372036854775807
min_value | bigint | 1
cache_value | bigint | 1
log_cnt | bigint | 28
is_cycled | boolean | f
is_called | boolean | f
E aí
estava o meu problema: o last_value era 6, enquanto existiam muito mais
endereços no banco de dados. Ao inspecionar a tabela relacionada (profile_billingaddress)
logo depois de a migração ter sido feita, constatei que os IDs utiizados foram 7,
9, 12, 14, 16 etc. Isso foi o resultado da minha decisão de copiar os IDs da
tabela de endereços original.
Então
quando os primeiros seis novos BillingAddresses foram criados, eu tive sorte, pois
eles tinham um único ID (1 até 6). É por isso que eu não tinha descoberto o
problema mais cedo: eu apenas confirmei que a migração tinha funcionado depois
de adicionar alguns (menos que 7) novos BillingAddresses.
A solução
Como é o
caso na maioria das vezes: uma vez que você descobriu a causa do problema, a
solução se torna trivial. Nesse caso, eu tinha apenas que configurar o last_value
da sequência como o mais alto ID da tabela.
Escolhi a solução rápida-e-suja para criar uma
nova e vazia migração:
$ manage schemamigration profile fix_sequence_problem --empty
Então
adicionei este código para o método forwards:
if orm.BillingAddress.objects.count():
highest_number = db.execute('select id from profile_billingaddress order by id desc limit 1;')[0][0]
db.execute('alter sequence profile_billingaddress_id_seq restart with %s;' % (highest_number + 1))
Provavelmente
eu poderia ter configurado a sequência para o número mais alto, mas escolhi
incrementar com um a mais só para garantir que eu não tivesse um erro
só-por-um. A segunda linha falha se não existir um BillingAddresses, então, para
prevenir que meus testes falhassem, eu chequei a existência de BillingAddresses
explicitamente.
Depois
que executei essa migração no ambiente de produção, os usuários podiam criar
perfis novamente sem acionar um IntegrityError.
?
Texto original disponível em http://www.vlent.nl/weblog/2011/05/06/integrityerror-duplicate-key-value-violates-unique-constraint/



