Model-Instances kopieren
Veröffentlich am 10.03.2009, 23:08
Ich war heute auf der Suche nach einer Möglichkeit, eine komplette Model-Instance inkl. aller Daten (normale Felder, ForeignKeys, Many-to-Many Feldern und Dateien) zu kopieren. Es sollte ein vollständiges Duplikat erzeugt werden.
Da ich nicht fündig geworden bin, möchte ich euch meine Implementierung nicht vorenthalten:
from django.db import models
from django.conf import settings
import os
import shutil
class CloneableModel(models.Model):
def clone(self):
initial = dict([(f.name, getattr(self, f.name))
for f in self._meta.fields if \
not isinstance(f, models.AutoField) \
and not isinstance(f, models.FileField) \
and not f in self._meta.parents.values()])
obj = self.__class__(**initial)
obj.save()
for field in [f.name for f in self._meta.many_to_many]:
oldfield = getattr(self, field)
setattr(obj, field, oldfield.all())
for field in [f.name for f in self._meta.fields if isinstance(f, models.FileField)]:
oldfield = getattr(self, field)
newfield = getattr(obj, field)
if oldfield:
newpath = newfield.field.storage.get_available_name(oldfield.name)
shutil.copy(
oldfield.path,
os.path.join(settings.MEDIA_ROOT, newpath)
)
setattr(obj, field, newpath)
obj.save()
return obj
class Meta:
abstract = True
Dieses Stück Code irgendwo in eurem Projekt abspeichern und die betroffenen Models von dieser Klasse statt von models.Model erben lassen. Dadurch erhalten euere Models eine zusätzliche Methode clone.
Diese Methode kopiert das gesamte Objekt mitsamt m2m Feldern und Dateien. Bei den Dateien ist zu beachten, dass jede Datei auch auf Filesystem Ebene kopiert wird, es wird also keine Referenz auf die Datei des alten Objekts angegeben.
Danke an Michael Elsdoerfer für die Grundidee.
Model-Instance Änderungen protokollieren
Veröffentlich am 18.02.2009, 17:45
Ich bin heute mal wieder auf ein scheinbar schwieriges Problem gestoßen.
Folgende Problemstellung ergibt sich:
Die Daten eines bestimmten Models sollen bei Änderung an einen anderen Dienstleister übertragen werden. Hierbei ist wichtig, dass nur geänderte Daten übertragen werden sollen.
- wie kann man mitbekommen, dass sich ein Objekt/Datensatz geändert hat?
- wie sollen diese "zum Export" gespeichert werden, ohne jedem Model ein weiteres Feld zuzuweisen?
- wie kann verhindert werden, dass Objekt mehrfach mehrfach übertragen wird?
Vorab erst einmal meine Lösung für die Problematik, die Erklärung folgt:
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from datetime import datetime
class ExportQueue(models.Model):
content_type = models.ForeignKey(ContentType, verbose_name=_('Content type'))
object_id = models.PositiveIntegerField(_('Instance'))
instance = generic.GenericForeignKey()
date_added = models.DateTimeField(_('Date (created)'), default=datetime.now, blank=False)
date_exported = models.DateTimeField(_('Date (exported)'), blank=True, null=True)
is_exported = models.BooleanField(_('Is exported'), default=False)
def __unicode__(self):
return '%s (%s)' % (self.instance, self.date_added)
@staticmethod
def add_to_queue(instance):
try:
ctype = ContentType.objects.get_for_model(instance)
ExportQueue.objects.get(content_type__pk=ctype.id, object_id=instance.id, is_exported=False)
return False
except:
ExportQueue.objects.create(instance=instance)
return True
class Meta:
verbose_name = _('Export Queue')
verbose_name_plural = _('Export Queue')
class ExportableBaseModel(models.Model):
def __cmp__(self, other):
return cmp(';'.join(['%s' % self.__dict__[v] for v in self.__dict__]), ';'.join(['%s' % other.__dict__[v] for v in other.__dict__]))
def save(self, *args, **kwargs):
add_to_queue = False
if not self.pk:
add_to_queue = True
else:
old = type(self).objects.get(pk=self.pk)
if cmp(self, old):
add_to_queue = True
obj = super(ExportableBaseModel, self).save(*args, **kwargs)
if add_to_queue:
ExportQueue.add_to_queue(self)
return obj
class Meta:
abstract = True
Alle Models, die exportiert werden sollen, erben von ExportableBaseModel. Dadurch muss nicht in jedem betroffenen Model ein Feld export_needed (o.ä.) einfügt werden.
Das ExportableBaseModel überschreibt die normale save Methode von models.Model. In dieser neuen save Methode wird dann das aktuelle Objekt (aus der Datenbank) mit dem geänderten und noch nicht gespeicherten neuen Objekt verglichen.
Dieser Vergleich basiert auf zwei Strings, welche aus dem zusammengeführten *dict* Feld der beiden Objekte besteht. Wenn dieser Vergleich nun eine Änderung feststellt, wird die add_to_queue Methode des ExportQueue Models aufgerufen.
Die add_to_queue Methode prüft vor dem Anlegen eines neuen Eintrags, ob bereits das gleiche Objekt in der ExportQueue angelegt ist (und berücksichtigt dabei auch das is_exported Feld).
Das in meinem Code-Beispiel enthaltene Model ExportQueue hat ein Feld is_exported, welches nach dem Export auf True gesetzt wird. So kann nachvollzogen werden ob das Objekt bereits exportiert wurde. Zusätzlich gibt es noch ein date_exported Feld, welches nachvollziehbar macht, wann der Datensatz exportiert wurde. (das Setzen dieser Felder ist im Beispiel nicht implementiert!)
Diese Lösung ist sicher noch optimierungswürdig, aber funktioniert grundsätzlich.
DB Update für Django, ein Konzept
Veröffentlich am 08.11.2008, 11:17
Ich habe mir heute morgen einige Schema Evolution Tools für Django angesehen. django-evolution fällt aufgrund schlechter Erfahrung aus, dmigrations kommt nicht in Frage da nur MySQL unterstützt wird.
Bis jetzt mache ich meine Datenbank-Änderungen manuell mit einem Ordner voller sql Dateien, für jede Änderung eine Datei. Das klappt auch super, einziges Manko: man muss wissen, bei welcher "Version" man mit dem ausführen der sql Dateien beginnt.
Diesen Vorgang möchte ich nun automatisieren. Grundsätzlich muss erwähnt werden, das ich solch ein Tool nur für das Aktualisieren von Webseiten, die sich bereits im Produktionsbetrieb befinden, verwenden würde. Zum Zeitpunkt der Entwicklung ist die Erstellung der Änderungsscripte zu aufwändig.
Die Anwendung und Vorgehensweise stelle ich mir wie folgt vor:
Es existiert ein Model welches für jedes Model die aktuelle Version speichert:
class ModelVersion(models.Model):
model = models.ForeignKey(ContentType, unique=True)
version = models.PositiveIntegerField()
.. für in jeder Application existiert ein Python Modul "dbupdates" welches ein Dictionary CHANGES enthält:
CHANGES = {
1, (
'ATLER TABLE myapp_mymodel ADD COLUMN ...',
),
2, (
'ALTER TABLE myapp_mymodel DROP COLUMN ...',
'DROP TABLE ...',
),
}
.. um Änderungen auszuführen, wird folgender Befehl ausgeführt:
alle Anwendungen aktualisieren:
./manage dbupdate
oder nur eine Anwendung aktualisieren:
./manage dbupdate [application]
Alternativ kann über den Parameter --startat=version die Startversion übergeben werden (ich merkte bereits an, das die Version zum Zeitpunkt des Checkouts bekannt sein muss, dannach wird die aktuelle Version in der Datenbank gespeichert.
Möglich ist auch, die CHANGES optional Datenbank-abhängig zu gestalten um auf eigenarten gewisser RDBMS einzugehen.
Soweit meine Idee, Feedback ist ausdrücklich erwünscht. Entweder als Kommentar, per Mail oder im IRC (#django-de / Freenode).
Nachtrag: Ich muss feststellen, dass ich hier bis auf wenige Kleinigekeiten dmigrations "nachbaue". Jedoch sehe ich derzeit keine andere Möglichkeit, da dmigrations ausschließlich mit MySQL arbeitet. Abgesehen davon wäre dmigrations jedoch genau das richtige für meine Anwendungszwecke.
Tags: database, dbupdate, django, schema evolution