Django - Ändern eines DB-Elementes erstellt ein neues?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
Judge
User
Beiträge: 129
Registriert: Mittwoch 13. Juni 2012, 22:27
Wohnort: Ratingen
Kontaktdaten:

Hallo zusammen,

heute mal mit einer Django-Frage ;-)
Ich habe folgendes model:

Code: Alles auswählen

from django.db import models

class User(models.Model):
    user_id     = models.IntegerField(unique=True, primary_key=True)
    first_name  = models.CharField(max_length=64)
    last_name   = models.CharField(max_length=64)
Als DB Backend verwende ich für die lokale Entwicklung SQLite3.

Wenn ich ein neues Element erstelle, ist alles OK:

Code: Alles auswählen

>>> from bot.models import User
>>> neuer_user = User(first_name="Vorname", last_name="Nachname", user_id=5)
>>> neuer_user.save()
>>> del neuer_user
>>> neuer_user = User.objects.get(last_name="Nachname")
>>> neuer_user.user_id
5
>>>
Aber wenn ich diesen Datensatz nun ändern und speichern will, wird stattdessen ein neuer Datensatz in der Datenbank erstellt:

Code: Alles auswählen

>>> User.objects.all()
<QuerySet [<User: Marc Richter>, <User: Vorname Nachname>, <User: Alucard Țepeș>]>
>>> neuer_user = User.objects.get(last_name="Nachname")
>>> neuer_user.user_id
5
>>> neuer_user.user_id = 7
>>> neuer_user.user_id
7
>>> neuer_user.save()
>>> User.objects.all()
<QuerySet [<User: Marc Richter>, <User: Vorname Nachname>, <User: Vorname Nachname>, <User: Alucard Țepeș>]>
>>>
Laut Django Doku soll das aber genau so gehen.

Was mache ich (oder Django?) falsch?
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Hast du das mal mit etwas anderem als der Id probiert? Die zu ändern ist eine ganz bescheidene Idee. Konzeptionell sichert die Eindeutigkeit zu, und wird automatisch vergeben. Und sollte sich niemals ändern.
Benutzeravatar
Judge
User
Beiträge: 129
Registriert: Mittwoch 13. Juni 2012, 22:27
Wohnort: Ratingen
Kontaktdaten:

Du hast Recht, aber das weiß ich. Das soll auch nicht Teil meines späteren Codes sein, sondern ich bereite gerade etwas Material für einen Django-Workshop vor und stolpere dabei selbst über dieses Verhalten. Das würde ich daher gerne verstehen.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Na ich würde mal behaupten die Zuweisung einer neuen ID erzwingt einfach eine neue (oder geupdatete im Fall einer schon existenten ID) Zeile in der DB. Die alte ist von diesem Objekt dann nicht mehr erreichbar, und bleibt eben bestehen.
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@Judge: da ein Datensatz anhand seiner ID identifiziert wird, bedeutet eine andere ID ein anderer Datensatz. Das Verhalten sollte also nicht überraschend sein.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Eine Datenbank habt ihr aber alle schonmal benutzt, ja?

Code: Alles auswählen

postgres=# create table foo (id serial primary key, first_name text, last_name text);
CREATE TABLE
postgres=# insert into foo (first_name, last_name) values ('Vorname', 'Nachname');
INSERT 0 1
postgres=# update foo set id = 2 where first_name = 'Vorname';
UPDATE 1
postgres=# select * from foo;
 id | first_name | last_name 
----+------------+-----------
  2 | Vorname    | Nachname
(1 row)
Es ist jetzt vielleicht keine gute Idee Spalten die Teil eines primary keys sind zu verändern aber man kann Werte in solchen Spalten verändern wie in allen anderen Spalten auch.

Djangos Verhalten hier ist weder in sich konsistent noch konsistent mit dem Verhalten von Datenbanken. Wenn man darüberhinaus noch an "Special cases aren't special enough to break the rules." denkt, verhält sich Django hier schon durchaus überraschend.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ach, wir dummerles. So einfach geht es!

