Atualização eficiente do banco de dados usando SQLAlchemy ORM


116

Estou iniciando um novo aplicativo e procurando usar um ORM - em particular, SQLAlchemy.

Digamos que tenho uma coluna 'foo' em meu banco de dados e desejo incrementá-la. No sqlite direto, isso é fácil:

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

Eu descobri o equivalente do 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)

Isso é um pouco mais lento, mas não há muito nele.

Aqui está meu melhor palpite para uma abordagem 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()

Isso faz a coisa certa, mas leva pouco menos de cinquenta vezes mais do que as outras duas se aproximam. Presumo que seja porque ele tem que trazer todos os dados para a memória antes de poder trabalhar com eles.

Existe alguma maneira de gerar o SQL eficiente usando ORM do SQLAlchemy? Ou usando qualquer outro python ORM? Ou devo simplesmente voltar a escrever o SQL manualmente?


1
Ok, estou assumindo que a resposta é "isso não é algo que os ORMs fazem bem". Ah bem; Eu vivo e aprendo.
John Fouhy

Houve alguns experimentos executados em ORMs diferentes e como eles funcionam sob carga e coação. Não tenho um link à mão, mas vale a pena ler.
Matthew Schinckel

Outro problema que existe com o último exemplo (ORM) é que ele não é atômico .
Marian

Respostas:


181

O ORM do SQLAlchemy deve ser usado junto com a camada SQL, e não ocultá-la. Mas você deve manter uma ou duas coisas em mente ao usar o ORM e o SQL simples na mesma transação. Basicamente, de um lado, as modificações de dados ORM só atingirão o banco de dados quando você liberar as alterações de sua sessão. Por outro lado, as instruções de manipulação de dados SQL não afetam os objetos que estão em sua sessão.

Então se você diz

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

ele fará o que diz, irá buscar todos os objetos do banco de dados, modificará todos os objetos e então quando for a hora de descarregar as alterações no banco de dados, atualizará as linhas uma por uma.

Em vez disso, você deve fazer isso:

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

Isso será executado como uma consulta como você esperaria, e porque pelo menos a configuração de sessão padrão expira todos os dados na sessão no commit, você não tem nenhum problema de dados obsoletos.

Na série 0.5 quase lançada, você também pode usar este método para atualização:

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

Isso basicamente executará a mesma instrução SQL do fragmento anterior, mas também selecionará as linhas alteradas e expirará todos os dados obsoletos na sessão. Se você sabe que não está usando nenhum dado de sessão após a atualização, você também pode adicionar synchronize_session=Falseà instrução de atualização e se livrar dessa seleção.


2
na terceira forma, ele irá disparar o evento orm (como after_update)?
Ken,

@Ken, não, não vai. Consulte a documentação da API para Query.update docs.sqlalchemy.org/en/13/orm/… . Em vez disso, você tem um 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()

Tente isto =)


Este método funcionou para mim. Mas o problema é que é lento. Ele precisa de um bom tempo para alguns registros de dados de 100k. Existe talvez um método mais rápido?
baermathias

Muito obrigado, essa abordagem funcionou para mim. É muito ruim que o sqlachemy não tenha uma maneira mais curta de atualizar a jsoncoluna
Jai Prakash

6
Para aqueles que ainda têm problemas de desempenho ao usar este método: por padrão, isso pode fazer um SELECT para cada registro primeiro e apenas UPDATE depois. Passar synchronize_session = False para o método update () evita que isso aconteça, mas certifique-se de fazer isso apenas se você não usar os objetos que você atualizar novamente antes de commit ().
teuneboon

25

Existem várias maneiras de ATUALIZAR 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)

6

Aqui está um exemplo de como resolver o mesmo problema sem ter que mapear os 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()

Portanto, para atualizar uma instância de mídia, você pode fazer algo assim:

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

1

Sem muitos testes, eu tentaria:

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

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

Eu descobri que às vezes fazer uma grande consulta e, em seguida, iterar em python pode ser até 2 ordens de magnitude mais rápido do que muitas consultas. Presumo que iterar sobre o objeto de consulta é menos eficiente do que iterar sobre uma lista gerada pelo método all () do objeto de consulta.

[Observe o comentário abaixo - isso não agilizou as coisas].


2
Adicionar .all () e remover .flush () não alterou a hora.
John Fouhy

1

Se for por causa da sobrecarga em termos de criação de objetos, então provavelmente não pode ser acelerado com o SA.

Se for porque ele está carregando objetos relacionados, talvez você consiga fazer algo com o carregamento lento. Existem muitos objetos sendo criados devido a referências? (Ou seja, obter um objeto Empresa também obtém todos os objetos Pessoas relacionados).


Nah, a mesa está sozinha. Eu nunca usei um ORM antes - isso é apenas algo em que eles são ruins?
John Fouhy

1
Há uma sobrecarga devido à criação de objetos, mas na minha opinião vale a pena - ser capaz de armazenar objetos de forma persistente em um banco de dados é incrível.
Matthew Schinckel de
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.