SQLAlchemy นั้นมีค่าเทียบเท่ากับ getango หรือ get_or_create ของ Django หรือไม่


160

ฉันต้องการรับวัตถุจากฐานข้อมูลถ้ามันมีอยู่แล้ว (ขึ้นอยู่กับพารามิเตอร์ที่ให้ไว้) หรือสร้างมันถ้ามันไม่ได้

Django's get_or_create(หรือแหล่งที่มา ) ทำสิ่งนี้ มีทางลัดที่เทียบเท่าใน SQLAlchemy หรือไม่?

ฉันกำลังเขียนมันออกมาอย่างชัดเจนเช่นนี้:

def get_or_create_instrument(session, serial_number):
    instrument = session.query(Instrument).filter_by(serial_number=serial_number).first()
    if instrument:
        return instrument
    else:
        instrument = Instrument(serial_number)
        session.add(instrument)
        return instrument

4
สำหรับผู้ที่ต้องการเพิ่มวัตถุหากยังไม่มีให้ดูsession.merge: stackoverflow.com/questions/12297156/…
Anton Tarasenko

คำตอบ:


96

นั่นเป็นวิธีที่จะทำได้ไม่มีทางลัด AFAIK ที่พร้อมใช้งาน

คุณสามารถพูดคุยทั่วไปของหลักสูตร:

def get_or_create(session, model, defaults=None, **kwargs):
    instance = session.query(model).filter_by(**kwargs).first()
    if instance:
        return instance, False
    else:
        params = dict((k, v) for k, v in kwargs.iteritems() if not isinstance(v, ClauseElement))
        params.update(defaults or {})
        instance = model(**params)
        session.add(instance)
        return instance, True

2
ฉันคิดว่าที่คุณอ่าน "session.Query (model.filter_by (** kwargs) .first ()" คุณควรอ่าน "session.Query (model.filter_by (** kwargs)). first ()"
pkoch

3
ควรมีการล็อกรอบนี้เพื่อให้เธรดอื่นไม่สร้างอินสแตนซ์ก่อนที่เธรดนี้จะมีโอกาสได้หรือไม่
EoghanM

2
@EoghanM: โดยปกติเซสชันของคุณจะเป็นเธรดในตัวดังนั้นจะไม่สำคัญ เซสชัน SQLAlchemy ไม่ได้หมายความว่าปลอดภัยต่อเธรด
Wolph

5
@ WolpH อาจเป็นอีกกระบวนการหนึ่งที่พยายามสร้างระเบียนเดียวกันพร้อมกัน ดูการใช้งาน get_or_create ของ Django ตรวจสอบข้อผิดพลาดด้านความสมบูรณ์และอาศัยการใช้งานข้อ จำกัด ที่ไม่ซ้ำกันอย่างเหมาะสม
Ivan Virabyan

1
@IvanVirabyan: ฉันคิดว่า @EoghanM กำลังพูดถึงอินสแตนซ์เซสชัน ในกรณีนี้ควรมีบล็อกtry...except IntegrityError: instance = session.Query(...)อยู่รอบ ๆ session.add
Wolph

109

ทำตามวิธีแก้ปัญหาของ @WoLpH นี่คือรหัสที่เหมาะกับฉัน (เวอร์ชั่นง่าย):

def get_or_create(session, model, **kwargs):
    instance = session.query(model).filter_by(**kwargs).first()
    if instance:
        return instance
    else:
        instance = model(**kwargs)
        session.add(instance)
        session.commit()
        return instance

ด้วยสิ่งนี้ฉันสามารถ get_or_create วัตถุใด ๆ ของแบบจำลองของฉันได้

สมมติว่าวัตถุโมเดลของฉันคือ:

class Country(Base):
    __tablename__ = 'countries'
    id = Column(Integer, primary_key=True)
    name = Column(String, unique=True)

รับหรือสร้างวัตถุของฉันฉันเขียน:

myCountry = get_or_create(session, Country, name=countryName)

3
สำหรับผู้ที่ค้นหาเช่นฉันนี่เป็นทางออกที่เหมาะสมในการสร้างแถวหากยังไม่มีอยู่
Spencer Rathbun

3
คุณไม่จำเป็นต้องเพิ่มอินสแตนซ์ใหม่ในเซสชันหรือไม่ มิฉะนั้นหากคุณออก session.commit () ในรหัสการโทรจะไม่มีอะไรเกิดขึ้นเนื่องจากไม่มีการเพิ่มอินสแตนซ์ใหม่ในเซสชัน
CadentOrange

