Ефективне оновлення бази даних за допомогою 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-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)

Це трохи повільніше, але в ньому мало що.

Ось моя найкраща здогадка щодо підходу 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()

Це робиться правильно, але це забирає трохи менше п'ятдесяти разів, ніж інші два підходи. Я припускаю, що це тому, що він повинен принести всі дані в пам'ять, перш ніж він може працювати з ними.

Чи є спосіб створити ефективний SQL за допомогою ORM SQLAlchemy? Або використовуєте будь-який інший ORM python? Або мені просто повернутися до написання SQL вручну?


1
Гаразд, я припускаю, що відповідь - це не те, що ORM добре справляються. Що ж, добре; Я живу і вчуся.
Джон Фугі

Було проведено кілька експериментів на різних ОРМ, і те, як вони працюють під навантаженням і примусом. Не майте зручного посилання, але його варто прочитати.
Меттью Шинкель

Ще одна проблема, яка існує з останнім прикладом (ORM), полягає в тому, що вона не є атомною .
Мар'ян

Відповіді:


181

ORM SQLAlchemy призначений для використання разом із шаром SQL, а не приховувати його. Але вам потрібно пам’ятати про одну або дві речі при використанні ORM та простого SQL в одній транзакції. В основному, з одного боку, зміни даних ORM будуть потрапляти в базу даних лише тоді, коли ви очистите зміни від свого сеансу. З іншого боку, оператори з обробки даних SQL не впливають на об'єкти, які перебувають у вашому сеансі.

Тож якщо ти скажеш

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

вона зробить те, що говорить, перейдіть за всіма об'єктами з бази даних, змініть всі об'єкти, а потім, коли настане час, щоб виправити зміни в базу даних, оновлюйте рядки по черзі.

Замість цього слід зробити це:

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

Це виконуватиметься як один запит, як ви очікували, і тому що принаймні конфігурація сеансу за замовчуванням закінчується всіма даними сеансу на фіксацію, у вас не виникає проблем із застарілими даними.

У майже випущеній серії 0,5 ви також можете використовувати цей метод для оновлення:

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

Це в основному запустить той самий оператор SQL, що і попередній фрагмент, але також вибере змінені рядки та закінчить термін дії будь-яких застарілих даних у сеансі. Якщо ви знаєте, що не використовуєте жодних даних сеансу після оновлення, ви також можете додати synchronize_session=Falseдо заяви оновлення та позбутися цього вибору.


2
по-третє, чи спровокує подія orm (наприклад, after_update)?
Кен

@Ken, ні, не буде. Див. Документ API для Query.update docs.sqlalchemy.org/en/13/orm/… . Натомість у вас є подія для after_bulk_update docs.sqlalchemy.org/uk/13/orm/…
TrilceAC

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

Спробуйте це =)


Цей метод спрацював для мене. Але проблема полягає в його повільності. Це потребує хорошого часу для декількох записів даних на 100 тис. Чи може бути більш швидкий метод?
baermathias

Дуже дякую, що цей підхід працював на мене. Дуже погано, що у sqlachemy немає коротшого способу оновлення jsonколонки
Jai Prakash,

6
Для тих, хто все ще має проблеми з продуктивністю при використанні цього методу: за замовчуванням це може зробити SELECT спочатку для кожного запису, а після цього лише UPDATE. Передача синхронізації_сесії = Неправда методу update () перешкоджає цьому, але переконайтеся, що це робити лише у тому випадку, якщо ви знову не використовуєте об'єкти, які ви оновлюєте, перед фіксацією ().
вівторок

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 () працює без потоку ()).

Я виявив, що час від часу великий запит, а потім повторення в python може бути до 2 порядків швидше, ніж багато запитів. Я припускаю, що ітерація над об’єктом запиту є менш ефективною, ніж ітерація над списком, сформованим методом all () об'єкта запиту.

[Зверніть увагу на коментар нижче - це зовсім не прискорило].


2
Додавання .all () та видалення .flush () зовсім не змінило час.
Джон Фугі

1

Якщо це пов’язано з накладними витратами в плані створення об'єктів, то, ймовірно, це зовсім не може бути пришвидшене SA.

Якщо це тому, що він завантажує пов'язані об’єкти, то, можливо, ви зможете зробити щось із ледачим завантаженням. Чи багато об’єктів створюється завдяки посиланням? (IE, отримання об’єкта компанії також отримує всі пов'язані з ними об'єкти People).


Ні, стіл все самостійно. Я ніколи раніше не використовував ORM - це просто щось, у чому вони погані?
Джон Фугі

1
Існує накладні витрати через створення об’єктів, але, на мою думку, варто покарати - можливість наполегливо зберігати об’єкти в базі даних є приголомшливим.
Метью Шінккель
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.