Эффективное обновление базы данных с помощью 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()

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

Vin
источник
Этот метод у меня сработал. Но проблема в том, что он медленный. Требуется немало времени для нескольких записей по 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
Из-за создания объектов возникают накладные расходы, но, на мой взгляд, они того стоят - возможность постоянно хранить объекты в базе данных - это здорово.
Мэтью Шинкель