1
ขอบคุณสำหรับสิ่งนี้. ฉันพบสิ่งนี้มีประโยชน์มากและฉันได้สร้างส่วนสำคัญของมันเพื่อใช้ในอนาคต gist.github.com/jangeador/e7221fc3b5ebeeac9a08
jangeador

ฉันต้องใส่รหัสที่ไหน
Victor Alvarado

7
เนื่องจากคุณผ่านเซสชันเป็นอาร์กิวเมนต์คุณควรหลีกเลี่ยงcommit(หรืออย่างน้อยใช้เพียง a flush) สิ่งนี้จะทำให้การควบคุมเซสชันกับผู้เรียกใช้วิธีนี้และจะไม่มีความเสี่ยงในการออกคำสั่งก่อนกำหนด นอกจากนี้การใช้one_or_none()แทนfirst()อาจปลอดภัยกว่าเล็กน้อย
exhuma

52

ฉันเล่นกับปัญหานี้แล้วและได้จบลงด้วยวิธีแก้ปัญหาที่ค่อนข้างแข็งแกร่ง:

def get_one_or_create(session,
                      model,
                      create_method='',
                      create_method_kwargs=None,
                      **kwargs):
    try:
        return session.query(model).filter_by(**kwargs).one(), False
    except NoResultFound:
        kwargs.update(create_method_kwargs or {})
        created = getattr(model, create_method, model)(**kwargs)
        try:
            session.add(created)
            session.flush()
            return created, True
        except IntegrityError:
            session.rollback()
            return session.query(model).filter_by(**kwargs).one(), False

ฉันเพิ่งเขียนโพสต์บล็อกที่ขยายตัวอย่างเป็นธรรมในทุกรายละเอียด แต่มีแนวคิดบางประการเกี่ยวกับสาเหตุที่ฉันใช้สิ่งนี้

  1. มัน unpacks เป็น tuple ที่บอกคุณว่ามีวัตถุอยู่หรือไม่ สิ่งนี้มักจะมีประโยชน์ในเวิร์กโฟลว์ของคุณ

  2. ฟังก์ชั่นนี้ให้ความสามารถในการทำงานกับ@classmethodฟังก์ชั่นผู้สร้างที่ได้รับการตกแต่ง (และคุณลักษณะเฉพาะสำหรับพวกมัน)

  3. โซลูชันนี้ป้องกันสภาวะการแข่งขันเมื่อคุณมีกระบวนการมากกว่าหนึ่งกระบวนการเชื่อมต่อกับที่เก็บข้อมูล

แก้ไข: ฉันได้เปลี่ยนsession.commit()ไปsession.flush()ตามที่อธิบายไว้ในบล็อกโพสต์นี้ โปรดทราบว่าการตัดสินใจเหล่านี้มีความเฉพาะเจาะจงกับที่เก็บข้อมูลที่ใช้ (Postgres ในกรณีนี้)

แก้ไข 2: ฉันได้รับการปรับปรุงโดยใช้ {} เป็นค่าเริ่มต้นในฟังก์ชั่นนี้เป็น Python gotcha ทั่วไป ขอบคุณสำหรับความคิดเห็นไนเจล! หากคุณอยากรู้เกี่ยวกับ gotcha ลองดูคำถาม StackOverflowและโพสต์บล็อกนี้


1
เมื่อเทียบกับสิ่งที่สเปนเซอร์พูดวิธีแก้ปัญหานี้เป็นวิธีที่ดีเนื่องจากป้องกันเงื่อนไขการแข่งขัน (โดยการยืนยัน / ล้างเซสชั่นระวัง) และเลียนแบบอย่างสมบูรณ์แบบที่ Django ทำ
kiddouk

@ kiddouk ไม่ไม่เลียนแบบ "สมบูรณ์แบบ" ของ Django get_or_createคือไม่ด้ายปลอดภัย มันไม่ใช่อะตอม นอกจากนี้ Django get_or_createจะส่งกลับค่าสถานะ True หากมีการสร้างอินสแตนซ์หรือตั้งค่าสถานะเท็จเป็นอย่างอื่น
Kar