Doof nur, dass Django ja lazy speichert. Man muss sich also *beide* IDs merken, waehrend die Transaktion noch offen ist. Die Objektidentitaet ist waehrenddessen natuerlich auch trivial zu bestimmen. Und was ist, wenn die ID schon vergeben ist? In der DB? Oder bei einem Objekt im Speicher? Wenn letzteres, dann merken wir uns einfach in einer grossen Datenstruktur zentral *ALLE* Objekte, die wir jemals aus der DB geholt haben, und pruefen, ob da schon eins die neue ID hat! In der Datenbank reicht nicht, denn man kann ja die gleiche ID schon zugewiesen haben an ein anderes Objekt. Siehe oben. Und solange man noch nicht auf beiden Objekten die ID umgesetzt hat, sind die Objekte schon irgendwie anders zu unterscheiden. Hier Magie einfuegen. Und wenn wir dann einen rollback ausfuehren, oder partiell speichern, oder.... das ist alles voellig trivial.

Nicht.

ORMs leiden nunmal an an impdenance mismatch. Niemand hat behauptet, das Verhalten waere besonders geschickt, oder wuenschenswert. Ich persoenlich haette das setzten der ID verboten, aber es mag durchaus Grunde geben, die wir nicht kennen, die auch das schwieriger machen, als man das so salopp denkt.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Nachtrag: ein Grund, warum man die id setzen kann: sie kann semantik haben, und darum bei Anlage des Objekts frei gewaehlt werden. Natuerlich unter dem Risiko einer Kollision. Womit wir also schon bei einer Fallunterscheidung mehr sind - frisches Objekt, oder existierendes aus der DB geholt? Will man diese Komplikation im Code? Ist es das Wert, statt halt mit der "inkonsistenz" zu leben?
Benutzeravatar
Judge
User
Beiträge: 129
Registriert: Mittwoch 13. Juni 2012, 22:27
Wohnort: Ratingen
Kontaktdaten:

Naja, also das Argument mit dem "man kann ja die gleiche ID schon zugewiesen haben an ein anderes Objekt" leuchtet mir nicht 100% ein. Entscheident ist diesbezüglich ja der Moment in dem das ganze mit .save() gespeichert wird, nicht bei jedweder Zuweisung. Und es ist ja nicht so, das Django das speichern aus diesen Gründen verbietet; hierbei käme ja die Kollision zustande, sondern einen neuen Datensatz mit der - potentiell bereits existierenden - ID zusätzlich anlegt. Hierbei bleiben ja jedwede der genannten "Gefahren" bestehen, da das Speichern die eigentlich "gefährliche" Aktion ist, nicht das entfernen des vorigen Datensatzes bzw. das UPDATE statt INSERT.

Aber ich will auch garnicht das für und wider diskutieren oder Django irgendein Fehlverhalten unterstellen.
Jedoch finde ich dieses Verhalten auch nach Euren Erklärungen noch etwas merkwürdig. Technisch nachvollziehbar, ja, aber keineswegs in die Kategorie "das konnte doch so (und NUR so) erwartet werden" passend.

Egal - jetzt weiß ich es ja :-)
Ich habe also nichts falsch gemacht (außer einen unique primary key ändern zu wollen) und damit ist meine Frage beantwortet.

Ring frei für den Schlagabtausch der Experten ;-D
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Natürlich ist da jeder Moment der Zuweisung vorher entscheidend. Oder wie sonst vergleichst du denn zB Objekte? Und nicht vergessen: der vergleich kommt ggf implizit bei der Nutzung als Schlüssel in einem Wörterbuch oder einer Menge zum tragen.

Ein weiterer Punkt ist, das aus einer Aktion welche das anlegen oder updaten einer Zeile bedeutet plötzlich potentiell zwei werden.

Auch ich will diese Verhalten nicht beschönigen oder als logisch darstellen. Es ist ein Trade-off: spiel nicht an der ID. Wir (=Django-ORM)verbieten dir das nicht, aber du musst halt mit den Konsequenzen leben. Dafür haben wir nicht viel Code verbaut, der permanent alles mögliche teuer prüft, obwohl das randfälle wären. Du hast ja noch nicht mal einen echten, sondern das ist beim rumspielen passiert.

Und das es hierfür eine klare, völlig schlüssige Strategie gäbe, die Django (und wir, die wir vermeintlich ahnungslos von Datenbanken sind..) nur nicht umgesetzt haben, ist eben Unfug.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Code: Alles auswählen

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker


engine = create_engine('sqlite:///:memory:')


Base = declarative_base()


class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    first_name = Column(String)
    last_name = Column(String)

    def __repr__(self):
        return f'<User id={self.id} first_name={self.first_name!r} last_name={self.last_name!r}>'


Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()

user = User(first_name='Vorname', last_name='Nachname')
session.add(user)
print("session.new", session.new)
session.commit()
print("user.id", user.id)
del user

