Mise à jour efficace de la base de données à l'aide de SQLAlchemy ORM

116

Je démarre une nouvelle application et cherche à utiliser un ORM - en particulier, SQLAlchemy.

Disons que j'ai une colonne «foo» dans ma base de données et que je souhaite l'incrémenter. En simple sqlite, c'est facile:

db = sqlite3.connect('mydata.sqlitedb')
cur = db.cursor()
cur.execute('update table stuff set foo = foo + 1')

J'ai trouvé l'équivalent SQLAlchemy SQL-builder:

engine = sqlalchemy.create_engine('sqlite:///mydata.sqlitedb')
md = sqlalchemy.MetaData(engine)
table = sqlalchemy.Table('stuff', md, autoload=True)
upd = table.update(values={table.c.foo:table.c.foo+1})
engine.execute(upd)

C'est légèrement plus lent, mais il n'y a pas grand-chose dedans.

Voici ma meilleure estimation pour une approche SQLAlchemy ORM:

# snip definition of Stuff class made using declarative_base
# snip creation of session object
for c in session.query(Stuff):
    c.foo = c.foo + 1
session.flush()
session.commit()

Cela fait la bonne chose, mais cela prend un peu moins de cinquante fois plus longtemps que les deux autres approches. Je suppose que c'est parce qu'il doit mettre toutes les données en mémoire avant de pouvoir fonctionner avec.

Existe-t-il un moyen de générer le SQL efficace en utilisant l'ORM de SQLAlchemy? Ou en utilisant un autre ORM python? Ou devrais-je simplement revenir à l'écriture manuelle du SQL?

John Fouhy
la source
1
Ok, je suppose que la réponse est "ce n'est pas quelque chose que les ORM font bien". Tant pis; Je vis et j'apprends.
John Fouhy
Il y a eu des expériences menées sur différents ORM et comment ils fonctionnent sous la charge et la contrainte. N'ayez pas de lien à portée de main, mais cela vaut la peine d'être lu.
Matthew Schinckel
Un autre problème qui existe avec le dernier exemple (ORM) est qu'il n'est pas atomique .
Marian

Réponses:

181

L'ORM de SQLAlchemy est destiné à être utilisé avec la couche SQL, pas à la cacher. Mais vous devez garder une ou deux choses à l'esprit lorsque vous utilisez l'ORM et le SQL brut dans la même transaction. Fondamentalement, d'un côté, les modifications de données ORM n'atteindront la base de données que lorsque vous supprimerez les modifications de votre session. De l'autre côté, les instructions de manipulation de données SQL n'affectent pas les objets qui se trouvent dans votre session.

Donc si tu dis

for c in session.query(Stuff).all():
    c.foo = c.foo+1
session.commit()

il fera ce qu'il dit, va chercher tous les objets de la base de données, modifie tous les objets et puis quand il est temps de vider les modifications de la base de données, met à jour les lignes une par une.

Au lieu de cela, vous devriez faire ceci:

session.execute(update(stuff_table, values={stuff_table.c.foo: stuff_table.c.foo + 1}))
session.commit()

Cela s'exécutera comme une requête comme vous vous en doutez, et comme au moins la configuration de session par défaut expire toutes les données de la session lors de la validation, vous n'avez aucun problème de données périmées.

Dans la série 0.5 presque sortie, vous pouvez également utiliser cette méthode pour la mise à jour:

session.query(Stuff).update({Stuff.foo: Stuff.foo + 1})
session.commit()

Cela exécutera essentiellement la même instruction SQL que l'extrait de code précédent, mais sélectionnera également les lignes modifiées et expirera toutes les données périmées de la session. Si vous savez que vous n'utilisez aucune donnée de session après la mise à jour, vous pouvez également ajouter synchronize_session=Falseà l'instruction de mise à jour et supprimer cette sélection.

Fourmis Aasma
la source
2
de la troisième manière, déclenchera-t-il un événement orm (comme after_update)?
Ken
@Ken, non, ce ne sera pas le cas. Consultez la documentation de l'API pour Query.update docs.sqlalchemy.org/en/13/orm/… . Au lieu de cela, vous avez un événement pour after_bulk_update docs.sqlalchemy.org/en/13/orm
...
91
session.query(Clients).filter(Clients.id == client_id_list).update({'status': status})
session.commit()