@Kate ถ้าคุณดู Django get_or_createมันเกือบจะเหมือนกันทุกประการ วิธีการแก้ปัญหานี้ยังส่งกลับTrue/Falseค่าสถานะการส่งสัญญาณถ้าวัตถุถูกสร้างหรือนำมาใช้และไม่ได้เป็นอะตอม อย่างไรก็ตามความปลอดภัยของเธรดและการอัปเดตของอะตอมนั้นเป็นเรื่องที่น่ากังวลสำหรับฐานข้อมูลไม่ใช่สำหรับ Django, Flask หรือ SQLAlchemy และทั้งในโซลูชันนี้และ Django นั้นได้รับการแก้ไขโดยการทำธุรกรรมบนฐานข้อมูล
erik

1
สมมติว่าเขตข้อมูลที่ไม่เป็นโมฆะได้รับการจัดเตรียมค่า Null สำหรับเร็กคอร์ดใหม่ซึ่งจะเพิ่ม IntegrityError สิ่งทั้งปวงสับสนตอนนี้เราไม่ทราบว่าเกิดอะไรขึ้นจริงและเราได้รับข้อผิดพลาดอีกครั้งซึ่งไม่พบบันทึก
rajat

2
ไม่ควรIntegrityErrorส่งคืนเคสFalseเนื่องจากไคลเอ็นต์นี้ไม่ได้สร้างวัตถุหรือไม่
kevmitch

11

คำตอบที่ยอดเยี่ยมของ erik รุ่นดัดแปลง

def get_one_or_create(session,
                      model,
                      create_method='',
                      create_method_kwargs=None,
                      **kwargs):
    try:
        return session.query(model).filter_by(**kwargs).one(), True
    except NoResultFound:
        kwargs.update(create_method_kwargs or {})
        try:
            with session.begin_nested():
                created = getattr(model, create_method, model)(**kwargs)
                session.add(created)
            return created, False
        except IntegrityError:
            return session.query(model).filter_by(**kwargs).one(), True
  • ใช้ธุรกรรมซ้อนเพื่อย้อนกลับการเพิ่มรายการใหม่แทนที่จะย้อนกลับทุกอย่าง (ดูคำตอบนี้เพื่อใช้ธุรกรรมซ้อนกับ SQLite)
  • create_methodย้าย ถ้าวัตถุที่สร้างขึ้นมีความสัมพันธ์และมันได้รับมอบหมายสมาชิกผ่านความสัมพันธ์เหล่านั้นมันจะถูกเพิ่มลงในเซสชั่นโดยอัตโนมัติ เช่นสร้าง a bookซึ่งมีuser_idและuserเป็นความสัมพันธ์ที่สอดคล้องกันจากนั้นการทำbook.user=<user object>ภายในcreate_methodจะเพิ่มbookไปยังเซสชัน ซึ่งหมายความว่าcreate_methodจะต้องอยู่ภายในwithเพื่อรับประโยชน์จากการย้อนกลับในที่สุด โปรดทราบว่าbegin_nestedทริกเกอร์ฟลัชโดยอัตโนมัติ

โปรดทราบว่าหากใช้ MySQL จะต้องตั้งค่าระดับการแยกธุรกรรมเป็นREAD COMMITTEDมากกว่าREPEATABLE READเพื่อให้สามารถใช้งานได้ ของ Django get_or_create (และนี่ ) ใช้อุบายเดียวกันดูเพิ่มเติม Django เอกสาร


ฉันชอบที่จะหลีกเลี่ยงการย้อนกลับการเปลี่ยนแปลงที่ไม่เกี่ยวข้อง แต่การIntegrityErrorสืบค้นใหม่อาจยังคงล้มเหลวด้วยNoResultFoundระดับการแยกค่าเริ่มต้นของ MySQL REPEATABLE READหากเซสชั่นเคยสอบถามรูปแบบในการทำธุรกรรมเดียวกันก่อนหน้านี้ ทางออกที่ดีที่สุดที่ฉันสามารถทำได้คือโทรติดต่อsession.commit()ก่อนการสืบค้นนี้ซึ่งไม่เหมาะเนื่องจากผู้ใช้อาจไม่คาดคิด คำตอบที่อ้างอิงไม่มีปัญหานี้เนื่องจาก session.rollback () มีผลเช่นเดียวกันกับการเริ่มต้นธุรกรรมใหม่
kevmitch

