트랜잭션

참고: 새로운 애플리케이션을 빌드하는 개발자는 NDB 클라이언트 라이브러리를 사용하는 것이 좋습니다. NDB 클라이언트 라이브러리는 이 클라이언트 라이브러리와 비교할 때 Memcache API를 통한 자동 항목 캐싱과 같은 여러 이점이 있습니다. 현재 이전 DB 클라이언트 라이브러리를 사용 중인 경우 DB에서 NDB로의 마이그레이션 가이드를 참조하세요.

Datastore는 트랜잭션을 지원합니다. 트랜잭션은 원자성(트랜잭션의 모든 작업이 발생하거나 발생하지 않음)인 하나의 작업 또는 작업 집합입니다. 애플리케이션은 단일 트랜잭션으로 여러 작업과 계산을 수행할 수 있습니다.

트랜잭션 사용

트랜잭션은 하나 이상의 항목에 대한 Datastore 작업 집합입니다. 각 트랜잭션에서는 원자성이 보장됩니다. 이는 트랜잭션의 일부분만 적용되는 경우는 없음을 의미합니다. 트랜잭션의 모든 작업이 적용되거나 하나도 적용되지 않거나, 둘 중 하나입니다. 트랜잭션의 최대 지속 시간은 60초이며, 30초 후에 10초의 유휴 만료 시간이 있습니다.

다음과 같은 경우 작업이 실패할 수 있습니다.

  • 동일한 항목 그룹에서 동시에 실행되는 수정 시도가 너무 많은 경우
  • 트랜잭션이 리소스 한도를 초과하는 경우
  • Datastore에 내부 오류가 발생하는 경우

이러한 모든 경우 Cloud Datastore API에서 예외가 발생합니다.

트랜잭션은 Datastore의 선택적 기능이므로 Datastore 작업을 수행하기 위해 반드시 트랜잭션을 사용할 필요는 없습니다.

애플리케이션은 단일 트랜잭션으로 일련의 문과 Datastore 작업을 실행할 수 있습니다. 따라서 일부 문 또는 작업에서 예외가 발생하면 어떤 Datastore 작업도 적용되지 않습니다. 애플리케이션은 Python 함수를 사용하여 트랜잭션에서 수행할 작업을 정의합니다. 트랜잭션이 단일 항목 그룹 내의 항목에 액세스하는지 또는 트랜잭션이 교차 그룹 트랜잭션인지에 따라 애플리케이션은 run_in_transaction 메서드 중 하나를 사용하여 트랜잭션을 시작합니다.

트랜잭션 내에서만 사용되는 일반적인 함수 사용 사례는 @db.transactional 데코레이터를 사용하세요.

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

@db.transactional
def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

increment_counter(acc.key(), 5)

함수가 트랜잭션 없이 호출되는 경우 데코레이터를 사용하는 대신 함수를 인수로 하여 db.run_in_transaction()을 호출하세요.

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

db.run_in_transaction(increment_counter, acc.key(), 5)

db.run_in_transaction()은 함수 객체, 그리고 함수에 전달할 위치 및 키워드 인수를 받습니다. 함수가 값을 반환하면 db.run_in_transaction()은 해당 값을 반환합니다.

함수가 반환하면 트랜잭션이 커밋되고 Datastore 작업의 모든 효과가 적용됩니다. 함수에서 예외가 발생하면 트랜잭션이 '롤백'되고 효과가 적용되지 않습니다. 예외에 대해서는 위 메모를 참조하세요.

한 트랜잭션 함수가 다른 트랜잭션 내에서 호출되면 @db.transactionaldb.run_in_transaction()의 기본 동작이 다릅니다. @db.transactional은 이를 허용하며 내부 트랜잭션이 외부 트랜잭션과 동일한 트랜잭션이 됩니다. db.run_in_transaction()을 호출하면 기존 트랜잭션 내에 다른 트랜잭션을 '중첩'하려는 시도가 수행되지만 이 동작은 아직 지원되지 않으며 db.BadRequestError가 발생합니다. 다른 동작을 지정할 수 있습니다. 자세한 내용은 트랜잭션 옵션을 참조하세요.

