Schimbarea tipului de câmp al modelului Django din CharField în ForeignKey (Programare, Django, Chei Străine, Migrarea Bazei De Date)

ChrisM a intrebat.

Am nevoie să schimb tipul unui câmp într-unul dintre modelele mele Django din CharField la ForeignKey. Câmpurile sunt deja populate cu date, așa că mă întrebam care este cea mai bună sau corectă modalitate de a face acest lucru. Pot doar să actualizez tipul de câmp și să migrez, sau există vreun „gotchas” posibil de care să fiu conștient? N.B.: Eu folosesc doar operațiunile de gestionare Django vanilie (makemigrations și migrate), nu South.

Comentarii

  • Aveți grijă la valorile duplicate dacă nu aveți o constrângere unică pe CharField? Depinde foarte mult dacă duplicați datele înainte sau doar doriți să le mutați în propriul tabel. Dacă este ultima variantă, cel mai bun lucru de făcut depinde de ceea ce înseamnă datele dvs. Django nu poate deduce un fel de conversie magică între CharField și ForeignKey. Va trebui să scrieți singur o migrare sensibilă. –  > Por user234461.
  • Cred că acest lucru este similar cu ceea ce căutați. stackoverflow.com/questions/5373906/… –  > Por Vibhu.
3 răspunsuri
Joey Wilhelm

Acesta este probabil un caz în care doriți să faceți o migrare în mai multe etape. Recomandarea mea pentru acest lucru ar arăta ceva de genul următor.

În primul rând, să presupunem că acesta este modelul dvs. inițial, în interiorul unei aplicații numite discography:

from django.db import models

class Album(models.Model):
    name = models.CharField(max_length=255)
    artist = models.CharField(max_length=255)

Acum, vă dați seama că doriți să folosiți în schimb un ForeignKey pentru artist. Ei bine, după cum am menționat, nu este un proces simplu pentru acest lucru. Trebuie să se facă în mai multe etape.

Pasul 1, adăugați un nou câmp pentru ForeignKey, asigurându-vă că îl marcați ca fiind nul:

from django.db import models

class Album(models.Model):
    name = models.CharField(max_length=255)
    artist = models.CharField(max_length=255)
    artist_link = models.ForeignKey('Artist', null=True)

class Artist(models.Model):
    name = models.CharField(max_length=255)

și creați o migrație pentru această modificare.

./manage.py makemigrations discography

Pasul 2, populează noul câmp. Pentru a face acest lucru, trebuie să creați o migrație goală.

./manage.py makemigrations --empty --name transfer_artists discography

Odată ce aveți această migrație goală, doriți să adăugați un singur câmp RunPython operațiune pentru a lega înregistrările. În acest caz, ar putea arăta cam așa:

def link_artists(apps, schema_editor):
    Album = apps.get_model('discography', 'Album')
    Artist = apps.get_model('discography', 'Artist')
    for album in Album.objects.all():
        artist, created = Artist.objects.get_or_create(name=album.artist)
        album.artist_link = artist
        album.save()

Acum, că datele dvs. sunt transferate în noul câmp, ați putea de fapt să terminați și să lăsați totul așa cum este, folosind noul câmp pentru tot. Sau, dacă doriți să faceți puțină curățenie, doriți să creați încă două migrări.

Pentru prima migrare, veți dori să ștergeți câmpul original, artist. Pentru a doua migrare, redenumiți noul câmp artist_link în artist.

Acest lucru se face în mai mulți pași pentru a ne asigura că Django recunoaște operațiunile în mod corespunzător. Ați putea crea o migrare manuală pentru a gestiona acest lucru, dar vă las pe dumneavoastră să vă dați seama.

Comentarii

  • Vă mulțumesc pentru răspunsul prompt și cuprinzător. Acest lucru m-a stimulat, de asemenea, să mă așez în sfârșit și să învăț în mod corespunzător despre migrațiile Django! –  > Por ChrisM.
  • Nu ar trebui să fie ./manage.py makemigrations --empty ..., totuși? Pasul 2 are makemigration (singular). –  > Por ChrisM.
  • Mulțumesc, am nevoie doar de –empty pe makemigrations între cele două aplicații –  > Por Roberth Solís.
  • Prefer să transfer datele în scripturi pură python, dar să schimb schema bazei de date în migrațiile django. –  > Por You Gakukou.
  • Joey Am adăugat un răspuns pe care poate ai vrea să te gândești să-l copiezi pentru a-l completa pe al tău, poate. Simțiți-vă liber să faceți acest lucru. –  > Por Pere Picornell.