อืม TIL การวางแบบสอบถามในธุรกรรมที่ซ้อนกันจะทำงานได้หรือไม่ คุณพูดถูกว่าcommitภายในฟังก์ชั่นนี้แย่กว่าการทำ a rollbackถึงแม้ว่าสำหรับกรณีการใช้งานที่เฉพาะเจาะจงมันก็เป็นที่ยอมรับได้
Adversus

ใช่การวางคิวรีเริ่มต้นในธุรกรรมซ้อนทำให้เป็นไปได้อย่างน้อยสำหรับเคียวรีที่สองเพื่อทำงาน มันจะยังคงล้มเหลวหากผู้ใช้สอบถามโมเดลอย่างชัดเจนก่อนหน้านี้ในธุรกรรมเดียวกัน ฉันได้ตัดสินใจว่าสิ่งนี้เป็นที่ยอมรับและผู้ใช้ควรได้รับการเตือนว่าจะไม่ทำเช่นนี้หรือจับข้อยกเว้นและตัดสินใจcommit()ด้วยตัวเอง ถ้าฉันเข้าใจรหัสถูกต้องนี่คือสิ่งที่ Django ทำ
kevmitch

ในเอกสาร django พวกเขาบอกว่าจะใช้, so it does not look like they try to handle this. Looking at the [source](https://github.com/django/django/blob/master/django/db/models/query.py#L491) confirms this. I'm not sure I understand your reply, you mean the user should put his/her query in a nested transaction? It's not clear to me how a อิทธิพลของ`READ COMMITTED SAVEPOINT REPEATABLE READ' หากไม่มีผลกระทบใด ๆ สถานการณ์ดูเหมือนจะไม่สามารถหลีกเลี่ยงได้หากผลกระทบดังนั้นข้อความค้นหาสุดท้ายจะซ้อนกันหรือไม่
Adversus

น่าสนใจเกี่ยวกับREAD COMMITEDบางทีฉันควรคิดใหม่การตัดสินใจของฉันที่จะไม่สัมผัสค่าเริ่มต้นของฐานข้อมูล ฉันมีการทดสอบที่เรียกคืนSAVEPOINTจากการสอบถามก่อนที่จะถูกสร้างขึ้นทำให้มันราวกับว่าแบบสอบถามที่ไม่เคย happend REPEATABLE READใน ดังนั้นฉันจึงพบว่าจำเป็นต้องใส่คิวรีในข้อลองในธุรกรรมซ้อนเพื่อให้เคียวรีในIntegrityErrorข้อยกเว้นสามารถทำงานได้เลย
kevmitch

6

สูตร SQLALchemyนี้ทำงานได้ดีและสวยงาม

สิ่งแรกที่ต้องทำคือการกำหนดฟังก์ชั่นที่กำหนดให้เซสชันทำงานด้วยและเชื่อมโยงพจนานุกรมกับ Session () ซึ่งติดตามคีย์ที่ไม่ซ้ำกันในปัจจุบัน

def _unique(session, cls, hashfunc, queryfunc, constructor, arg, kw):
    cache = getattr(session, '_unique_cache', None)
    if cache is None:
        session._unique_cache = cache = {}

    key = (cls, hashfunc(*arg, **kw))
    if key in cache:
        return cache[key]
    else:
        with session.no_autoflush:
            q = session.query(cls)
            q = queryfunc(q, *arg, **kw)
            obj = q.first()
            if not obj:
                obj = constructor(*arg, **kw)
                session.add(obj)
        cache[key] = obj
        return obj

ตัวอย่างของการใช้ฟังก์ชันนี้จะอยู่ใน mixin:

class UniqueMixin(object):
    @classmethod
    def unique_hash(cls, *arg, **kw):
        raise NotImplementedError()

    @classmethod
    def unique_filter(cls, query, *arg, **kw):
        raise NotImplementedError()

    @classmethod
    def as_unique(cls, session, *arg, **kw):
        return _unique(
                    session,
                    cls,
                    cls.unique_hash,
                    cls.unique_filter,
                    cls,
                    arg, kw
            )

และในที่สุดก็สร้างโมเดล get_or_create ที่ไม่เหมือนใคร:

from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

engine = create_engine('sqlite://', echo=True)

Session = sessionmaker(bind=engine)

class Widget(UniqueMixin, Base):
    __tablename__ = 'widget'

    id = Column(Integer, primary_key=True)
    name = Column(String, unique=True, nullable=False)

    @classmethod
    def unique_hash(cls, name):
        return name

    @classmethod
    def unique_filter(cls, query, name):
        return query.filter(Widget.name == name)

Base.metadata.create_all(engine)

session = Session()

w1, w2, w3 = Widget.as_unique(session, name='w1'), \
                Widget.as_unique(session, name='w2'), \
                Widget.as_unique(session, name='w3')
w1b = Widget.as_unique(session, name='w1')

assert w1 is w1b
assert w2 is not w3
assert w2 is not w1

session.commit()

สูตรนั้นลึกเข้าไปในความคิดและให้แนวทางที่แตกต่างกัน แต่ฉันใช้มันด้วยความสำเร็จที่ยิ่งใหญ่


1
ฉันชอบสูตรนี้ถ้ามีเพียงออบเจ็กต์ SQLAlchemy Session เดียวเท่านั้นที่สามารถแก้ไขฐานข้อมูลได้ ฉันอาจจะผิด แต่ถ้าช่วงอื่น (SQLAlchemy หรือไม่) แก้ไขฐานข้อมูลพร้อมกันฉันไม่เห็นว่าสิ่งนี้ป้องกันวัตถุที่อาจถูกสร้างขึ้นโดยช่วงอื่น ๆ ในขณะที่การทำธุรกรรมอย่างต่อเนื่อง ในกรณีเหล่านั้นฉันคิดว่าโซลูชันที่พึ่งพาการล้างข้อมูลหลังจาก session.add () และการจัดการข้อยกเว้นเช่นstackoverflow.com/a/21146492/3690333มีความน่าเชื่อถือมากกว่า
TrilceAC

3

ความหมายที่ใกล้เคียงที่สุดน่าจะเป็น:

def get_or_create(model, **kwargs):
    """SqlAlchemy implementation of Django's get_or_create.
    """
    session = Session()
    instance = session.query(model).filter_by(**kwargs).first()
    if instance:
        return instance, False
    else:
        instance = model(**kwargs)
        session.add(instance)
        session.commit()
        return instance, True

ไม่แน่ใจว่าเป็น kosher ได้อย่างไรมันขึ้นอยู่กับการกำหนดทั่วโลกSessionใน sqlalchemy แต่รุ่น Django ไม่ได้ใช้การเชื่อมต่อดังนั้น ...

tuple ที่ส่งคืนมีอินสแตนซ์และบูลีนที่ระบุว่าสร้างอินสแตนซ์นั้นหรือไม่ (เช่นมันเป็นเท็จถ้าเราอ่านอินสแตนซ์จาก db)

get_or_createบ่อยครั้งที่Django ใช้เพื่อให้แน่ใจว่ามีข้อมูลทั่วโลกดังนั้นฉันจึงมุ่งมั่นในจุดเริ่มต้นที่เป็นไปได้


สิ่งนี้จะทำงานได้ตราบใดที่มีการสร้างและติดตามเซสชันscoped_sessionซึ่งควรใช้การจัดการเซสชันที่ปลอดภัยของเธรด (มีอยู่ในปี 2014 หรือไม่)
cowbert

2

ฉันลดความซับซ้อนของ @Kevin เล็กน้อย วิธีการแก้ปัญหาเพื่อหลีกเลี่ยงการห่อฟังก์ชั่นทั้งหมดในif/ elseคำสั่ง วิธีนี้มีเพียงอันเดียวreturnที่ฉันพบว่าสะอาดกว่า:

def get_or_create(session, model, **kwargs):
    instance = session.query(model).filter_by(**kwargs).first()

    if not instance:
        instance = model(**kwargs)
        session.add(instance)

    return instance

1

ขึ้นอยู่กับระดับการแยกที่คุณนำมาใช้ไม่มีวิธีการแก้ปัญหาข้างต้นใดที่จะทำงานได้ ทางออกที่ดีที่สุดที่ฉันพบคือ RAW SQL ในรูปแบบต่อไปนี้:

INSERT INTO table(f1, f2, unique_f3) 
SELECT 'v1', 'v2', 'v3' 
WHERE NOT EXISTS (SELECT 1 FROM table WHERE f3 = 'v3')

สิ่งนี้ปลอดภัยสำหรับธุรกรรมไม่ว่าระดับการแยกและระดับความเท่าเทียม

ระวัง: เพื่อให้มีประสิทธิภาพคุณควรมี INDEX สำหรับคอลัมน์ที่ไม่ซ้ำกัน

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