교차 그룹(XG) 트랜잭션 사용

여러 항목 그룹에 걸쳐 작동하는 교차 그룹 트랜잭션은 단일 그룹 트랜잭션과 동일하게 작동하지만 코드가 둘 이상의 항목 그룹에서 항목을 업데이트하려고 시도하더라도 실패하지 않습니다. 교차 그룹 트랜잭션을 호출하려면 트랜잭션 옵션을 사용합니다.

@db.transactional 사용:

from google.appengine.ext import db

@db.transactional(xg=True)
def make_things():
  thing1 = Thing(a=3)
  thing1.put()
  thing2 = Thing(a=7)
  thing2.put()

make_things()

db.run_in_transaction_options 사용:

from google.appengine.ext import db

xg_on = db.create_transaction_options(xg=True)

def my_txn():
    x = MyModel(a=3)
    x.put()
    y = MyModel(a=7)
    y.put()

db.run_in_transaction_options(xg_on, my_txn)

트랜잭션에서 가능한 작업

Datastore는 단일 트랜잭션 내부에서 가능한 작업을 제한합니다.

트랜잭션이 단일 그룹 트랜잭션인 경우 트랜잭션의 모든 Datastore 작업은 같은 항목 그룹의 항목에서 작동해야 합니다. 또는 트랜잭션이 교차 그룹 트랜잭션인 경우 최대 25개 항목 그룹의 항목에서 작동해야 합니다. 여기에는 상위 그룹별 항목 쿼리, 키별 항목 쿼리, 항목 업데이트, 항목 삭제가 포함됩니다. 각 루트 항목은 별도의 항목 그룹에 속하므로 교차 그룹 트랜잭션이 아닌 한 두 개 이상의 루트 항목에서 단일 트랜잭션을 생성 또는 작동할 수 없습니다.

2개 이상의 트랜잭션이 동시에 하나 이상의 공통된 항목 그룹에 있는 항목을 수정하려고 시도하면 변경 사항을 커밋한 첫 번째 트랜잭션만 성공하고 나머지 모든 항목은 커밋에 실패합니다. 설계상 항목 그룹을 사용하면 임의의 그룹 항목에 동시에 실행할 수 있는 쓰기 수가 제한됩니다 트랜잭션이 시작되면 Datastore에서는 트랜잭션에 사용되는 항목 그룹의 최종 업데이트 시간을 확인하여 최적의 동시 실행 제어를 수행합니다. 항목 그룹에 트랜잭션을 커밋하면 Datastore가 트랜잭션에 사용된 항목 그룹의 최종 업데이트 시간을 다시 확인합니다. 최초 확인 이후 변경된 경우 예외가 발생합니다.

앱은 상위 필터를 포함한 경우에만 트랜잭션 중에 쿼리를 수행할 수 있습니다. 또한 앱은 트랜잭션 중에 키별로 Datastore 항목을 가져올 수 있습니다. 트랜잭션 전에 키를 준비하거나 트랜잭션 내에서 키 이름 또는 ID를 사용하여 키를 만들 수 있습니다.

다른 모든 Python 코드는 트랜잭션 함수 내에서 허용됩니다. db.is_in_transaction()을 사용하여 현재 범위가 트랜잭션 함수에 중첩되는지 여부를 확인할 수 있습니다. 트랜잭션 함수에는 Datastore 작업 이외의 부수적 효과는 없어야 합니다. 다른 사용자가 동시에 항목 그룹에 항목을 업데이트 중이라서 Datastore 작업이 실패하는 경우 트랜잭션 함수가 여러 번 호출될 수 있습니다. 이러한 경우가 발생하면 Datastore API는 고정된 횟수만큼 트랜잭션을 재시도합니다. 모두 실패하면 db.run_in_transaction()TransactionFailedError를 발생시킵니다. db.run_in_transaction () 대신 db.run_in_transaction_custom_retries()를 사용하여 트랜잭션 재시도 횟수를 조정할 수 있습니다.

