SQLAlchemy ORMを使用してデータベースを効率的に更新する


116

新しいアプリケーションを開始し、ORM、特にSQLAlchemyの使用を検討しています。

データベースに「foo」という列があり、それを増やしたいとしましょう。ストレートSQLiteでは、これは簡単です。

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

SQLAlchemy SQLビルダーに相当するものを見つけました。

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)

これは少し遅いですが、それほど多くはありません。

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

これは正しいことですが、他の2つのアプローチを実行する場合と比べて50倍弱かかります。それはそれが機能する前にすべてのデータをメモリに読み込まなければならないためだと思います。

SQLAlchemyのORMを使用して効率的なSQLを生成する方法はありますか?または他のPython ORMを使用していますか?それとも、SQLを手作業で書くことに戻るべきでしょうか?


1
わかりました、「これはORMがうまくやるものではない」という答えを想定しています。しかたがない; 私は生きて学びます。
John Fouhy

いくつかの実験がさまざまなORMで実行されており、それらが負荷と強要の下でどのように機能するかが示されています。便利なリンクはありませんが、読む価値はあります。
Matthew Schinckel、2008年

最後の(ORM)例に存在する別の問題は、アトミックではないことです。
マリアン

回答:


181

SQLAlchemyのORMは、SQLレイヤーと共に使用するためのものであり、非表示にするものではありません。ただし、同じトランザクションでORMとプレーンSQLを使用する場合は、1つまたは2つのことに注意する必要があります。基本的に、一方の側から、ORMデータの変更は、セッションから変更をフラッシュするときにのみデータベースにヒットします。一方、SQLデータ操作ステートメントは、セッション内のオブジェクトには影響しません。

だからあなたが言うなら

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

つまり、データベースからすべてのオブジェクトをフェッチし、すべてのオブジェクトを変更してから、データベースへの変更をフラッシュするときに、行を1つずつ更新します。

代わりに、これを行う必要があります。

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

これは、予想どおり1つのクエリとして実行されます。少なくともデフォルトのセッション構成では、コミット時にセッション内のすべてのデータが期限切れになるため、古いデータの問題はありません。

ほぼリリースされた0.5シリーズでは、このメソッドを使用して更新することもできます。

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

これは基本的に前のスニペットと同じSQLステートメントを実行しますが、変更された行を選択し、セッション内の古いデータを期限切れにします。更新後にセッションデータを使用していないことがわかっている場合はsynchronize_session=False、更新ステートメントに追加して、その選択を解除することもできます。


2
3番目の方法では、それは(after_updateのような)ormイベントをトリガーしますか?
Ken

@ケン、いいえ、それはしません。Query.updateのためのAPIドキュメントを参照してくださいdocs.sqlalchemy.org/en/13/orm/...を。代わりに、after_bulk_updatedocs.sqlalchemy.org /en
13

91
session.query(Clients).filter(Clients.id == client_id_list).update({'status': status})
session.commit()

これを試してください=)


この方法でうまくいきました。しかし、問題はその遅いことです。数10万のデータレコードに十分な時間が必要です。おそらくもっと速い方法はありますか?
baermathias 2016

おかげで、このアプローチはうまくいきました。sqlachemyでjson列を更新する短い方法がないのは本当に悪いことです
Jai Prakash

6
この方法を使用してもパフォーマンスの問題が解決しない場合:デフォルトでは、最初にすべてのレコードに対してSELECTを実行し、その後はUPDATEのみを実行します。synchronize_session = Falseをupdate()メソッドに渡すと、これが発生するのを防ぐことができますが、commit()の前に再度更新するオブジェクトを使用しない場合にのみこれを実行してください。
teuneboon

25

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

以下は、フィールドを手動でマップせずに同じ問題を解決する方法の例です。

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

したがって、Mediaインスタンスを更新するには、次のようにします。

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

1

十分なテストをして、私は試してみます:

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

(IIRC、commit()はflush()なしで機能します)。

大規模なクエリを実行してからpythonで反復すると、多くのクエリよりも最大2桁速くなる場合があることを発見しました。クエリオブジェクトの繰り返し処理は、クエリオブジェクトのall()メソッドによって生成されたリストの繰り返し処理よりも効率が悪いと思います。

[下のコメントに注意してください-これはまったくスピードアップしませんでした]。


2
.all()を追加して.flush()を削除しても、時間はまったく変わりませんでした。
John Fouhy

1

オブジェクトの作成に関するオーバーヘッドが原因である場合は、SAを使用して高速化することはおそらくできません。

関連オブジェクトをロードしていることが原因である場合は、遅延ロードで何かを実行できる可能性があります。参照のために多くのオブジェクトが作成されていますか?(つまり、Companyオブジェクトを取得すると、関連するPeopleオブジェクトもすべて取得されます)。


いや、テーブルはそれだけですべてです。私はこれまでORMを使用したことがありません。これは、彼らが得意とするものなのですか?
John Fouhy

1
オブジェクトの作成によるオーバーヘッドがありますが、私の意見では、ペナルティの価値はあります。データベースにオブジェクトを永続的に格納できることは素晴らしいことです。
Matthew Schinckel、2008年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.