Essayez ceci =)

Vin
la source
Cette méthode a fonctionné pour moi. Mais le problème est sa lenteur. Il faut un bon morceau de temps pour quelques enregistrements de données de 100 000. Existe-t-il peut-être une méthode plus rapide?
baermathias
Merci beaucoup cette approche a fonctionné pour moi. C'est vraiment dommage que sqlachemy n'ait pas de moyen plus court de mettre à jour la jsoncolonne
Jai Prakash
6
Pour ceux qui ont encore des problèmes de performances lors de l'utilisation de cette méthode: par défaut, cela peut faire un SELECT pour chaque enregistrement en premier, et seulement UPDATE par la suite. Passer synchronize_session = False à la méthode update () empêche que cela se produise, mais assurez-vous de ne le faire que si vous n'utilisez pas à nouveau les objets que vous mettez à jour avant la validation ().
teuneboon le
25

Il existe plusieurs façons de METTRE À JOUR à l'aide de sqlalchemy

1) for c in session.query(Stuff).all():
       c.foo += 1
   session.commit()

2) session.query().\
       update({"foo": (Stuff.foo + 1)})
   session.commit()

3) conn = engine.connect()
   stmt = Stuff.update().\
       values(Stuff.foo = (Stuff.foo + 1))
   conn.execute(stmt)
Nima Soroush
la source
6

Voici un exemple de résolution du même problème sans avoir à mapper les champs manuellement:

from sqlalchemy import Column, ForeignKey, Integer, String, Date, DateTime, text, create_engine
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.attributes import InstrumentedAttribute

engine = create_engine('postgres://postgres@localhost:5432/database')
session = sessionmaker()
session.configure(bind=engine)

Base = declarative_base()


class Media(Base):
  __tablename__ = 'media'
  id = Column(Integer, primary_key=True)
  title = Column(String, nullable=False)
  slug = Column(String, nullable=False)
  type = Column(String, nullable=False)

  def update(self):
    s = session()
    mapped_values = {}
    for item in Media.__dict__.iteritems():
      field_name = item[0]
      field_type = item[1]
      is_column = isinstance(field_type, InstrumentedAttribute)
      if is_column:
        mapped_values[field_name] = getattr(self, field_name)

    s.query(Media).filter(Media.id == self.id).update(mapped_values)
    s.commit()

Donc, pour mettre à jour une instance Media, vous pouvez faire quelque chose comme ceci:

media = Media(id=123, title="Titular Line", slug="titular-line", type="movie")
media.update()
laboureur
la source
1

Sans tests, j'essaierais:

for c in session.query(Stuff).all():
     c.foo = c.foo+1
session.commit()

(IIRC, commit () fonctionne sans flush ()).

J'ai constaté que parfois, faire une grande requête puis itérer en python peut être jusqu'à 2 ordres de grandeur plus rapide que de nombreuses requêtes. Je suppose que l'itération sur l'objet de requête est moins efficace que l'itération sur une liste générée par la méthode all () de l'objet de requête.

[Veuillez noter le commentaire ci-dessous - cela n'a pas du tout accéléré les choses].

Matthew Schinckel
la source
2
L'ajout de .all () et la suppression de .flush () n'ont pas du tout changé l'heure.
John Fouhy
1

Si c'est à cause de la surcharge en termes de création d'objets, alors il ne peut probablement pas être accéléré du tout avec SA.

Si c'est parce qu'il charge des objets associés, vous pourrez peut-être faire quelque chose avec le chargement différé. Y a-t-il beaucoup d'objets créés en raison de références? (IE, l'obtention d'un objet Company obtient également tous les objets People associés).

Matthew Schinckel
la source
Non, la table est toute seule. Je n'ai jamais utilisé d'ORM auparavant - est-ce juste quelque chose pour lequel ils sont mauvais?
John Fouhy
1
Il y a une surcharge due à la création d'objets, mais à mon avis, cela en vaut la peine - pouvoir stocker de manière persistante des objets dans une base de données est génial.
Matthew Schinckel