마찬가지로, 트랜잭션 함수를 호출하는 코드가 부수적 효과를 되돌리는 방법을 아는 경우를 제외하고 트랜잭션 함수에는 트랜잭션의 성공에 따른 부작용이 없어야 합니다. 예를 들어 트랜잭션이 새 Datastore 항목을 저장하고, 나중에 사용할 수 있도록 생성된 항목의 ID를 저장하면 트랜잭션은 실패하며 항목 생성이 롤백되었기 때문에 저장된 ID는 의도된 항목을 참조하지 않습니다. 이 경우 호출하는 함수는 저장된 ID를 사용하지 않도록 주의를 기울여야 합니다.

격리 및 일관성

트랜잭션 외에 Datastore의 격리 수준은 커밋된 읽기에 가장 근접합니다. 트랜잭션 내에서 직렬 가능한 격리가 적용됩니다. 즉, 다른 트랜잭션은 이 트랜잭션에서 읽거나 수정한 데이터를 동시에 수정할 수 없습니다.

트랜잭션의 모든 읽기에는 트랜잭션이 시작된 시점에 Datastore의 현재 일관된 상태가 반영됩니다. 트랜잭션 내 쿼리 및 조회를 통해 트랜잭션 시작 시점을 기준으로 Datastore의 일관된 단일 스냅샷을 확인할 수 있습니다. 트랜잭션 항목 그룹 내의 항목 및 색인 행은 완전히 업데이트되므로 트랜잭션 외부의 쿼리에서 발생할 수 있는 거짓 양성 또는 거짓 음성 없이 완전하고 정확한 결과 항목 집합이 반환됩니다.

이 일관된 스냅샷 뷰는 트랜잭션 내 쓰기 후 읽기까지도 확장됩니다. 대부분의 데이터베이스와 달리 Datastore 트랜잭션 내 쿼리 및 가져오기는 트랜잭션 내에서 이전에 발생한 쓰기 결과를 보여주지 않습니다. 특히 트랜잭션 내에서 항목이 수정되거나 삭제된 경우 쿼리 또는 가져오기를 수행하면 트랜잭션 시작 시점의 원래 버전 항목이 반환되거나, 그 당시에 항목이 존재하지 않았다면 아무것도 반환되지 않습니다.

트랜잭션 용도

다음 예시에서는 트랜잭션의 용도 중 하나로서 항목의 현재 값을 기준으로 항목을 새 속성 값으로 업데이트하는 방법을 보여줍니다.

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

여기서는 이 코드가 객체를 불러온 후, 수정된 객체를 저장하기 전에 다른 사용자에 의해 값이 업데이트될 수 있으므로 트랜잭션이 필요합니다. 트랜잭션이 없으면 사용자의 요청은 다른 사용자의 업데이트 전에 count 값을 사용하고 새 값을 덮어씁니다. 트랜잭션이 있으면 애플리케이션에 다른 사용자의 업데이트가 전달됩니다. 트랜잭션 중에 항목이 업데이트되는 경우 모든 단계가 중단 없이 완료될 때까지 트랜잭션이 재시도됩니다.

트랜잭션의 다른 일반적인 용도는 명명된 키로 항목을 가져오거나 항목이 아직 존재하지 않으면 생성하는 것입니다.

class SalesAccount(db.Model):
    address = db.PostalAddressProperty()
    phone_number = db.PhoneNumberProperty()

def get_or_create(parent_key, account_id, address, phone_number):
    obj = db.get(db.Key.from_path("SalesAccount", account_id, parent=parent_key))
    if not obj:
        obj = SalesAccount(key_name=account_id,
                           parent=parent_key,
                           address=address,
                           phone_number=phone_number)
        obj.put()
    else:
        obj.address = address
        obj.phone_number = phone_number

