2011-08-23 4 views
35

J'ai un enregistrement que je veux exister dans la base de données si ce n'est pas là, et si c'est déjà là (clé primaire existe) je veux que les champs soient mis à jour à l'état actuel. Ceci est souvent appelé upsert. L'extrait de code incomplet suivant montre ce qui va fonctionner, mais il semble excessivement maladroit (surtout s'il y avait beaucoup plus de colonnes). Quel est le meilleur/meilleur moyen?Comment faire un upsert avec SqlAlchemy?

Base = declarative_base() 
class Template(Base): 
    __tablename__ = 'templates' 
    id = Column(Integer, primary_key = True) 
    name = Column(String(80), unique = True, index = True) 
    template = Column(String(80), unique = True) 
    description = Column(String(200)) 
    def __init__(self, Name, Template, Desc): 
     self.name = Name 
     self.template = Template 
     self.description = Desc 

def UpsertDefaultTemplate(): 
    sess = Session() 
    desired_default = Template("default", "AABBCC", "This is the default template") 
    try: 
     q = sess.query(Template).filter_by(name = desiredDefault.name) 
     existing_default = q.one() 
    except sqlalchemy.orm.exc.NoResultFound: 
     #default does not exist yet, so add it... 
     sess.add(desired_default) 
    else: 
     #default already exists. Make sure the values are what we want... 
     assert isinstance(existing_default, Template) 
     existing_default.name = desired_default.name 
     existing_default.template = desired_default.template 
     existing_default.description = desired_default.description 
    sess.flush() 

Existe-t-il une façon meilleure ou moins verbeuse de le faire? Quelque chose comme ça serait génial:

sess.upsert_this(desired_default, unique_key = "name") 

bien que le unique_key kwarg est évidemment inutile (l'ORM devrait être en mesure de comprendre facilement ceci) je l'ai ajouté juste parce que SQLAlchemy a tendance à ne travailler avec la clé primaire. Par exemple: J'ai cherché à savoir si Session.merge serait applicable, mais cela ne fonctionne que sur la clé primaire, qui dans ce cas est un identifiant auto-incrémenté qui n'est pas très utile à cette fin.

Un exemple d'utilisation pour cela est simplement lors du démarrage d'une application serveur qui peut avoir mis à niveau ses données attendues par défaut. c'est-à-dire: pas de problèmes de simultanéité pour cet upsert.

+1

Pourquoi ne pouvez-vous faire le champ 'name' une clé primaire si elle est unique (et fusionner fonctionnerait dans ce cas). Pourquoi avez-vous besoin d'une clé primaire séparée? – abbot

+4

@abbot: Je ne veux pas entrer dans un débat sur le terrain, mais ... la réponse courte est "clés étrangères". Plus long est que même si le nom est en effet la seule clé unique requise, il y a deux problèmes. 1) quand un enregistrement de modèle est référencé par 50 millions d'enregistrements dans une autre table ayant ce FK comme champ de chaîne est fou. Un nombre entier indexé est meilleur, d'où la colonne id apparemment inutile. et 2) s'étendant sur cela, si la chaîne * was * utilisée comme FK, il y a maintenant deux endroits pour mettre à jour le nom si/quand il change, ce qui est ennuyeux et regorge de problèmes de relation morts. L'identifiant * jamais * change. – Russ

+0

vous pouvez essayer une nouvelle (beta) [bibliothèque upsert pour python] (https://github.com/seamusabshere/py-upsert) ... c'est compatible avec psycopg2, sqlite3, MySQLdb –

Répondre

31

SQLAlchemy a un comportement de «sauvegarde ou mise à jour» qui, dans les versions récentes, a été intégré dans session.add, mais était auparavant l'appel session.saveorupdate distinct. Ce n'est pas un "upsert", mais il peut être assez bon pour vos besoins.

Il est bon que vous posiez des questions sur une classe avec plusieurs clés uniques; Je crois que c'est précisément la raison pour laquelle il n'y a pas une seule façon correcte de le faire. La clé primaire est également une clé unique. S'il n'y avait pas de contraintes uniques, seulement la clé primaire, il s'agirait d'un problème assez simple: s'il n'y a rien avec l'ID donné, ou si ID est None, créez un nouvel enregistrement; Sinon, mettez à jour tous les autres champs de l'enregistrement existant avec cette clé primaire. Toutefois, lorsqu'il existe des contraintes uniques supplémentaires, il existe des problèmes logiques avec cette approche simple. Si vous voulez "upert" un objet, et la clé primaire de votre objet correspond à un enregistrement existant, mais une autre colonne unique correspond à un différent enregistrement, alors que faites-vous? De même, si la clé primaire ne correspond à aucun enregistrement existant, mais qu'une autre colonne unique correspond à un enregistrement existant, alors quoi? Il peut y avoir une réponse correcte à votre situation particulière, mais en général, je dirais qu'il n'y a pas de réponse correcte unique.

Ce serait la raison pour laquelle il n'y a pas d'opération "upsert" intégrée. L'application doit définir ce que cela signifie dans chaque cas particulier.

6

SQLAlchemy soutient ON CONFLICT maintenant avec deux méthodes on_conflict_do_update() et on_conflict_do_nothing():

copie de la documentation:

from sqlalchemy.dialects.postgresql import insert 

stmt = insert(my_table).values(user_email='[email protected]', data='inserted data') 
stmt = stmt.on_conflict_do_update(
    index_elements=[my_table.c.user_email], 
    index_where=my_table.c.user_email.like('%@gmail.com'), 
    set_=dict(data=stmt.excluded.data) 
    ) 
conn.execute(stmt) 

http://docs.sqlalchemy.org/en/latest/dialects/postgresql.html?highlight=conflict#insert-on-conflict-upsert

1

J'utilise une approche "regarder avant de sauter":

# first get the object from the database if it exists 
# we're guaranteed to only get one or zero results 
# because we're filtering by primary key 
switch_command = session.query(Switch_Command).\ 
    filter(Switch_Command.switch_id == switch.id).\ 
    filter(Switch_Command.command_id == command.id).first() 

# If we didn't get anything, make one 
if not switch_command: 
    switch_command = Switch_Command(switch_id=switch.id, command_id=command.id) 

# update the stuff we care about 
switch_command.output = 'Hooray!' 
switch_command.lastseen = datetime.datetime.utcnow() 

session.add(switch_command) 
# This will generate either an INSERT or UPDATE 
# depending on whether we have a new object or not 
session.commit() 

L'avantage est que c'est db-neutre et je pense que c'est clair à lire.L'inconvénient est qu'il ya une condition de course potentielle dans un scénario comme ce qui suit:

  • nous interrogeons le db pour un switch_command et ne trouve pas un
  • nous créons un switch_command
  • un autre processus ou thread CRÉE un switch_command avec la même clé primaire que la nôtre
  • nous essayons de commettre notre switch_command
+0

[Cette question] (https : //stackoverflow.com/questions/14520340/sqlalchemy-and-explicit-locking) gère l'état de la course avec un essai/catch – Ben