Actualización eficiente de la base de datos utilizando SQLAlchemy ORM

116

Estoy iniciando una nueva aplicación y estoy pensando en usar un ORM, en particular, SQLAlchemy.

Digamos que tengo una columna 'foo' en mi base de datos y quiero incrementarla. En sqlite simple, esto es fácil:

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

Descubrí el equivalente del constructor SQL de SQLAlchemy:

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)

Esto es un poco más lento, pero no tiene mucho contenido.

Aquí está mi mejor suposición para un enfoque ORM de SQLAlchemy:

# 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()

Esto hace lo correcto, pero tarda menos de cincuenta veces más que los otros dos enfoques. Supongo que es porque tiene que traer todos los datos a la memoria antes de que pueda trabajar con ellos.

¿Hay alguna forma de generar el SQL eficiente usando el ORM de SQLAlchemy? ¿O usando cualquier otro ORM de Python? ¿O debería volver a escribir el SQL a mano?

John Fouhy
fuente
1
Ok, supongo que la respuesta es "esto no es algo que los ORM hagan bien". Oh bien; Vivo y aprendo.
John Fouhy
Se han realizado algunos experimentos en diferentes ORM y cómo funcionan bajo carga y coacción. No tienes un enlace a la mano, pero vale la pena leerlo.
Matthew Schinckel
Otro problema que existe con el último ejemplo (ORM) es que no es atómico .
Marian

Respuestas:

181

El ORM de SQLAlchemy está diseñado para usarse junto con la capa SQL, no para ocultarlo. Pero debe tener en cuenta una o dos cosas al usar el ORM y SQL simple en la misma transacción. Básicamente, por un lado, las modificaciones de los datos de ORM solo afectarán a la base de datos cuando elimine los cambios de su sesión. Por otro lado, las declaraciones de manipulación de datos SQL no afectan los objetos que están en su sesión.

Entonces si dices

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

hará lo que dice, irá a buscar todos los objetos de la base de datos, modificará todos los objetos y luego, cuando sea el momento de eliminar los cambios en la base de datos, actualizará las filas una por una.

En su lugar, debería hacer esto:

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

Esto se ejecutará como una consulta como es de esperar, y debido a que al menos la configuración de sesión predeterminada expira todos los datos de la sesión en la confirmación, no tiene problemas de datos obsoletos.

En la serie 0.5 casi lanzada, también puede usar este método para actualizar:

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

Eso básicamente ejecutará la misma declaración SQL que el fragmento anterior, pero también seleccionará las filas modificadas y expirará cualquier dato obsoleto en la sesión. Si sabe que no está usando ningún dato de sesión después de la actualización, también puede agregarlo synchronize_session=Falsea la declaración de actualización y deshacerse de esa selección.

Hormigas Aasma
fuente
2
en la tercera forma, ¿activará el evento orm (como after_update)?
Ken
@Ken, no, no lo hará. Consulte el documento de la API para Query.update docs.sqlalchemy.org/en/13/orm/… . En su lugar, tiene un evento para after_bulk_update docs.sqlalchemy.org/en/13/orm/…
TrilceAC
91
session.query(Clients).filter(Clients.id == client_id_list).update({'status': status})
session.commit()

Prueba esto =)

Vin
fuente
Este método funcionó para mi. Pero el problema es que es lento. Se necesita un buen tiempo para unos 100.000 registros de datos. ¿Existe quizás un método más rápido?
baermathias
Muchas gracias, este enfoque funcionó para mí. Es realmente malo que sqlachemy no tenga una forma más corta de actualizar la jsoncolumna
Jai Prakash
6
Para aquellos que aún tienen problemas de rendimiento al usar este método: de forma predeterminada, esto podría hacer una SELECCIÓN para cada registro primero y solo una ACTUALIZACIÓN después. Pasar synchronize_session = False al método update () evita que esto suceda, pero asegúrese de hacerlo solo si no usa los objetos que actualiza nuevamente antes de la confirmación ().
teuneboon
25

Hay varias formas de ACTUALIZAR usando 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
fuente
6

A continuación, se muestra un ejemplo de cómo resolver el mismo problema sin tener que mapear los campos manualmente:

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()

Entonces, para actualizar una instancia de Media, puede hacer algo como esto:

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

Sin pruebas suficientes, intentaría:

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

(IIRC, commit () funciona sin flush ()).

Descubrí que, a veces, hacer una consulta grande y luego iterar en Python puede ser hasta 2 órdenes de magnitud más rápido que muchas consultas. Supongo que iterar sobre el objeto de consulta es menos eficiente que iterar sobre una lista generada por el método all () del objeto de consulta.

[Tenga en cuenta el comentario a continuación: esto no aceleró las cosas en absoluto].

Matthew Schinckel
fuente
2
Agregar .all () y eliminar .flush () no cambió la hora en absoluto.
John Fouhy
1

Si se debe a la sobrecarga en términos de creación de objetos, entonces probablemente no se pueda acelerar en absoluto con SA.

Si es porque está cargando objetos relacionados, es posible que pueda hacer algo con la carga diferida. ¿Se están creando muchos objetos debido a las referencias? (Es decir, obtener un objeto Company también obtiene todos los objetos People relacionados).

Matthew Schinckel
fuente
No, la mesa está sola. Nunca antes había usado un ORM, ¿es esto algo en lo que son malos?
John Fouhy
1
Hay una sobrecarga debido a la creación de objetos, pero en mi opinión, vale la pena la pena: poder almacenar objetos de manera persistente en una base de datos es increíble.
Matthew Schinckel