SQLAlchemy có tương đương với get_or_create của Django không?


160

Tôi muốn lấy một đối tượng từ cơ sở dữ liệu nếu nó đã tồn tại (dựa trên các tham số được cung cấp) hoặc tạo nó nếu nó không có.

Django get_or_create(hoặc nguồn ) làm điều này. Có một lối tắt tương đương trong SQLAlchemy không?

Tôi hiện đang viết nó ra một cách rõ ràng như thế này:

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
Đối với những người chỉ muốn thêm đối tượng nếu nó chưa tồn tại, hãy xem session.merge: stackoverflow.com/questions/12297156/
mẹo

Câu trả lời:


96

Về cơ bản đó là cách để làm điều đó, không có phím tắt nào có sẵn AFAIK.

Bạn có thể khái quát hóa nó:

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
Tôi nghĩ rằng nơi bạn đọc "session.Query (model.filter_by (** kwargs) .first ()", bạn nên đọc "session.Query (model.filter_by (** kwargs)). First ()".
pkoch

3
Có nên có một khóa xung quanh điều này để một chủ đề khác không tạo ra một thể hiện trước khi chủ đề này có cơ hội?
EoghanM

2
@EoghanM: Thông thường phiên của bạn sẽ là chủ đề nên điều này không thành vấn đề. Phiên SQLAlchemy không có nghĩa là an toàn cho chuỗi.
Wolph

5
@WolpH có thể là một quá trình khác cố gắng tạo cùng một bản ghi. Nhìn vào việc triển khai get_or_create của Django. Nó kiểm tra lỗi toàn vẹn và dựa vào việc sử dụng đúng các ràng buộc duy nhất.
Ivan Virabyan

1
@IvanVirabyan: Tôi giả sử @EoghanM đã nói về phiên làm việc. Trong trường hợp đó nên có một try...except IntegrityError: instance = session.Query(...)xung quanh session.addkhối.
Wolph

109

Theo giải pháp của @WoLpH, đây là mã phù hợp với tôi (phiên bản đơn giản):

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

Với điều này, tôi có thể get_or_create bất kỳ đối tượng nào trong mô hình của tôi.

Giả sử đối tượng mô hình của tôi là:

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

Để có được hoặc tạo đối tượng của tôi, tôi viết:

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

3
Đối với những bạn tìm kiếm như tôi, đây là giải pháp thích hợp để tạo một hàng nếu nó chưa tồn tại.
Spencer Rathbun

3
Bạn không cần thêm phiên bản mới vào phiên? Mặt khác, nếu bạn phát hành session.commit () trong mã gọi, sẽ không có gì xảy ra vì phiên bản mới không được thêm vào phiên.
CadentOrange

1
Cảm ơn vì điều này. Tôi đã tìm thấy điều này hữu ích đến nỗi tôi đã tạo ra một ý chính của nó để sử dụng trong tương lai. gist.github.com/jangeador/e7221fc3b5ebeeac9a08
jangeador

Tôi cần đặt mã ở đâu?, tôi có thể xử lý lỗi ngữ cảnh thực thi không?
Victor Alvarado

7
Cho rằng bạn vượt qua phiên làm đối số, tốt hơn là nên tránh commit(hoặc ít nhất chỉ sử dụng flushthay thế). Điều này để lại quyền kiểm soát phiên cho người gọi phương thức này và sẽ không có nguy cơ đưa ra một cam kết sớm. Ngoài ra, sử dụng one_or_none()thay vì first()có thể an toàn hơn một chút.
shoutuma

52

Tôi đã chơi với vấn đề này và đã kết thúc với một giải pháp khá mạnh mẽ:

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

Tôi chỉ viết một bài đăng blog khá rộng rãi về tất cả các chi tiết, nhưng một vài ý tưởng khá lý do tại sao tôi sử dụng nó.

  1. Nó giải nén thành một tuple cho bạn biết đối tượng có tồn tại hay không. Điều này thường có thể hữu ích trong quy trình làm việc của bạn.

  2. Hàm cung cấp khả năng làm việc với các @classmethodchức năng của người tạo trang trí (và các thuộc tính dành riêng cho chúng).

  3. Giải pháp bảo vệ chống lại Điều kiện cuộc đua khi bạn có nhiều hơn một quy trình được kết nối với kho dữ liệu.

