web-dev-qa-db-fra.com

Comment faire un upsert avec SqlAlchemy?

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.

43
Russ

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.

41
wberry

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)

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

14
P.R.

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:

  • nous interrogeons la base de données pour obtenir un switch_command et n'en trouvons pas
  • 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
7
Ben

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 ForeignKeys 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)
2
NirIzr

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)
1
ThePsyjo