jerrymouse

Adăugând pe lângă răspunsul lui Joey, pași detaliați pentru Django 2.2.11.

Iată modelele din cazul meu de utilizare, care constă dintr-un Company și Employee model. Trebuie să convertim designation într-un câmp cu cheie externă. Numele aplicației se numește core

class Company(CommonFields):
    name = models.CharField(max_length=255, blank=True, null=True

class Employee(CommonFields):
    company = models.ForeignKey("Company", on_delete=models.CASCADE, blank=True, null=True)
    designation = models.CharField(max_length=100, blank=True, null=True)

Pasul 1

Creați o cheie externă designation_link în Employee și marcați-o ca null=True

class Designation(CommonFields):
    name = models.CharField(max_length=255)
    company = models.ForeignKey("Company", on_delete=models.CASCADE, blank=True, null=True)

class Employee(CommonFields):
    company = models.ForeignKey("Company", on_delete=models.CASCADE, blank=True, null=True)
    designation = models.CharField(max_length=100, blank=True, null=True)
    designation_link = models.ForeignKey("Designation", on_delete=models.CASCADE, blank=True, null=True)

Pasul 2

Creați o migrație goală. Folosind comanda:

python app_code/manage.py makemigrations --empty --name transfer_designations core

Aceasta va crea următorul fișier în migrations director.

# Generated by Django 2.2.11 on 2020-04-02 05:56

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('core', '0006_auto_20200402_1119'),
    ]

    operations = [
    ]

Pasul 3

Populați migrația goală cu o funcție care face o buclă peste toate Employees, creează un fișier Designation și îl leagă de fișierul Employee.

În cazul meu de utilizare, fiecare Designation este, de asemenea, legat de un Company. Ceea ce înseamnă că Designation poate conține două rânduri pentru „manageri”, unul pentru societatea A, altul pentru societatea B.

Migrarea finală ar arăta cam așa:

# core/migrations/0007_transfer_designations.py

# Generated by Django 2.2.11 on 2020-04-02 05:56

from django.db import migrations

def link_designation(apps, schema_editor):
    Employee = apps.get_model('core', 'Employee')
    Designation = apps.get_model('core', 'Designation')
    for emp in Employee.objects.all():
        if(emp.designation is not None and emp.company is not None):
            desig, created = Designation.objects.get_or_create(name=emp.designation, company=emp.company)
            emp.designation_link = desig
            emp.save()

class Migration(migrations.Migration):

    dependencies = [
        ('core', '0006_auto_20200402_1119'),
    ]

    operations = [
        migrations.RunPython(link_designation),
    ]

Pasul 4

În cele din urmă, rulați această migrare folosind:

python app_code/manage.py migrate core 0007

Comentarii

  • Bine explicat @jerrymouse –  > Por Hemant Kumar.
Pere Picornell

Aceasta este o continuare a răspunsului excelent al lui Joey.Cum să redenumiți noul câmp cu numele original?

Dacă câmpul are date, înseamnă probabil că îl folosiți în altă parte în proiect, prin urmare, această soluție vă va lăsa cu un câmp numit diferit și va trebui fie să refactorizați proiectul pentru a utiliza noul câmp, fie să ștergeți câmpul vechi și să îl redenumiți pe cel nou.

Fiți conștienți de faptul că acest proces nu vă va împiedica să refactorizați codul. Dacă foloseați un CharField cu CHOICES, accesați conținutul acestuia cu get_filename_display(), de exemplu.

Dacă încercați să ștergeți câmpul pentru a face o migrare, pentru ca apoi să redenumiți celălalt câmp și să faceți o altă migrare, veți vedea că Django se va plânge pentru că nu puteți șterge un câmp pe care îl utilizați în proiect.

Doar creați o migrare goală, așa cum a explicat Joey, și puneți acest lucru în operațiuni:

operations = [
    migrations.RemoveField(
        model_name='app_name',
        name='old_field_name',
    ),
    migrations.RenameField(
        model_name='app_name',
        old_name='old_field_name_link',
        new_name='old_field_name',
    ),
]

Apoi rulați migrate și veți avea modificările făcute în baza de date, dar evident nu și în model, este timpul acum să ștergeți câmpul vechi și să redenumiți noul câmp ForeignKey cu numele original.

Nu cred că acest lucru este deosebit de complicat, dar, cu toate acestea, faceți acest tip de lucruri numai dacă înțelegeți pe deplin cu ce vă confruntați.