user = session.query(User).filter_by(first_name='Vorname').first()
user.id = 2
print("session.dirty", session.dirty)
session.commit()

print('user count', session.query(User).count())

Code: Alles auswählen

$ python foo.py 
session.new IdentitySet([<User id=None first_name='Vorname' last_name='Nachname'>])
user.id 1
session.dirty IdentitySet([<User id=2 first_name='Vorname' last_name='Nachname'>])
user count 1
Benutzeravatar
Judge
User
Beiträge: 129
Registriert: Mittwoch 13. Juni 2012, 22:27
Wohnort: Ratingen
Kontaktdaten:

Macht es einen Unterschied ob man dafür ein unique und/oder not null property definiert oder nicht?
Ansonsten: Ja, das habe ich so eigentlich auch von Django erwartet.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und die foreign key constraints updaten sich auch? Und wie wird Gleichheit gehandhabt?
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Wieso sollten sich foreign key constraints auf magische weise implizit updaten? Ich würde davon ausgehen dass Gleichheit auf Model Instanzen nicht definiert ist.

Ich erwarte von einem ORM dass es möglichst direkt Operationen auf Objekten in der Datenbank mit äquivalenten Operationen umsetzt. Wenn ich ein UPDATE in SQL mache, werden auch nicht magisch irgendwelche Spalten anderswo verändert. Ich möchte höhere Produktivität nicht dass jemand mir die Datenbank komplett wegabstrahiert was sowieso schief geht und sicherlich nicht performant wäre.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wieso sollte es denn NICHT die foreign keys updaten? Das Schema von SA hat alle dazu notwendigen Informationen. Es ist also auch eine Entwurfsentscheidung, das eben nicht zu machen.

Und alles, was ausser dem trivialen Fall von alleinstehendem Objekt mit neuer ID versehen, die auch nicht kollidieren darf, laeuft dann darauf hinaus, das man sich eine Funktion schreiben muss, die einem die ID im Objektgraphen umhaengt. Und bei Django macht man dann halt einmal loeschen und neu-anlegen des Kopf-Objekts. Big whoop.

Es ist einfach ein esoterischer use-case, und sich daran zu klammern, dass der in einem Fall ein mu weiter kommt als im anderen - wozu?

Auch ist deine Erwartung, das ein ORM moeglichst wenig tut, nicht notwendigerweise allgemeingueltig. ICH teile die sogar (nach einer kurzen, schmerzhaften Phase der reichen Model-Objekte in meinem Leben...). Aber viele wollen und werben mit einer moeglichst transparenten Abbildung von Objektgraphen auf SQL-Zeilen.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Ich möchte nicht dass ein ORM möglichst wenig tut, ich erwarte nur dass es eben nur Objekte zu Relationen und Relationen zu Objekten mappt. Alles darüberhinaus sollte nur mit explizitem opt-in erfolgen. Wenn ich ein Objekt habe dass einer Zeile in einer Tabelle entspricht und ein Attribut des Objektes verändere, entspricht dies eben einer Veränderung der korrespondieren Spalte, nicht aber Veränderungen auch noch an 25 anderen Zeilen.

Es gibt sicherlich Use Cases, in denen man darüberhinaus auch noch mehr haben möchte als ein Mapping zwischen Objekten und Relationen. Ich kenne auch durchaus Leute die tatsächlich gerne hätten dass die Datenbank zum Implementationsdetail wird, die sich gar nicht mit SQL beschäftigen möchten. Das ist dann aber halt kein ORM mehr, sondern eine (oder mehrere) Abstraktionsschichten höher. Ich denke dies hat auch zum dem NoSQL Hype geführt.

Bei Django möchte man irgendwie beides haben und einfach muss es irgendwie auch sein und dass geht halt nicht sonderlich gut.
Benutzeravatar
noisefloor
User
Beiträge: 3853
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

@Judge: nein, dass soll laut Doku _nicht_ gehen, siehe https://docs.djangoproject.com/en/2.1/r ... rimary-key Django verbietet das Ändern der PK, der ist read-only und es wird automatisch ein neues Objekt angelegt. Ob das "clever" ist oder nicht ist Geschmackssache, aber es ist immerhin so dokumentiert. It's not a bug, it's a feature! ;-)

`unique=True, primary_key=True` ist auch nicht sinnvoll - ein PK muss einmalig sein, sonst wäre es kein PK. Steht auch ein gleicher Stelle in der Doku, dass `primary_key=True` `unique=True` impliziert.

Gruß, noisefloor
Antworten