อัปเดตฐานข้อมูลอย่างมีประสิทธิภาพโดยใช้ 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 หรือไม่ หรือใช้ python ORM อื่น ๆ ? หรือฉันควรกลับไปเขียน SQL ด้วยมือ?


1
โอเคฉันคิดว่าคำตอบคือ "นี่ไม่ใช่สิ่งที่ ORM ทำได้ดี" โอ้ดี; ฉันใช้ชีวิตและเรียนรู้
John Fouhy

มีการทดลองบางอย่างที่ใช้ ORM ที่แตกต่างกันและวิธีการดำเนินการภายใต้ภาระและการบังคับ ไม่มีลิงค์สะดวก แต่ควรอ่าน
Matthew Schinckel

ปัญหาที่เกิดขึ้นกับที่ผ่านมา (ออม) ตัวอย่างก็คือว่ามันไม่ได้เป็นอะตอม
Marian

คำตอบ:


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
ในวิธีที่ 3 มันจะทริกเกอร์เหตุการณ์ orm (เช่น after_update) หรือไม่
Ken

@ เคนไม่ยอมหรอก ดูเอกสาร 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()

ลองสิ่งนี้ =)


วิธีนี้ใช้ได้ผลสำหรับฉัน แต่ปัญหาคือมันช้า ต้องมีช่วงเวลาที่ดีสำหรับการบันทึกข้อมูลเพียงไม่กี่ 100k อาจมีวิธีที่เร็วกว่านี้หรือไม่?
baermathias

ขอบคุณมากวิธีนี้ใช้ได้ผลสำหรับฉัน แย่จริงๆที่ sqlachemy ไม่มีวิธีที่สั้นกว่าในการอัปเดตjsonคอลัมน์
Jai Prakash

6
สำหรับผู้ที่ยังคงมีปัญหาด้านประสิทธิภาพเมื่อใช้วิธีนี้: โดยค่าเริ่มต้นสิ่งนี้อาจทำการเลือกสำหรับทุกระเบียนก่อนและจะอัปเดตหลังจากนั้นเท่านั้น การส่งผ่าน synchronize_session = False ไปยังวิธี update () จะป้องกันไม่ให้สิ่งนี้เกิดขึ้น แต่อย่าลืมทำสิ่งนี้ก็ต่อเมื่อคุณไม่ได้ใช้วัตถุที่คุณอัปเดตอีกครั้งก่อนที่จะกระทำ ()
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(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, กระทำ () ทำงานโดยไม่ต้องล้าง ())

ฉันพบว่าบางครั้งการทำแบบสอบถามขนาดใหญ่และการทำซ้ำใน python อาจมีขนาดได้ถึง 2 คำสั่งที่เร็วกว่าการสืบค้นจำนวนมาก ฉันคิดว่าการวนซ้ำบนวัตถุแบบสอบถามมีประสิทธิภาพน้อยกว่าการวนซ้ำในรายการที่สร้างโดยวิธี all () ของวัตถุแบบสอบถาม

[โปรดทราบความคิดเห็นด้านล่าง - สิ่งนี้ไม่ได้เร่งความเร็วเลย]


2
การเพิ่ม. all () และการลบ. flush () ไม่ได้เปลี่ยนเวลาเลย
John Fouhy

1

ถ้าเป็นเพราะค่าใช้จ่ายในการสร้างวัตถุก็คงไม่สามารถเร่งความเร็วได้เลยด้วย SA

หากเป็นเพราะมันกำลังโหลดออบเจ็กต์ที่เกี่ยวข้องคุณอาจสามารถทำบางอย่างได้โดยขี้เกียจโหลด มีการสร้างวัตถุจำนวนมากเนื่องจากการอ้างอิงหรือไม่? (IE การรับวัตถุ บริษัท จะได้รับวัตถุ People ที่เกี่ยวข้องทั้งหมดด้วย)


ไม่โต๊ะทั้งหมดเป็นของตัวเอง ฉันไม่เคยใช้ ORM มาก่อน - นี่เป็นเพียงสิ่งที่ไม่ดีหรือไม่?
John Fouhy

1
มีค่าใช้จ่ายเนื่องจากการสร้าง Objects แต่ในความคิดของฉันมันคุ้มค่ากับการลงโทษ - ความสามารถในการจัดเก็บวัตถุไว้ในฐานข้อมูลอย่างต่อเนื่องนั้นยอดเยี่ยมมาก
Matthew Schinckel
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.