Эффективное обновление базы данных с помощью SQLAlchemy ORM


117

Я запускаю новое приложение и ищу возможность использования 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 и с тем, как они работают под нагрузкой и принуждением. У меня нет удобной ссылки, но ее стоит прочитать.
— Мэтью Шинкель

Другая проблема, которая существует с последним примером (ORM), заключается в том, что он не атомарен .
— Мариан

Ответы:


182

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)?
— Кен

@ Кен, нет, не будет. См. Документ API для Query.update docs.sqlalchemy.org/en/13/orm/… . Вместо этого у вас есть событие для 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()

Попробуй это =)


Этот метод у меня сработал. Но проблема в том, что он медленный. Требуется немало времени для нескольких записей по 100 тыс. Данных. Может быть, есть более быстрый способ?
— baermathias

Большое спасибо, этот подход сработал для меня. Очень плохо, что sqlachemy не имеет более короткого способа обновить jsonстолбец
— Jai Prakash

6
Для тех, у кого все еще есть проблемы с производительностью при использовании этого метода: по умолчанию он может сначала выполнять SELECT для каждой записи, а затем только UPDATE. Передача synchronize_session = False методу update () предотвращает это, но убедитесь, что вы делаете это только в том случае, если вы не используете объекты, которые вы обновляете снова перед commit ().
— teuneboon

26

Есть несколько способов ОБНОВЛЕНИЯ с помощью 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)

7

Вот пример того, как решить ту же проблему без необходимости вручную отображать поля:

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 () вообще не изменили время.
— Джон Фухи,

1

Если это связано с накладными расходами на создание объектов, то, вероятно, это вообще невозможно ускорить с помощью SA.

Если это связано с загрузкой связанных объектов, возможно, вы сможете что-то сделать с ленивой загрузкой. Много ли объектов создается по ссылкам? (IE, получение объекта Company также получает все связанные объекты People).


Нет, стол сам по себе. Я никогда раньше не использовал ORM - они просто в этом плохо разбираются?
— Джон Фухи

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