J'ai un enregistrement que je veux exister dans la base de données s'il n'est pas là, et s'il y est déjà (la clé primaire existe), je veux que les champs soient mis à jour à l'état actuel. Ceci est souvent appelé un upsert .
L'extrait de code incomplet suivant montre ce qui fonctionnera, mais il semble excessivement maladroit (surtout s'il y avait beaucoup plus de colonnes). Quel est le meilleur/meilleur moyen?
Base = declarative_base()
class Template(Base):
__table= '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()
Y a-t-il une manière 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 soit évidemment inutile (l'ORM devrait pouvoir le comprendre facilement), je l'ai ajouté simplement parce que SQLAlchemy tend à ne fonctionner qu'avec la clé primaire. 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.
Voici un exemple de cas d'utilisation lors du démarrage d'une application serveur susceptible d'avoir mis à niveau les données attendues par défaut. c'est-à-dire: pas de problèmes de concurrence pour cet upsert.
SQLAlchemy a un comportement "enregistrer-ou-mettre à jour" qui, dans les versions récentes, a été intégré à session.add
, mais était auparavant l'appel distinct session.saveorupdate
. Ce n'est pas un "upsert" mais cela peut suffire à vos besoins.
Il est bon que vous vous interrogiez sur une classe avec plusieurs clés uniques; Je pense que c’est précisément la raison pour laquelle il n’existe pas de bonne façon de procéder. La clé primaire est également une clé unique. S'il n'y avait pas de contrainte unique, mais uniquement la clé primaire, le problème serait assez simple: s'il n'existe rien avec l'ID donné ou si l'ID est Aucun, créez un nouvel enregistrement; sinon, mettre à jour tous les autres champs de l'enregistrement existant avec cette clé primaire.
Cependant, lorsqu'il existe des contraintes uniques supplémentaires, cette approche simple pose des problèmes logiques. Si vous voulez "insérer" un objet et que la clé primaire de votre objet correspond à un enregistrement existant, mais qu'une autre colonne unique correspond à un enregistrement different, que faites-vous? De même, si la clé primaire ne correspond à aucun enregistrement existant, mais à une autre colonne unique est-ce que correspond à un enregistrement existant, alors quoi? Il peut y avoir une réponse correcte pour 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.
SQLAlchemy supporte maintenant ON CONFLICT
avec deux méthodes on_conflict_do_update()
et on_conflict_do_nothing()
:
Copier 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)
J'utilise une approche "regarde 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 existe un potentiel condition de concurrence dans un scénario tel que celui-ci:
switch_command
et n'en trouvons passwitch_command
switch_command
avec la même clé primaire que la nôtreswitch_command
De nos jours, SQLAlchemy fournit deux fonctions utiles on_conflict_do_nothing
et on_conflict_do_update
. Ces fonctions sont utiles, mais vous devez passer de l'interface ORM à l'interface de niveau inférieur - SQLAlchemy Core .
Bien que ces deux fonctions ne rendent pas très difficile l’utilisation de la syntaxe SQLAlchemy, ces fonctions sont loin de fournir une solution prête à l’emploi complète pour la conversion.
Mon cas d'utilisation courant consiste à transférer un grand nombre de lignes dans une seule requête/exécution de session SQL. Habituellement, je rencontre deux problèmes avec la rotation:
Par exemple, il manque des fonctionnalités ORM de niveau supérieur auxquelles nous sommes habitués. Vous ne pouvez pas utiliser d'objets ORM mais devez plutôt fournir ForeignKey
s au moment de l'insertion.
J'utilise this la fonction suivante que j'ai écrite pour traiter ces deux problèmes:
def upsert(session, model, rows):
table = model.__table__
stmt = postgresql.insert(table)
primary_keys = [key.name for key in inspect(table).primary_key]
update_dict = {c.name: c for c in stmt.excluded if not c.primary_key}
if not update_dict:
raise ValueError("insert_or_update resulted in an empty update_dict")
stmt = stmt.on_conflict_do_update(index_elements=primary_keys,
set_=update_dict)
seen = set()
foreign_keys = {col.name: list(col.foreign_keys)[0].column for col in table.columns if col.foreign_keys}
unique_constraints = [c for c in table.constraints if isinstance(c, UniqueConstraint)]
def handle_foreignkeys_constraints(row):
for c_name, c_value in foreign_keys.items():
foreign_obj = row.pop(c_value.table.name, None)
row[c_name] = getattr(foreign_obj, c_value.name) if foreign_obj else None
for const in unique_constraints:
unique = Tuple([const,] + [row[col.name] for col in const.columns])
if unique in seen:
return None
seen.add(unique)
return row
rows = list(filter(None, (handle_foreignkeys_constraints(row) for row in rows)))
session.execute(stmt, rows)
Cela fonctionne pour moi avec sqlite3 et postgres. Bien qu'il puisse échouer avec des contraintes de clé primaire combinées et échouera probablement avec des contraintes uniques supplémentaires.
try:
t = self._meta.tables[data['table']]
except KeyError:
self._log.error('table "%s" unknown', data['table'])
return
try:
q = insert(t, values=data['values'])
self._log.debug(q)
self._db.execute(q)
except IntegrityError:
self._log.warning('integrity error')
where_clause = [c.__eq__(data['values'][c.name]) for c in t.c if c.primary_key]
update_dict = {c.name: data['values'][c.name] for c in t.c if not c.primary_key}
q = update(t, values=update_dict).where(*where_clause)
self._log.debug(q)
self._db.execute(q)
except Exception as e:
self._log.error('%s: %s', t.name, e)