EDIT: Tôi đã thay đổi session.commit()đến session.flush()như đã giải thích trong bài viết trên blog này . Lưu ý rằng các quyết định này là cụ thể cho kho dữ liệu được sử dụng (Postgres trong trường hợp này).

EDIT 2: Tôi đã cập nhật bằng cách sử dụng {} làm giá trị mặc định trong hàm vì đây là mã xác thực Python điển hình. Cảm ơn vì nhận xét , Nigel! Nếu bạn tò mò về gotcha này, hãy xem câu hỏi StackOverflow nàybài đăng trên blog này .


1
So với những gì Spencer nói , giải pháp này là giải pháp tốt vì nó ngăn chặn các điều kiện Race (bằng cách cam kết / xóa phiên, hãy cẩn thận) và bắt chước hoàn hảo những gì Django làm.
kiddouk

@kiddouk Không, nó không bắt chước "hoàn hảo". Django không phảiget_or_create là chủ đề an toàn. Nó không phải là nguyên tử. Ngoài ra, Django trả về một cờ True nếu thể hiện được tạo hoặc một cờ Sai. get_or_create
Kar

@Kate nếu bạn nhìn vào Django get_or_createthì nó gần như giống hệt như vậy. Giải pháp này cũng trả về True/Falsecờ để báo hiệu nếu đối tượng được tạo hoặc tìm nạp và cũng không phải là nguyên tử. Tuy nhiên, cập nhật nguyên tử và cập nhật nguyên tử là mối quan tâm đối với cơ sở dữ liệu, không phải cho Django, Flask hay SQLAlchemy, và trong cả giải pháp này và Django, đều được giải quyết bằng các giao dịch trên cơ sở dữ liệu.
erik

1
Giả sử một trường không null được cung cấp giá trị null cho một bản ghi mới, nó sẽ tăng IntegrityError. Toàn bộ sự việc bị rối tung, bây giờ chúng tôi không biết chuyện gì đã thực sự xảy ra và chúng tôi gặp một lỗi khác, rằng không có bản ghi nào được tìm thấy.
rajat

2
Không nên IntegrityErrortrả lại trường hợp Falsevì khách hàng này không tạo đối tượng?
kevmitch

11

Một phiên bản sửa đổi của câu trả lời xuất sắc của 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
  • Sử dụng giao dịch lồng nhau để chỉ quay lại việc thêm mục mới thay vì khôi phục mọi thứ (Xem câu trả lời này để sử dụng giao dịch lồng nhau với SQLite)
  • Di chuyển create_method. Nếu đối tượng được tạo có quan hệ và nó được gán thành viên thông qua các quan hệ đó, nó sẽ tự động được thêm vào phiên. Ví dụ: tạo một book, có user_idusernhư mối quan hệ tương ứng, sau đó thực hiện book.user=<user object>bên trong create_methodsẽ thêm bookvào phiên. Điều này có nghĩa là create_methodphải ở bên trong withđể hưởng lợi từ việc khôi phục cuối cùng. Lưu ý rằng begin_nestedtự động kích hoạt một tuôn ra.

Lưu ý rằng nếu sử dụng MySQL, mức cô lập giao dịch phải được đặt thành READ COMMITTEDthay vì REPEATABLE READđể nó hoạt động. Get_or_create của Django (và ở đây ) sử dụng cùng một chiến lược, xem thêm tài liệu về Django .


Tôi thích điều này tránh việc đẩy lùi các thay đổi không liên quan, tuy nhiên IntegrityErrortruy vấn lại vẫn có thể thất bại với NoResultFoundmức cô lập mặc định của MySQL REPEATABLE READnếu phiên trước đó đã truy vấn mô hình trong cùng một giao dịch. Giải pháp tốt nhất tôi có thể đưa ra là gọi session.commit()trước truy vấn này, điều này cũng không lý tưởng vì người dùng có thể không mong đợi nó. Câu trả lời được tham chiếu không có vấn đề này vì session.rollback () có cùng tác dụng bắt đầu một giao dịch mới.
kevmitch

Hừ, TIL. Sẽ đặt truy vấn trong một giao dịch lồng nhau? Bạn đúng rằng commitbên trong chức năng này tệ hơn nhiều so với thực hiện rollback, mặc dù đối với các trường hợp sử dụng cụ thể, nó có thể được chấp nhận.
Adversus

