क्या SQLAlchemy में Django के get_or_create के बराबर है?


160

मैं डेटाबेस से एक ऑब्जेक्ट प्राप्त करना चाहता हूं यदि यह पहले से मौजूद है (प्रदान किए गए मापदंडों के आधार पर) या यदि ऐसा नहीं है तो इसे बनाएं।

Django के 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/…
एंटोन तारासेंको

जवाबों:


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)) पढ़ना चाहिए। पहला (पहला")
pkoch

3
क्या इसके चारों ओर एक ताला होना चाहिए ताकि एक और धागा एक उदाहरण न बना सके, इससे पहले कि इस धागे को मौका मिले?
EoghanM

2
@ EoghanM: आम तौर पर आपका सत्र थ्रेडलोक होगा इसलिए यह कोई मायने नहीं रखेगा। SQLAlchemy सत्र थ्रेड-सुरक्षित होने के लिए नहीं है।
वोल्फ

5
@WolpH यह एक साथ एक ही रिकॉर्ड बनाने के लिए एक और प्रक्रिया हो सकती है। Django के get_or_create के कार्यान्वयन को देखें। यह अखंडता त्रुटि के लिए जाँच करता है, और अद्वितीय बाधाओं के उचित उपयोग पर निर्भर करता है।
इवान विरबाईन

1
@IvanVirabyan: मैंने मान लिया @ EoghanM सत्र उदाहरण के बारे में बात कर रहा था। उस स्थिति try...except IntegrityError: instance = session.Query(...)में session.addब्लॉक के आसपास होना चाहिए ।
वोल्फ

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

इसके साथ, मैं अपने मॉडल की किसी भी वस्तु को पाने में सक्षम हूं।

मान लीजिए कि मेरा मॉडल ऑब्जेक्ट है:

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
आप में से मेरे जैसे खोज करने वालों के लिए, यह एक पंक्ति बनाने का उचित समाधान है यदि यह पहले से मौजूद नहीं है।
स्पेंसर रथबुन

3
क्या आपको सत्र में नया उदाहरण जोड़ने की आवश्यकता नहीं है? अन्यथा यदि आप कॉलिंग कोड में session.commit () जारी करते हैं, तो कुछ भी नहीं होगा क्योंकि नया उदाहरण सत्र में जोड़ा नहीं गया है।
CadentOrange

1
इसके लिए शुक्रिया। मुझे यह इतना उपयोगी लगा कि मैंने इसे भविष्य में उपयोग के लिए तैयार कर लिया। gist.github.com/jangeador/e7221fc3b5ebeeac9a08
jangeador

जहां मुझे कोड डालने की आवश्यकता है?, मुझे निष्पादन संदर्भ त्रुटि प्राप्त हो रही है?
विक्टर अल्वाराडो

7
यह देखते हुए कि आप सत्र को तर्क के रूप में पारित करते हैं, इससे बचने के लिए बेहतर हो सकता है commit(या केवल flushइसके बजाय कम से कम उपयोग करें )। यह इस विधि के कॉलर को सत्र नियंत्रण छोड़ देता है और समय से पहले कमिट जारी करने का जोखिम नहीं उठाएगा। इसके अलावा, उपयोग one_or_none()करने के बजाय first()थोड़ा सुरक्षित हो सकता है।
प्रथमा

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. यह एक टपल को अनपैक करता है जो आपको बताता है कि ऑब्जेक्ट मौजूद था या नहीं। यह अक्सर आपके वर्कफ़्लो में उपयोगी हो सकता है।

  2. फ़ंक्शन @classmethodसजाए गए निर्माता कार्यों (और उनके लिए विशिष्ट विशेषताओं) के साथ काम करने की क्षमता देता है ।

  3. जब आप डेटासटर से जुड़ी एक से अधिक प्रक्रिया करते हैं, तो समाधान रेस की स्थितियों से बचाता है।

संपादित करें: मैं बदल दिया है session.commit()करने के लिए session.flush()के रूप में में विस्तार से बताया इस ब्लॉग पोस्ट । ध्यान दें कि ये निर्णय उपयोग किए गए डेटास्टोर (इस मामले में पोस्टग्रेज) के लिए विशिष्ट हैं।

संपादित करें 2: मैंने फ़ंक्शन में डिफ़ॉल्ट मान के रूप में {} का उपयोग करके अपडेट किया है क्योंकि यह विशिष्ट पायथन गोचा है। टिप्पणी के लिए धन्यवाद , निगेल! यदि इस गोच के बारे में आपकी उत्सुकता है, तो इस StackOverflow प्रश्न और इस ब्लॉग पोस्ट को देखें


1
स्पेंसर जो कहता है उसकी तुलना में , यह समाधान अच्छा है क्योंकि यह रेस की स्थिति को रोकता है (सत्र को रोकना / भड़काना, सावधान रहना) और पूरी तरह से मिक्स करता है कि जोंगो क्या करता है।
किद्दौक

@kiddouk नहीं, यह "पूरी तरह से" नकल नहीं करता है। Django के get_or_createहै नहीं धागा सुरक्षित। यह परमाणु नहीं है। इसके अलावा, get_or_createअगर उदाहरण के तौर पर बनाया गया या गलत झंडा है तो Django एक सच्चा झंडा लौटाता है।
कर

@ अगर आप Django को देखो get_or_createयह लगभग एक ही बात करता है। यह समाधान True/Falseध्वज को संकेत करने के लिए भी लौटाता है यदि ऑब्जेक्ट बनाया गया था या लाया गया था, और परमाणु भी नहीं है। हालाँकि, थ्रेड-सुरक्षा और परमाणु अद्यतन डेटाबेस के लिए एक चिंता का विषय है, न कि Django, फ्लास्क या SQLAlchemy के लिए, और इस समाधान और Django के दोनों में, डेटाबेस पर लेनदेन द्वारा हल किए जाते हैं।
इरिक

1
मान लीजिए कि एक गैर शून्य क्षेत्र को एक नए रिकॉर्ड के लिए शून्य मूल्य प्रदान किया गया था, तो यह इंटीग्रिटीइरोर जुटाएगा। पूरी बात गड़बड़ हो जाती है, अब हम नहीं जानते कि वास्तव में क्या हुआ था और हमें एक और त्रुटि मिलती है, कि कोई रिकॉर्ड नहीं मिला है।
रजत

2
क्या IntegrityErrorमामला वापस नहीं आना चाहिए Falseक्योंकि इस ग्राहक ने वस्तु नहीं बनाई है?
केवमिच

11

एरिक के उत्कृष्ट उत्तर का एक संशोधित संस्करण

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। यदि बनाई गई वस्तु में संबंध हैं और यह उन संबंधों के माध्यम से सदस्यों को सौंपा गया है, तो यह स्वचालित रूप से सत्र में जोड़ा जाता है। उदाहरण के लिए एक बनाने के 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फिर से क्वेरी NoResultFoundMySQL डिफ़ॉल्ट अलगाव स्तर के साथ विफल हो सकती है REPEATABLE READ। सबसे अच्छा समाधान मैं session.commit()इस क्वेरी से पहले कॉल कर सकता हूं , जो आदर्श भी नहीं है क्योंकि उपयोगकर्ता इसकी उम्मीद नहीं कर सकता है। संदर्भित जवाब सत्र के बाद से यह समस्या नहीं है। एक नया लेनदेन शुरू करने का एक ही प्रभाव है।
केवमिच

हुह, टीआईएल। क्या नेस्टेड लेनदेन कार्य में क्वेरी डालना होगा? आप सही कह रहे हैं कि commitइस फंक्शन के अंदर यकीनन ए करने से ज्यादा बुरा है rollback, भले ही विशिष्ट उपयोग के मामलों के लिए यह स्वीकार्य हो।
एडवर्सस

हां, शुरुआती क्वेरी को नेस्टेड ट्रांजेक्शन में डालने से दूसरी क्वेरी के लिए काम करना कम से कम संभव हो जाता है। यह तब भी विफल होगा जब उपयोगकर्ता ने स्पष्ट रूप से उसी लेन-देन में पहले मॉडल की पुष्टि की थी। मैंने फैसला किया है कि यह स्वीकार्य है और उपयोगकर्ता को केवल ऐसा नहीं करने के लिए चेतावनी दी जानी चाहिए या अन्यथा अपवाद को पकड़ना चाहिए और यह निर्णय लेना चाहिए कि क्या commit()खुद को। अगर कोड के बारे में मेरी समझ सही है, तो यह Django क्या करता है।
केवमिच

Django के प्रलेखन में वे कहते हैं कि 'READ COMMITTED , 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 SAVEPOINT` प्रभावों का उपयोग करना चाहिए REPEATABLE READ। यदि कोई प्रभाव नहीं है तो स्थिति असम्भव लगती है, यदि प्रभाव तब बहुत ही अंतिम क्वेरी को नेस्टेड किया जा सकता है?
एडवर्सस

यह दिलचस्प है READ COMMITED, शायद मुझे अपने निर्णय को डेटाबेस की चूक को न छूने के लिए पुनर्विचार करना चाहिए। मैंने परीक्षण किया है कि SAVEPOINTएक क्वेरी बनाने से पहले से बहाल करना यह बनाता है जैसे कि वह क्वेरी कभी भी ख़ुशी से नहीं होती है REPEATABLE READ। इसलिए, मुझे नेस्टेड ट्रांजेक्शन में कोशिश क्लॉज में क्वेरी को संलग्न करना आवश्यक लगा, ताकि IntegrityErrorक्लॉज को छोड़कर क्वेरी बिल्कुल काम कर सके।
केवमिच

6

यह SQLALchemy रेसिपी अच्छा और सुरुचिपूर्ण काम करती है।

पहली बात यह है कि एक फ़ंक्शन को परिभाषित करने के लिए जिसे सत्र के साथ काम करने के लिए दिया जाता है, और एक शब्दकोश को सत्र () के साथ जोड़ा जाता है जो वर्तमान अद्वितीय कुंजी का ट्रैक रखता है ।

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

इस फ़ंक्शन का उपयोग करने का एक उदाहरण एक मिश्रण में होगा:

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 सत्र ऑब्जेक्ट डेटाबेस को संशोधित कर सकता है। मैं गलत हो सकता हूं, लेकिन यदि अन्य सत्र (SQLAlchemy या नहीं) डेटाबेस को समवर्ती रूप से संशोधित करते हैं, तो मैं यह नहीं देखता कि यह उन वस्तुओं से कैसे बचाता है जो लेन-देन चालू होने के दौरान अन्य सत्रों द्वारा बनाए गए हो सकते हैं। उन मामलों में, मुझे लगता है कि समाधान है कि session.add () और जैसे अपवाद हैंडलिंग के बाद निस्तब्धता पर भरोसा करते हैं stackoverflow.com/a/21146492/3690333 अधिक विश्वसनीय हैं।
त्रिलोक

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

यह सुनिश्चित नहीं है कि कैसे यह विश्व स्तर पर Sessionsqlalchemy में परिभाषित पर भरोसा करना है , लेकिन Django संस्करण एक कनेक्शन नहीं लेता है ...

टपल लौटे में उदाहरण और एक बूलियन है जो यह दर्शाता है कि यदि उदाहरण बनाया गया था (यानी यह गलत है यदि हम db से उदाहरण पढ़ते हैं)।

Django का get_or_createउपयोग अक्सर यह सुनिश्चित करने के लिए किया जाता है कि वैश्विक डेटा उपलब्ध है, इसलिए मैं जल्द से जल्द संभव हो रहा हूं।


यह तब तक काम करना चाहिए जब तक सत्र बनाया और ट्रैक किया जाता है scoped_session, जिसे थ्रेड-सुरक्षित सत्र प्रबंधन को लागू करना चाहिए (क्या यह 2014 में मौजूद था?)।
काऊबर्ट

2

मैंने @ केविन को थोड़ा सरल किया। संपूर्ण फ़ंक्शन को 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

आपके द्वारा अपनाए गए अलगाव स्तर के आधार पर, उपरोक्त समाधानों में से कोई भी काम नहीं करेगा। मैंने जो सबसे अच्छा समाधान पाया है, वह निम्न रूप में एक रॉ एसक्यूएल है:

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

यह अलग-अलग सुरक्षित है जो भी अलगाव स्तर और समानता की डिग्री है।

खबरदार: इसे कुशल बनाने के लिए, अद्वितीय कॉलम के लिए एक INDEX होना बुद्धिमानी होगी।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.