이전과 마찬가지로 트랜잭션은 다른 사용자가 동일한 문자열 ID의 항목을 만들거나 업데이트하려 하는 경우를 처리해야 합니다. 트랜잭션이 없는 경우 항목이 없는 상태에서 사용자 두 명이 항목을 만들려고 하면 두 번째 항목이 첫 번째 생성 사실을 알지 못한 채 첫 번째 항목을 덮어씁니다. 트랜잭션을 사용하면 두 번째 시도가 재시도되면서 항목이 현재 존재함을 인식하고 항목을 덮어쓰는 대신 업데이트합니다.

트랜잭션이 실패하는 경우 성공할 때까지 재시도하도록 앱을 설정하거나 앱의 사용자 인터페이스 수준에 전달하여 사용자가 오류에 대처하도록 할 수 있습니다. 모든 트랜잭션에서 재시도 루프를 생성할 필요는 없습니다.

가져오기 또는 만들기는 매우 유용하므로 이를 위한 메서드가 기본 제공됩니다. Model.get_or_insert()는 키 이름, 선택사항인 상위 요소, 그리고 해당 이름 및 경로를 가진 항목이 없는 경우 모델 생성자에 전달할 인수를 사용합니다. 가져오기 시도와 만들기는 하나의 트랜잭션에서 일어나므로 트랜잭션이 성공적인 경우 메서드는 항상 실제 항목을 나타내는 모델 인스턴스를 반환합니다.

마지막으로 트랜잭션을 사용하여 Datastore의 일관된 스냅샷을 읽을 수 있습니다. 이는 페이지를 렌더링하거나 일관성이 필요한 데이터를 내보내기 위해 여러 번의 읽기가 필요한 경우 유용할 수 있습니다. 이러한 종류의 트랜잭션은 쓰기를 수행하지 않으므로 보통 읽기 전용 트랜잭션이라고 합니다. 읽기 전용 단일 그룹 트랜잭션은 동시 수정으로 인해 실패하는 경우가 없으므로 실패 시 재시도를 구현할 필요가 없습니다. 그러나 교차 그룹 트랜잭션은 동시 수정으로 인해 실패할 수 있으므로 재시도를 포함해야 합니다. 읽기 전용 트랜잭션의 커밋과 롤백은 모두 무운영입니다.

class Customer(db.Model):
    user = db.StringProperty()

class Account(db.Model):
    """An Account has a Customer as its parent."""
    address = db.PostalAddressProperty()
    balance = db.FloatProperty()

def get_all_accounts():
    """Returns a consistent view of the current user's accounts."""
    accounts = []
    for customer in Customer.all().filter('user =', users.get_current_user().user_id()):
        accounts.extend(Account.all().ancestor(customer))
    return accounts

큐에 트랜잭션 태스크 추가

Datastore 트랜잭션의 일부로 큐에 작업을 추가하면 트랜잭션이 성공적으로 커밋되는 경우에만 큐에 작업이 추가됩니다. 트랜잭션이 커밋되지 않으면 작업은 큐에 추가되지 않습니다. 트랜잭션이 커밋되면 작업이 대기열에 추가됩니다. 대기열에 추가된 작업은 즉시 실행되지 않으므로 작업은 트랜잭션에서 원자성을 갖지 않습니다. 일단 큐에 추가된 태스크는 성공할 때까지 재시도됩니다. 이는 run_in_transaction() 함수 실행 중에 큐에 추가된 모든 태스크에 적용됩니다.

트랜잭션 작업은 Datastore 이외의 작업을 해당 트랜잭션이 성공해야 실행되는 트랜잭션(예: 구매 확인을 위한 이메일 전송)에 결합할 수 있다는 면에서 유용합니다. 또한 Datastore 작업을 트랜잭션에 연결하여 해당 트랜잭션이 성공하는 경우에만 트랜잭션 외부의 항목 그룹에 대한 변경사항을 커밋할 수 있습니다.

애플리케이션은 단일 트랜잭션 중에 태스크 큐트랜잭션 태스크를 5개를 초과하여 삽입할 수 없습니다. 트랜잭션 태스크의 이름은 사용자가 지정한 이름이 아니어야 합니다.

def do_something_in_transaction(...)
    taskqueue.add(url='/path/to/my/worker', transactional=True)
  ...

db.run_in_transaction(do_something_in_transaction, ....)