Có, việc đặt truy vấn ban đầu trong một giao dịch lồng nhau làm cho ít nhất có thể để truy vấn thứ hai hoạt động. Nó vẫn sẽ thất bại nếu người dùng truy vấn rõ ràng mô hình trước đó trong cùng một giao dịch. Tôi đã quyết định rằng điều này có thể chấp nhận được và người dùng chỉ nên được cảnh báo không làm điều này hoặc nếu không bắt ngoại lệ và quyết định xem có nên commit()tự mình không. Nếu sự hiểu biết của tôi về mã là chính xác, đây là những gì Django làm.
kevmitch

Trong tài liệu django, họ nói sẽ sử dụng các , 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 ảnh hưởng của `READ CAMITED SAVEPOINT` REPEATABLE READ. Nếu không có hiệu lực thì tình hình dường như không thể giải quyết được, nếu có hiệu lực thì truy vấn cuối cùng có thể được lồng nhau không?
Adversus

Điều đó thật thú vị READ COMMITED, có lẽ tôi nên suy nghĩ lại về quyết định không chạm vào cơ sở dữ liệu mặc định. Tôi đã kiểm tra rằng việc khôi phục một SAVEPOINTtừ trước khi một truy vấn được thực hiện làm cho nó như thể truy vấn đó không bao giờ xảy ra REPEATABLE READ. Do đó, tôi thấy cần phải đặt truy vấn trong mệnh đề thử trong giao dịch lồng nhau để truy vấn trong IntegrityErrormệnh đề ngoại trừ có thể hoạt động hoàn toàn.
kevmitch

6

Công thức SQLALchemy này thực hiện công việc tốt đẹp và thanh lịch.

Điều đầu tiên cần làm là xác định một chức năng được cung cấp một Phiên để làm việc và liên kết một từ điển với Phiên () để theo dõi các khóa duy nhất hiện tại .

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

Một ví dụ về việc sử dụng chức năng này sẽ có trong một 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
            )

Và cuối cùng tạo ra mô hình get_or_create độc ​​đáo:

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()

Công thức đi sâu hơn vào ý tưởng và cung cấp các cách tiếp cận khác nhau nhưng tôi đã sử dụng phương pháp này rất thành công.


1
Tôi thích công thức này nếu chỉ một đối tượng Phiên SQLAlchemy duy nhất có thể sửa đổi cơ sở dữ liệu. Tôi có thể sai, nhưng nếu các phiên khác (SQLAlchemy hoặc không) sửa đổi cơ sở dữ liệu đồng thời thì tôi không thấy cách này bảo vệ chống lại các đối tượng có thể được tạo bởi các phiên khác trong khi giao dịch đang diễn ra. Trong những trường hợp đó, tôi nghĩ rằng các giải pháp dựa vào việc xả sau session.add () và xử lý ngoại lệ như stackoverflow.com/a/21146492/3690333 đáng tin cậy hơn.
TrilceAC

3

Về mặt ngữ nghĩa gần nhất có lẽ là:

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

không chắc nó sẽ dựa vào thế nào Sessiontrong việc xác định toàn cầu trong sqlalchemy, nhưng phiên bản Django không có kết nối nên ...

Bộ dữ liệu được trả về chứa cá thể và boolean cho biết nếu cá thể đó được tạo ra (tức là Sai nếu chúng ta đọc cá thể từ db).

Django get_or_createthường được sử dụng để đảm bảo rằng dữ liệu toàn cầu có sẵn, vì vậy tôi cam kết ở điểm sớm nhất có thể.


điều này sẽ hoạt động miễn là Phiên được tạo và theo dõi bởi scoped_session, phiên này sẽ triển khai quản lý phiên an toàn theo luồng (điều này có tồn tại trong năm 2014 không?).
cowbert

2

Tôi hơi đơn giản hóa @Kevin. giải pháp để tránh gói toàn bộ hàm trong một câu lệnh if/ else. Cách này chỉ có một return, mà tôi thấy sạch hơn:

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

Tùy thuộc vào mức độ cô lập mà bạn áp dụng, không có giải pháp nào ở trên sẽ hoạt động. Giải pháp tốt nhất tôi đã tìm thấy là một SQL RAW ở dạng sau:

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

Điều này là an toàn giao dịch bất kể mức độ cô lập và mức độ song song là gì.

Chú ý: để làm cho nó hiệu quả, sẽ là khôn ngoan khi có INDEX cho cột duy nhất.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.